refactor(endpoints): implement deferred submission and fix clear-all bug
Implement Solution A (complete deferred submission) for custom endpoint
management, replacing the dual-mode system with unified local staging.
Changes:
- Remove immediate backend saves from EndpointSpeedTest
* handleAddEndpoint: local state update only
* handleRemoveEndpoint: local state update only
* handleSelect: remove lastUsed timestamp update
- Add explicit clear detection in ProviderForm
* Distinguish "user cleared endpoints" from "user didn't modify"
* Pass empty object {} as clear signal vs null for no-change
- Fix mergeProviderMeta to handle three distinct cases:
* null/undefined: don't modify endpoints (no meta sent)
* empty object {}: explicitly clear endpoints (send empty meta)
* with data: add/update endpoints (overwrite)
Fixed Critical Bug:
When users deleted all custom endpoints, changes were not saved because:
- draftCustomEndpoints=[] resulted in customEndpointsToSave=null
- mergeProviderMeta(meta, null) returned undefined
- Backend interpreted missing meta as "don't modify", preserving old values
Solution:
Detect when user had endpoints and cleared them (hadEndpoints && length===0),
then pass empty object to mergeProviderMeta as explicit clear signal.
Architecture Improvements:
- Transaction atomicity: all fields submitted together on form save
- UX consistency: add/edit modes behave identically
- Cancel button: true rollback with no immediate saves
- Code simplification: removed ~40 lines of immediate save error handling
Testing:
- TypeScript type check: passed
- Rust backend tests: 10/10 passed
- Build: successful
This commit is contained in:
@@ -440,21 +440,16 @@ impl ProviderService {
|
|||||||
let merged = if let Some(existing) = manager.providers.get(&provider_id) {
|
let merged = if let Some(existing) = manager.providers.get(&provider_id) {
|
||||||
let mut updated = provider_clone.clone();
|
let mut updated = provider_clone.clone();
|
||||||
match (existing.meta.as_ref(), updated.meta.take()) {
|
match (existing.meta.as_ref(), updated.meta.take()) {
|
||||||
|
// 前端未提供 meta,表示不修改,沿用旧值
|
||||||
(Some(old_meta), None) => {
|
(Some(old_meta), None) => {
|
||||||
updated.meta = Some(old_meta.clone());
|
updated.meta = Some(old_meta.clone());
|
||||||
}
|
}
|
||||||
(Some(old_meta), Some(mut new_meta)) => {
|
(None, None) => {
|
||||||
let mut merged_map = old_meta.custom_endpoints.clone();
|
updated.meta = None;
|
||||||
for (url, ep) in new_meta.custom_endpoints.drain() {
|
|
||||||
merged_map.entry(url).or_insert(ep);
|
|
||||||
}
|
|
||||||
updated.meta = Some(ProviderMeta {
|
|
||||||
custom_endpoints: merged_map,
|
|
||||||
usage_script: new_meta.usage_script.clone(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
(None, maybe_new) => {
|
// 前端提供的 meta 视为权威,直接覆盖(其中 custom_endpoints 允许是空,表示删除所有自定义端点)
|
||||||
updated.meta = maybe_new;
|
(_old, Some(new_meta)) => {
|
||||||
|
updated.meta = Some(new_meta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updated
|
updated
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export function EditProviderDialog({
|
|||||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||||
<ProviderForm
|
<ProviderForm
|
||||||
appId={appId}
|
appId={appId}
|
||||||
|
providerId={provider.id}
|
||||||
submitLabel={t("common.save")}
|
submitLabel={t("common.save")}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onCancel={() => onOpenChange(false)}
|
onCancel={() => onOpenChange(false)}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface EndpointCandidate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ClaudeFormFieldsProps {
|
interface ClaudeFormFieldsProps {
|
||||||
|
providerId?: string;
|
||||||
// API Key
|
// API Key
|
||||||
shouldShowApiKey: boolean;
|
shouldShowApiKey: boolean;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@@ -53,6 +54,7 @@ interface ClaudeFormFieldsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ClaudeFormFields({
|
export function ClaudeFormFields({
|
||||||
|
providerId,
|
||||||
shouldShowApiKey,
|
shouldShowApiKey,
|
||||||
apiKey,
|
apiKey,
|
||||||
onApiKeyChange,
|
onApiKeyChange,
|
||||||
@@ -144,6 +146,7 @@ export function ClaudeFormFields({
|
|||||||
{shouldShowSpeedTest && isEndpointModalOpen && (
|
{shouldShowSpeedTest && isEndpointModalOpen && (
|
||||||
<EndpointSpeedTest
|
<EndpointSpeedTest
|
||||||
appId="claude"
|
appId="claude"
|
||||||
|
providerId={providerId}
|
||||||
value={baseUrl}
|
value={baseUrl}
|
||||||
onChange={onBaseUrlChange}
|
onChange={onBaseUrlChange}
|
||||||
initialEndpoints={speedTestEndpoints}
|
initialEndpoints={speedTestEndpoints}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface EndpointCandidate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CodexFormFieldsProps {
|
interface CodexFormFieldsProps {
|
||||||
|
providerId?: string;
|
||||||
// API Key
|
// API Key
|
||||||
codexApiKey: string;
|
codexApiKey: string;
|
||||||
onApiKeyChange: (key: string) => void;
|
onApiKeyChange: (key: string) => void;
|
||||||
@@ -28,6 +29,7 @@ interface CodexFormFieldsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CodexFormFields({
|
export function CodexFormFields({
|
||||||
|
providerId,
|
||||||
codexApiKey,
|
codexApiKey,
|
||||||
onApiKeyChange,
|
onApiKeyChange,
|
||||||
category,
|
category,
|
||||||
@@ -81,6 +83,7 @@ export function CodexFormFields({
|
|||||||
{shouldShowSpeedTest && isEndpointModalOpen && (
|
{shouldShowSpeedTest && isEndpointModalOpen && (
|
||||||
<EndpointSpeedTest
|
<EndpointSpeedTest
|
||||||
appId="codex"
|
appId="codex"
|
||||||
|
providerId={providerId}
|
||||||
value={codexBaseUrl}
|
value={codexBaseUrl}
|
||||||
onChange={onBaseUrlChange}
|
onChange={onBaseUrlChange}
|
||||||
initialEndpoints={speedTestEndpoints}
|
initialEndpoints={speedTestEndpoints}
|
||||||
|
|||||||
@@ -281,70 +281,35 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
|
|
||||||
setAddError(null);
|
setAddError(null);
|
||||||
|
|
||||||
// 保存到后端
|
// 更新本地状态(延迟提交,不立即保存到后端)
|
||||||
try {
|
setEntries((prev) => {
|
||||||
if (providerId) {
|
if (prev.some((e) => e.url === sanitized)) return prev;
|
||||||
await vscodeApi.addCustomEndpoint(appId, providerId, sanitized);
|
return [
|
||||||
}
|
...prev,
|
||||||
|
{
|
||||||
|
id: randomId(),
|
||||||
|
url: sanitized,
|
||||||
|
isCustom: true,
|
||||||
|
latency: null,
|
||||||
|
status: undefined,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
// 更新本地状态
|
if (!normalizedSelected) {
|
||||||
setEntries((prev) => {
|
onChange(sanitized);
|
||||||
if (prev.some((e) => e.url === sanitized)) return prev;
|
|
||||||
return [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: randomId(),
|
|
||||||
url: sanitized,
|
|
||||||
isCustom: true,
|
|
||||||
latency: null,
|
|
||||||
status: undefined,
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!normalizedSelected) {
|
|
||||||
onChange(sanitized);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCustomUrl("");
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
setAddError(message || t("endpointTest.saveFailed"));
|
|
||||||
console.error(t("endpointTest.addEndpointFailed"), error);
|
|
||||||
}
|
}
|
||||||
}, [customUrl, entries, normalizedSelected, onChange, appId, providerId, t]);
|
|
||||||
|
setCustomUrl("");
|
||||||
|
}, [customUrl, entries, normalizedSelected, onChange]);
|
||||||
|
|
||||||
const handleRemoveEndpoint = useCallback(
|
const handleRemoveEndpoint = useCallback(
|
||||||
async (entry: EndpointEntry) => {
|
(entry: EndpointEntry) => {
|
||||||
// 清空之前的错误提示
|
// 清空之前的错误提示
|
||||||
setLastError(null);
|
setLastError(null);
|
||||||
|
|
||||||
// 如果有 providerId,尝试从后端删除
|
// 更新本地状态(延迟提交,不立即从后端删除)
|
||||||
if (entry.isCustom && providerId) {
|
|
||||||
try {
|
|
||||||
await vscodeApi.removeCustomEndpoint(appId, providerId, entry.url);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMsg =
|
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
|
|
||||||
// 只有"端点不存在"时才允许删除本地条目
|
|
||||||
if (
|
|
||||||
errorMsg.includes("not found") ||
|
|
||||||
errorMsg.includes("does not exist") ||
|
|
||||||
errorMsg.includes("不存在")
|
|
||||||
) {
|
|
||||||
console.warn(t("endpointTest.removeEndpointFailed"), errorMsg);
|
|
||||||
// 继续删除本地条目
|
|
||||||
} else {
|
|
||||||
// 其他错误:显示错误提示,阻止删除
|
|
||||||
setLastError(t("endpointTest.removeFailed", { error: errorMsg }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新本地状态(删除成功)
|
|
||||||
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) {
|
||||||
@@ -354,7 +319,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[normalizedSelected, onChange, appId, providerId, t],
|
[normalizedSelected, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const runSpeedTest = useCallback(async () => {
|
const runSpeedTest = useCallback(async () => {
|
||||||
@@ -432,22 +397,11 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
}, [entries, autoSelect, appId, normalizedSelected, onChange, t]);
|
}, [entries, autoSelect, appId, normalizedSelected, onChange, t]);
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
async (url: string) => {
|
(url: string) => {
|
||||||
if (!url || url === normalizedSelected) return;
|
if (!url || url === normalizedSelected) return;
|
||||||
|
|
||||||
// 更新最后使用时间(对自定义端点)
|
|
||||||
const entry = entries.find((e) => e.url === url);
|
|
||||||
if (entry?.isCustom && providerId) {
|
|
||||||
try {
|
|
||||||
await vscodeApi.updateEndpointLastUsed(appId, providerId, url);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(t("endpointTest.updateLastUsedFailed"), error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(url);
|
onChange(url);
|
||||||
},
|
},
|
||||||
[normalizedSelected, onChange, appId, entries, providerId, t],
|
[normalizedSelected, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
useModelState,
|
useModelState,
|
||||||
useCodexConfigState,
|
useCodexConfigState,
|
||||||
useApiKeyLink,
|
useApiKeyLink,
|
||||||
useCustomEndpoints,
|
|
||||||
useTemplateValues,
|
useTemplateValues,
|
||||||
useCommonConfigSnippet,
|
useCommonConfigSnippet,
|
||||||
useCodexCommonConfig,
|
useCodexCommonConfig,
|
||||||
@@ -48,6 +47,7 @@ type PresetEntry = {
|
|||||||
|
|
||||||
interface ProviderFormProps {
|
interface ProviderFormProps {
|
||||||
appId: AppId;
|
appId: AppId;
|
||||||
|
providerId?: string;
|
||||||
submitLabel: string;
|
submitLabel: string;
|
||||||
onSubmit: (values: ProviderFormValues) => void;
|
onSubmit: (values: ProviderFormValues) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@@ -63,6 +63,7 @@ interface ProviderFormProps {
|
|||||||
|
|
||||||
export function ProviderForm({
|
export function ProviderForm({
|
||||||
appId,
|
appId,
|
||||||
|
providerId,
|
||||||
submitLabel,
|
submitLabel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
@@ -82,8 +83,15 @@ export function ProviderForm({
|
|||||||
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
|
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
|
||||||
|
|
||||||
// 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints
|
// 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints
|
||||||
|
// 编辑供应商:从 initialData.meta.custom_endpoints 恢复端点列表
|
||||||
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>(
|
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>(
|
||||||
[],
|
() => {
|
||||||
|
if (!initialData?.meta?.custom_endpoints) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// 从 Record<string, CustomEndpoint> 中提取 URL 列表
|
||||||
|
return Object.keys(initialData.meta.custom_endpoints);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 使用 category hook
|
// 使用 category hook
|
||||||
@@ -97,6 +105,13 @@ export function ProviderForm({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedPresetId(initialData ? null : "custom");
|
setSelectedPresetId(initialData ? null : "custom");
|
||||||
setActivePreset(null);
|
setActivePreset(null);
|
||||||
|
|
||||||
|
// 重新初始化 draftCustomEndpoints(编辑模式时从 meta 恢复)
|
||||||
|
if (initialData?.meta?.custom_endpoints) {
|
||||||
|
setDraftCustomEndpoints(Object.keys(initialData.meta.custom_endpoints));
|
||||||
|
} else {
|
||||||
|
setDraftCustomEndpoints([]);
|
||||||
|
}
|
||||||
}, [appId, initialData]);
|
}, [appId, initialData]);
|
||||||
|
|
||||||
const defaultValues: ProviderFormData = useMemo(
|
const defaultValues: ProviderFormData = useMemo(
|
||||||
@@ -272,7 +287,7 @@ export function ProviderForm({
|
|||||||
type: "manual",
|
type: "manual",
|
||||||
message: t("providerForm.fillParameter", {
|
message: t("providerForm.fillParameter", {
|
||||||
label: validation.missingField.label,
|
label: validation.missingField.label,
|
||||||
defaultValue: `请填写 ${validation.missingField.label}`,
|
defaultValue: `<EFBFBD><EFBFBD><EFBFBD>填写 ${validation.missingField.label}`,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -313,8 +328,35 @@ export function ProviderForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 meta 字段(新建与编辑使用不同策略)
|
// 处理 meta 字段:基于 draftCustomEndpoints 生成 custom_endpoints
|
||||||
const mergedMeta = mergeProviderMeta(initialData?.meta, customEndpointsMap);
|
// 注意:不使用 customEndpointsMap,因为它包含了候选端点(预设、Base URL 等)
|
||||||
|
// 而我们只需要保存用户真正添加的自定义端点
|
||||||
|
const customEndpointsToSave: Record<string, import("@/types").CustomEndpoint> | null =
|
||||||
|
draftCustomEndpoints.length > 0
|
||||||
|
? draftCustomEndpoints.reduce((acc, url) => {
|
||||||
|
// 尝试从 initialData.meta 中获取原有的端点元数据(保留 addedAt 和 lastUsed)
|
||||||
|
const existing = initialData?.meta?.custom_endpoints?.[url];
|
||||||
|
if (existing) {
|
||||||
|
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 = initialData?.meta?.custom_endpoints &&
|
||||||
|
Object.keys(initialData.meta.custom_endpoints).length > 0;
|
||||||
|
const needsClearEndpoints = hadEndpoints && draftCustomEndpoints.length === 0;
|
||||||
|
|
||||||
|
// 如果用户明确清空了端点,传递空对象(而不是 null)让后端知道要删除
|
||||||
|
const mergedMeta = needsClearEndpoints
|
||||||
|
? mergeProviderMeta(initialData?.meta, {})
|
||||||
|
: mergeProviderMeta(initialData?.meta, customEndpointsToSave);
|
||||||
|
|
||||||
if (mergedMeta) {
|
if (mergedMeta) {
|
||||||
payload.meta = mergedMeta;
|
payload.meta = mergedMeta;
|
||||||
}
|
}
|
||||||
@@ -369,16 +411,6 @@ export function ProviderForm({
|
|||||||
formWebsiteUrl: form.watch("websiteUrl") || "",
|
formWebsiteUrl: form.watch("websiteUrl") || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 使用自定义端点 hook
|
|
||||||
const customEndpointsMap = useCustomEndpoints({
|
|
||||||
appId,
|
|
||||||
selectedPresetId,
|
|
||||||
presetEntries,
|
|
||||||
draftCustomEndpoints,
|
|
||||||
baseUrl,
|
|
||||||
codexBaseUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用端点测速候选 hook
|
// 使用端点测速候选 hook
|
||||||
const speedTestEndpoints = useSpeedTestEndpoints({
|
const speedTestEndpoints = useSpeedTestEndpoints({
|
||||||
appId,
|
appId,
|
||||||
@@ -473,6 +505,7 @@ export function ProviderForm({
|
|||||||
{/* Claude 专属字段 */}
|
{/* Claude 专属字段 */}
|
||||||
{appId === "claude" && (
|
{appId === "claude" && (
|
||||||
<ClaudeFormFields
|
<ClaudeFormFields
|
||||||
|
providerId={providerId}
|
||||||
shouldShowApiKey={shouldShowApiKey(
|
shouldShowApiKey={shouldShowApiKey(
|
||||||
form.watch("settingsConfig"),
|
form.watch("settingsConfig"),
|
||||||
isEditMode,
|
isEditMode,
|
||||||
@@ -505,6 +538,7 @@ export function ProviderForm({
|
|||||||
{/* Codex 专属字段 */}
|
{/* Codex 专属字段 */}
|
||||||
{appId === "codex" && (
|
{appId === "codex" && (
|
||||||
<CodexFormFields
|
<CodexFormFields
|
||||||
|
providerId={providerId}
|
||||||
codexApiKey={codexApiKey}
|
codexApiKey={codexApiKey}
|
||||||
onApiKeyChange={handleCodexApiKeyChange}
|
onApiKeyChange={handleCodexApiKeyChange}
|
||||||
category={category}
|
category={category}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import type { CustomEndpoint, ProviderMeta } from "@/types";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 合并供应商元数据中的自定义端点。
|
* 合并供应商元数据中的自定义端点。
|
||||||
* - 当 customEndpoints 为空对象或 null 时,移除自定义端点但保留其它元数据。
|
* - 当 customEndpoints 为空对象时,明确删除自定义端点但保留其它元数据。
|
||||||
|
* - 当 customEndpoints 为 null/undefined 时,不修改端点(保留原有端点)。
|
||||||
* - 当 customEndpoints 存在时,覆盖原有自定义端点。
|
* - 当 customEndpoints 存在时,覆盖原有自定义端点。
|
||||||
* - 若结果为空对象则返回 undefined,避免写入空 meta。
|
* - 若结果为空对象且非明确清空场景则返回 undefined,避免写入空 meta。
|
||||||
*/
|
*/
|
||||||
export function mergeProviderMeta(
|
export function mergeProviderMeta(
|
||||||
initialMeta: ProviderMeta | undefined,
|
initialMeta: ProviderMeta | undefined,
|
||||||
@@ -13,6 +14,12 @@ export function mergeProviderMeta(
|
|||||||
const hasCustomEndpoints =
|
const hasCustomEndpoints =
|
||||||
!!customEndpoints && Object.keys(customEndpoints).length > 0;
|
!!customEndpoints && Object.keys(customEndpoints).length > 0;
|
||||||
|
|
||||||
|
// 明确清空:传入空对象(非 null/undefined)表示用户想要删除所有端点
|
||||||
|
const isExplicitClear =
|
||||||
|
customEndpoints !== null &&
|
||||||
|
customEndpoints !== undefined &&
|
||||||
|
Object.keys(customEndpoints).length === 0;
|
||||||
|
|
||||||
if (hasCustomEndpoints) {
|
if (hasCustomEndpoints) {
|
||||||
return {
|
return {
|
||||||
...(initialMeta ? { ...initialMeta } : {}),
|
...(initialMeta ? { ...initialMeta } : {}),
|
||||||
@@ -20,6 +27,25 @@ export function mergeProviderMeta(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 明确清空端点
|
||||||
|
if (isExplicitClear) {
|
||||||
|
if (!initialMeta) {
|
||||||
|
// 新供应商且用户没有添加端点(理论上不会到这里)
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("custom_endpoints" in initialMeta) {
|
||||||
|
const { custom_endpoints, ...rest } = initialMeta;
|
||||||
|
// 保留其他字段(如 usage_script)
|
||||||
|
// 即使 rest 为空,也要返回空对象(让后端知道要清空 meta)
|
||||||
|
return Object.keys(rest).length > 0 ? rest : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialMeta 中本来就没有 custom_endpoints
|
||||||
|
return { ...initialMeta };
|
||||||
|
}
|
||||||
|
|
||||||
|
// null/undefined:用户没有修改端点,保持不变
|
||||||
if (!initialMeta) {
|
if (!initialMeta) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user