diff --git a/src-tauri/src/services/provider.rs b/src-tauri/src/services/provider.rs
index 492da7b..e1afb35 100644
--- a/src-tauri/src/services/provider.rs
+++ b/src-tauri/src/services/provider.rs
@@ -440,21 +440,16 @@ impl ProviderService {
let merged = if let Some(existing) = manager.providers.get(&provider_id) {
let mut updated = provider_clone.clone();
match (existing.meta.as_ref(), updated.meta.take()) {
+ // 前端未提供 meta,表示不修改,沿用旧值
(Some(old_meta), None) => {
updated.meta = Some(old_meta.clone());
}
- (Some(old_meta), Some(mut new_meta)) => {
- let mut merged_map = old_meta.custom_endpoints.clone();
- 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, None) => {
+ updated.meta = None;
}
- (None, maybe_new) => {
- updated.meta = maybe_new;
+ // 前端提供的 meta 视为权威,直接覆盖(其中 custom_endpoints 允许是空,表示删除所有自定义端点)
+ (_old, Some(new_meta)) => {
+ updated.meta = Some(new_meta);
}
}
updated
diff --git a/src/components/providers/EditProviderDialog.tsx b/src/components/providers/EditProviderDialog.tsx
index a446d04..46a79d9 100644
--- a/src/components/providers/EditProviderDialog.tsx
+++ b/src/components/providers/EditProviderDialog.tsx
@@ -123,6 +123,7 @@ export function EditProviderDialog({
onOpenChange(false)}
diff --git a/src/components/providers/forms/ClaudeFormFields.tsx b/src/components/providers/forms/ClaudeFormFields.tsx
index 1506ea3..252bace 100644
--- a/src/components/providers/forms/ClaudeFormFields.tsx
+++ b/src/components/providers/forms/ClaudeFormFields.tsx
@@ -11,6 +11,7 @@ interface EndpointCandidate {
}
interface ClaudeFormFieldsProps {
+ providerId?: string;
// API Key
shouldShowApiKey: boolean;
apiKey: string;
@@ -53,6 +54,7 @@ interface ClaudeFormFieldsProps {
}
export function ClaudeFormFields({
+ providerId,
shouldShowApiKey,
apiKey,
onApiKeyChange,
@@ -144,6 +146,7 @@ export function ClaudeFormFields({
{shouldShowSpeedTest && isEndpointModalOpen && (
void;
@@ -28,6 +29,7 @@ interface CodexFormFieldsProps {
}
export function CodexFormFields({
+ providerId,
codexApiKey,
onApiKeyChange,
category,
@@ -81,6 +83,7 @@ export function CodexFormFields({
{shouldShowSpeedTest && isEndpointModalOpen && (
= ({
setAddError(null);
- // 保存到后端
- try {
- if (providerId) {
- await vscodeApi.addCustomEndpoint(appId, providerId, sanitized);
- }
+ // 更新本地状态(延迟提交,不立即保存到后端)
+ setEntries((prev) => {
+ if (prev.some((e) => e.url === sanitized)) return prev;
+ return [
+ ...prev,
+ {
+ id: randomId(),
+ url: sanitized,
+ isCustom: true,
+ latency: null,
+ status: undefined,
+ error: null,
+ },
+ ];
+ });
- // 更新本地状态
- setEntries((prev) => {
- 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);
+ if (!normalizedSelected) {
+ onChange(sanitized);
}
- }, [customUrl, entries, normalizedSelected, onChange, appId, providerId, t]);
+
+ setCustomUrl("");
+ }, [customUrl, entries, normalizedSelected, onChange]);
const handleRemoveEndpoint = useCallback(
- async (entry: EndpointEntry) => {
+ (entry: EndpointEntry) => {
// 清空之前的错误提示
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) => {
const next = prev.filter((item) => item.id !== entry.id);
if (entry.url === normalizedSelected) {
@@ -354,7 +319,7 @@ const EndpointSpeedTest: React.FC = ({
return next;
});
},
- [normalizedSelected, onChange, appId, providerId, t],
+ [normalizedSelected, onChange],
);
const runSpeedTest = useCallback(async () => {
@@ -432,22 +397,11 @@ const EndpointSpeedTest: React.FC = ({
}, [entries, autoSelect, appId, normalizedSelected, onChange, t]);
const handleSelect = useCallback(
- async (url: string) => {
+ (url: string) => {
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);
},
- [normalizedSelected, onChange, appId, entries, providerId, t],
+ [normalizedSelected, onChange],
);
return (
diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx
index 431c528..7c320d5 100644
--- a/src/components/providers/forms/ProviderForm.tsx
+++ b/src/components/providers/forms/ProviderForm.tsx
@@ -30,7 +30,6 @@ import {
useModelState,
useCodexConfigState,
useApiKeyLink,
- useCustomEndpoints,
useTemplateValues,
useCommonConfigSnippet,
useCodexCommonConfig,
@@ -48,6 +47,7 @@ type PresetEntry = {
interface ProviderFormProps {
appId: AppId;
+ providerId?: string;
submitLabel: string;
onSubmit: (values: ProviderFormValues) => void;
onCancel: () => void;
@@ -63,6 +63,7 @@ interface ProviderFormProps {
export function ProviderForm({
appId,
+ providerId,
submitLabel,
onSubmit,
onCancel,
@@ -82,8 +83,15 @@ export function ProviderForm({
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
// 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints
+ // 编辑供应商:从 initialData.meta.custom_endpoints 恢复端点列表
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState(
- [],
+ () => {
+ if (!initialData?.meta?.custom_endpoints) {
+ return [];
+ }
+ // 从 Record 中提取 URL 列表
+ return Object.keys(initialData.meta.custom_endpoints);
+ },
);
// 使用 category hook
@@ -97,6 +105,13 @@ export function ProviderForm({
useEffect(() => {
setSelectedPresetId(initialData ? null : "custom");
setActivePreset(null);
+
+ // 重新初始化 draftCustomEndpoints(编辑模式时从 meta 恢复)
+ if (initialData?.meta?.custom_endpoints) {
+ setDraftCustomEndpoints(Object.keys(initialData.meta.custom_endpoints));
+ } else {
+ setDraftCustomEndpoints([]);
+ }
}, [appId, initialData]);
const defaultValues: ProviderFormData = useMemo(
@@ -272,7 +287,7 @@ export function ProviderForm({
type: "manual",
message: t("providerForm.fillParameter", {
label: validation.missingField.label,
- defaultValue: `请填写 ${validation.missingField.label}`,
+ defaultValue: `���填写 ${validation.missingField.label}`,
}),
});
return;
@@ -313,8 +328,35 @@ export function ProviderForm({
}
}
- // 处理 meta 字段(新建与编辑使用不同策略)
- const mergedMeta = mergeProviderMeta(initialData?.meta, customEndpointsMap);
+ // 处理 meta 字段:基于 draftCustomEndpoints 生成 custom_endpoints
+ // 注意:不使用 customEndpointsMap,因为它包含了候选端点(预设、Base URL 等)
+ // 而我们只需要保存用户真正添加的自定义端点
+ const customEndpointsToSave: Record | 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)
+ : 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) {
payload.meta = mergedMeta;
}
@@ -369,16 +411,6 @@ export function ProviderForm({
formWebsiteUrl: form.watch("websiteUrl") || "",
});
- // 使用自定义端点 hook
- const customEndpointsMap = useCustomEndpoints({
- appId,
- selectedPresetId,
- presetEntries,
- draftCustomEndpoints,
- baseUrl,
- codexBaseUrl,
- });
-
// 使用端点测速候选 hook
const speedTestEndpoints = useSpeedTestEndpoints({
appId,
@@ -473,6 +505,7 @@ export function ProviderForm({
{/* Claude 专属字段 */}
{appId === "claude" && (
0;
+ // 明确清空:传入空对象(非 null/undefined)表示用户想要删除所有端点
+ const isExplicitClear =
+ customEndpoints !== null &&
+ customEndpoints !== undefined &&
+ Object.keys(customEndpoints).length === 0;
+
if (hasCustomEndpoints) {
return {
...(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) {
return undefined;
}