fix: preserve meta.custom_endpoints on update and persist preset candidates on create

- Preserve and merge meta.custom_endpoints in update_provider to avoid losing custom endpoints added via Tauri commands during edit/save. Merge old and incoming meta; keep existing entries and timestamps, add new URLs only.
- Persist endpoint candidates when creating a provider: union of user-added custom endpoints, selected base URL (Claude/Codex), and preset.endpointCandidates; normalize and de-duplicate. Ensures PackyCode keeps all 5 nodes after saving.

Files:
- src-tauri/src/commands.rs
- src/components/ProviderForm.tsx

Validation:
- cargo check passes
- Manual: create from PackyCode preset -> save -> reopen edit -> Manage & Test lists all preset nodes; edit existing provider -> add endpoint -> save -> reopen -> endpoint persists.
This commit is contained in:
Jason
2025-10-10 20:20:08 +08:00
parent c350e64687
commit bfdf7d4ad5
2 changed files with 89 additions and 13 deletions

View File

@@ -217,7 +217,7 @@ pub async fn update_provider(
}
}
// 更新内存并保存
// 更新内存并保存(保留/合并已有的 meta.custom_endpoints避免丢失在编辑流程中新增的自定义端点
{
let mut config = state
.config
@@ -226,9 +226,43 @@ pub async fn update_provider(
let manager = config
.get_manager_mut(&app_type)
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
// 若已存在旧供应商,合并其 meta尤其是 custom_endpoints到新对象
let merged_provider = if let Some(existing) = manager.providers.get(&provider.id) {
// 克隆入参作为基准
let mut updated = provider.clone();
match (existing.meta.as_ref(), updated.meta.take()) {
// 入参未携带 meta直接沿用旧 meta
(Some(old_meta), None) => {
updated.meta = Some(old_meta.clone());
}
// 入参携带 meta与旧 meta 合并(以旧值为准,保留新增项)
(Some(old_meta), Some(mut new_meta)) => {
// 合并 custom_endpointsURL 去重,保留旧端点的时间信息,补充新增端点)
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(crate::provider::ProviderMeta {
custom_endpoints: merged_map,
});
}
// 旧 meta 不存在:使用入参(可能为 None
(None, maybe_new) => {
updated.meta = maybe_new;
}
}
updated
} else {
// 不存在旧供应商(理论上不应发生,因为前面已校验 exists
provider.clone()
};
manager
.providers
.insert(provider.id.clone(), provider.clone());
.insert(merged_provider.id.clone(), merged_provider);
}
state.save()?;

View File

@@ -615,19 +615,61 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
...(category ? { category } : {}),
};
// 若为"新建供应商"且已在弹窗中添加了自定义端点,则随提交一并落盘
if (!initialData && draftCustomEndpoints.length > 0) {
const now = Date.now();
const customMap: Record<string, CustomEndpoint> = {};
for (const raw of draftCustomEndpoints) {
const url = raw.trim().replace(/\/+$/, "");
if (!url) continue;
if (!customMap[url]) {
customMap[url] = { url, addedAt: now, lastUsed: undefined };
// 若为"新建供应商"将端点候选一并随提交落盘到 meta.custom_endpoints
// - 用户在弹窗中新增的自定义端点draftCustomEndpoints已去重
// - 预设中的 endpointCandidates若存在
// - 当前选中的基础 URLbaseUrl/codexBaseUrl
if (!initialData) {
const urlSet = new Set<string>();
const push = (raw?: string) => {
const url = (raw || "").trim().replace(/\/+$/, "");
if (url) urlSet.add(url);
};
// 自定义端点(仅来自用户新增)
for (const u of draftCustomEndpoints) push(u);
// 预设端点候选
if (!isCodex) {
if (
selectedPreset !== null &&
selectedPreset >= 0 &&
selectedPreset < providerPresets.length
) {
const preset = providerPresets[selectedPreset] as any;
if (Array.isArray(preset?.endpointCandidates)) {
for (const u of preset.endpointCandidates as string[]) push(u);
}
}
// 当前 Claude 基础地址
push(baseUrl);
} else {
if (
selectedCodexPreset !== null &&
selectedCodexPreset >= 0 &&
selectedCodexPreset < codexProviderPresets.length
) {
const preset = codexProviderPresets[selectedCodexPreset] as any;
if (Array.isArray(preset?.endpointCandidates)) {
for (const u of preset.endpointCandidates as string[]) push(u);
}
}
// 当前 Codex 基础地址
push(codexBaseUrl);
}
const urls = Array.from(urlSet.values());
if (urls.length > 0) {
const now = Date.now();
const customMap: Record<string, CustomEndpoint> = {};
for (const url of urls) {
if (!customMap[url]) {
customMap[url] = { url, addedAt: now, lastUsed: undefined };
}
}
onSubmit({ ...basePayload, meta: { custom_endpoints: customMap } });
return;
}
onSubmit({ ...basePayload, meta: { custom_endpoints: customMap } });
return;
}
onSubmit(basePayload);