From e9ead2841db6d161e02cad2644fdcd4d151af013 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Sat, 28 Mar 2026 01:49:07 +0800 Subject: [PATCH] feat(provider): support additive provider key lifecycle management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `addToLive` parameter to add_provider so callers can opt out of writing to the live config (e.g. when duplicating an inactive provider). Add `originalId` parameter to update_provider to support provider key renames — the old key is removed from live config before the new one is written. Frontend: ProviderForm now exposes provider-key input for openclaw app type, and EditProviderDialog forwards originalId on save. Deep-link import passes addToLive=true to preserve existing behavior. --- src-tauri/src/commands/provider.rs | 8 +- src-tauri/src/deeplink/provider.rs | 2 +- src-tauri/src/services/provider/live.rs | 15 ++ src-tauri/src/services/provider/mod.rs | 61 ++++++- src/App.tsx | 18 ++- .../providers/EditProviderDialog.tsx | 18 ++- .../providers/forms/ProviderForm.tsx | 151 +++++++++++++++--- src/hooks/useProviderActions.ts | 5 +- src/i18n/locales/en.json | 6 +- src/i18n/locales/ja.json | 6 +- src/i18n/locales/zh.json | 6 +- src/lib/api/providers.ts | 16 +- src/lib/query/mutations.ts | 23 ++- 13 files changed, 282 insertions(+), 53 deletions(-) diff --git a/src-tauri/src/commands/provider.rs b/src-tauri/src/commands/provider.rs index 883f8d3b9..e56b4402d 100644 --- a/src-tauri/src/commands/provider.rs +++ b/src-tauri/src/commands/provider.rs @@ -36,9 +36,11 @@ pub fn add_provider( state: State<'_, AppState>, app: String, provider: Provider, + #[allow(non_snake_case)] addToLive: Option, ) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; - ProviderService::add(state.inner(), app_type, provider).map_err(|e| e.to_string()) + ProviderService::add(state.inner(), app_type, provider, addToLive.unwrap_or(true)) + .map_err(|e| e.to_string()) } #[tauri::command] @@ -46,9 +48,11 @@ pub fn update_provider( state: State<'_, AppState>, app: String, provider: Provider, + #[allow(non_snake_case)] originalId: Option, ) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; - ProviderService::update(state.inner(), app_type, provider).map_err(|e| e.to_string()) + ProviderService::update(state.inner(), app_type, originalId.as_deref(), provider) + .map_err(|e| e.to_string()) } #[tauri::command] diff --git a/src-tauri/src/deeplink/provider.rs b/src-tauri/src/deeplink/provider.rs index cdb0f3e39..d94189481 100644 --- a/src-tauri/src/deeplink/provider.rs +++ b/src-tauri/src/deeplink/provider.rs @@ -110,7 +110,7 @@ pub fn import_provider_from_deeplink( let provider_id = provider.id.clone(); // Use ProviderService to add the provider - ProviderService::add(state, app_type.clone(), provider)?; + ProviderService::add(state, app_type.clone(), provider, true)?; // Add extra endpoints as custom endpoints (skip first one as it's the primary) for ep in all_endpoints.iter().skip(1) { diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index b4b3aa0fd..cba4daf26 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -33,6 +33,21 @@ pub(crate) fn sanitize_claude_settings_for_live(settings: &Value) -> Value { v } +pub(crate) fn provider_exists_in_live_config( + app_type: &AppType, + provider_id: &str, +) -> Result { + match app_type { + AppType::OpenCode => crate::opencode_config::get_providers() + .map(|providers| providers.contains_key(provider_id)) + .map_err(Into::into), + AppType::OpenClaw => crate::openclaw_config::get_providers() + .map(|providers| providers.contains_key(provider_id)) + .map_err(Into::into), + _ => Ok(false), + } +} + fn json_is_subset(target: &Value, source: &Value) -> bool { match source { Value::Object(source_map) => { diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index bbae722ca..360abdd06 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -29,7 +29,8 @@ pub use live::{ pub(crate) use live::sanitize_claude_settings_for_live; pub(crate) use live::{ build_effective_settings_with_common_config, normalize_provider_common_config_for_storage, - strip_common_config_from_live_settings, sync_current_provider_for_app_to_live, + provider_exists_in_live_config, strip_common_config_from_live_settings, + sync_current_provider_for_app_to_live, write_live_with_common_config, }; @@ -166,7 +167,12 @@ impl ProviderService { } /// Add a new provider - pub fn add(state: &AppState, app_type: AppType, provider: Provider) -> Result { + pub fn add( + state: &AppState, + app_type: AppType, + provider: Provider, + add_to_live: bool, + ) -> Result { let mut provider = provider; // Normalize Claude model keys Self::normalize_provider_if_claude(&app_type, &mut provider); @@ -176,7 +182,7 @@ impl ProviderService { // Save to database state.db.save_provider(app_type.as_str(), &provider)?; - // Additive mode apps (OpenCode, OpenClaw) - always write to live config + // Additive mode apps (OpenCode, OpenClaw): optionally write to live config. if app_type.is_additive_mode() { // OMO / OMO Slim providers use exclusive mode and write to dedicated config file. if matches!(app_type, AppType::OpenCode) @@ -186,6 +192,9 @@ impl ProviderService { // Users must explicitly switch/apply an OMO provider to activate it. return Ok(true); } + if !add_to_live { + return Ok(true); + } write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?; return Ok(true); } @@ -207,18 +216,59 @@ impl ProviderService { pub fn update( state: &AppState, app_type: AppType, + original_id: Option<&str>, provider: Provider, ) -> Result { let mut provider = provider; + let original_id = original_id.unwrap_or(provider.id.as_str()).to_string(); + let provider_id_changed = original_id != provider.id; // Normalize Claude model keys Self::normalize_provider_if_claude(&app_type, &mut provider); Self::validate_provider_settings(&app_type, &provider)?; normalize_provider_common_config_for_storage(state.db.as_ref(), &app_type, &mut provider)?; + if provider_id_changed { + if !app_type.is_additive_mode() { + return Err(AppError::Message( + "Only additive-mode providers support changing provider key".to_string(), + )); + } + + if provider_exists_in_live_config(&app_type, &original_id)? { + return Err(AppError::Message( + "Provider key cannot be changed after the provider has been added to the app config" + .to_string(), + )); + } + + if state + .db + .get_provider_by_id(&provider.id, app_type.as_str())? + .is_some() + || provider_exists_in_live_config(&app_type, &provider.id)? + { + return Err(AppError::Message(format!( + "Provider '{}' already exists in app '{}'", + provider.id, + app_type.as_str() + ))); + } + + state.db.save_provider(app_type.as_str(), &provider)?; + state.db.delete_provider(app_type.as_str(), &original_id)?; + + if crate::settings::get_current_provider(&app_type).as_deref() == Some(&original_id) { + crate::settings::set_current_provider(&app_type, Some(provider.id.as_str()))?; + } + + return Ok(true); + } + // Save to database state.db.save_provider(app_type.as_str(), &provider)?; - // Additive mode apps (OpenCode, OpenClaw) - always update in live config + // Additive mode apps (OpenCode, OpenClaw): only sync to live when the provider + // already exists in live config. Editing a DB-only provider must not auto-add it. if app_type.is_additive_mode() { if matches!(app_type, AppType::OpenCode) && provider.category.as_deref() == Some("omo") { @@ -250,6 +300,9 @@ impl ProviderService { } return Ok(true); } + if !provider_exists_in_live_config(&app_type, &provider.id)? { + return Ok(true); + } write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?; return Ok(true); } diff --git a/src/App.tsx b/src/App.tsx index 5ee35ddc5..48f24f2e5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -533,8 +533,14 @@ function App() { } }; - const handleEditProvider = async (provider: Provider) => { - await updateProvider(provider); + const handleEditProvider = async ({ + provider, + originalId, + }: { + provider: Provider; + originalId?: string; + }) => { + await updateProvider(provider, originalId); setEditingProvider(null); }; @@ -571,7 +577,7 @@ function App() { setConfirmAction(null); }; - const generateUniqueOpencodeKey = ( + const generateUniqueProviderCopyKey = ( originalKey: string, existingKeys: string[], ): string => { @@ -594,6 +600,7 @@ function App() { const duplicatedProvider: Omit & { providerKey?: string; + addToLive?: boolean; } = { name: `${provider.name} copy`, settingsConfig: JSON.parse(JSON.stringify(provider.settingsConfig)), // 深拷贝 @@ -607,12 +614,13 @@ function App() { iconColor: provider.iconColor, }; - if (activeApp === "opencode") { + if (activeApp === "opencode" || activeApp === "openclaw") { const existingKeys = Object.keys(providers); - duplicatedProvider.providerKey = generateUniqueOpencodeKey( + duplicatedProvider.providerKey = generateUniqueProviderCopyKey( provider.id, existingKeys, ); + duplicatedProvider.addToLive = false; } if (provider.sortIndex !== undefined) { diff --git a/src/components/providers/EditProviderDialog.tsx b/src/components/providers/EditProviderDialog.tsx index 072fdb64d..9b2d5bbbe 100644 --- a/src/components/providers/EditProviderDialog.tsx +++ b/src/components/providers/EditProviderDialog.tsx @@ -14,7 +14,10 @@ interface EditProviderDialogProps { open: boolean; provider: Provider | null; onOpenChange: (open: boolean) => void; - onSubmit: (provider: Provider) => Promise | void; + onSubmit: (payload: { + provider: Provider; + originalId?: string; + }) => Promise | void; appId: AppId; isProxyTakeover?: boolean; // 代理接管模式下不读取 live(避免显示被接管后的代理配置) } @@ -165,9 +168,15 @@ export function EditProviderDialog({ string, unknown >; + const nextProviderId = + (appId === "opencode" || appId === "openclaw") && + values.providerKey?.trim() + ? values.providerKey.trim() + : provider.id; const updatedProvider: Provider = { ...provider, + id: nextProviderId, name: values.name.trim(), notes: values.notes?.trim() || undefined, websiteUrl: values.websiteUrl?.trim() || undefined, @@ -179,10 +188,13 @@ export function EditProviderDialog({ ...(values.meta ? { meta: values.meta } : {}), }; - await onSubmit(updatedProvider); + await onSubmit({ + provider: updatedProvider, + originalId: provider.id, + }); onOpenChange(false); }, - [onSubmit, onOpenChange, provider], + [appId, onSubmit, onOpenChange, provider], ); if (!provider || !initialData) { diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 8ee74daf6..5c91e9f56 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -1,13 +1,14 @@ import { useEffect, useMemo, useState, useCallback } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; -import type { AppId } from "@/lib/api"; +import { providersApi, type AppId } from "@/lib/api"; import type { ProviderCategory, ProviderMeta, @@ -91,6 +92,7 @@ import { normalizePricingSource, } from "./helpers/opencodeFormUtils"; import { resolveManagedAccountId } from "@/lib/authBinding"; +import { useOpenClawLiveProviderIds } from "@/hooks/useOpenClaw"; type PresetEntry = { id: string; @@ -569,6 +571,15 @@ export function ProviderForm({ existingOpencodeKeys, } = useOmoModelSource({ isOmoCategory: isAnyOmoCategory, providerId }); + const { + data: opencodeLiveProviderIds = [], + isLoading: isOpencodeLiveProviderIdsLoading, + } = useQuery({ + queryKey: ["opencodeLiveProviderIds"], + queryFn: () => providersApi.getOpenCodeLiveProviderIds(), + enabled: appId === "opencode" && !isAnyOmoCategory, + }); + const opencodeForm = useOpencodeFormState({ initialData, appId, @@ -597,6 +608,78 @@ export function ProviderForm({ onSettingsConfigChange: (config) => form.setValue("settingsConfig", config), getSettingsConfig: () => form.getValues("settingsConfig"), }); + const { + data: openclawLiveProviderIds = [], + isLoading: isOpenclawLiveProviderIdsLoading, + } = useOpenClawLiveProviderIds(appId === "openclaw"); + + const additiveExistingProviderKeys = useMemo(() => { + if (appId === "opencode" && !isAnyOmoCategory) { + return Array.from( + new Set( + [...existingOpencodeKeys, ...opencodeLiveProviderIds].filter( + (key) => key !== providerId, + ), + ), + ); + } + + if (appId === "openclaw") { + return Array.from( + new Set( + [ + ...openclawForm.existingOpenclawKeys, + ...openclawLiveProviderIds, + ].filter((key) => key !== providerId), + ), + ); + } + + return []; + }, [ + appId, + existingOpencodeKeys, + isAnyOmoCategory, + openclawForm.existingOpenclawKeys, + openclawLiveProviderIds, + opencodeLiveProviderIds, + providerId, + ]); + + const isProviderKeyLockStateLoading = useMemo(() => { + if (!isEditMode) return false; + if (appId === "opencode" && !isAnyOmoCategory) { + return isOpencodeLiveProviderIdsLoading; + } + if (appId === "openclaw") { + return isOpenclawLiveProviderIdsLoading; + } + return false; + }, [ + appId, + isAnyOmoCategory, + isEditMode, + isOpenclawLiveProviderIdsLoading, + isOpencodeLiveProviderIdsLoading, + ]); + + const isProviderKeyLocked = useMemo(() => { + if (!isEditMode || !providerId) return false; + if (appId === "opencode" && !isAnyOmoCategory) { + return opencodeLiveProviderIds.includes(providerId); + } + if (appId === "openclaw") { + return openclawLiveProviderIds.includes(providerId); + } + return false; + }, [ + appId, + isAnyOmoCategory, + isEditMode, + openclawLiveProviderIds, + opencodeLiveProviderIds, + providerId, + ]); const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); @@ -633,9 +716,17 @@ export function ProviderForm({ toast.error(t("opencode.providerKeyInvalid")); return; } + if (isProviderKeyLockStateLoading) { + toast.error( + t("providerForm.providerKeyStatusLoading", { + defaultValue: "正在加载供应商标识状态,请稍后再试", + }), + ); + return; + } if ( - !isEditMode && - existingOpencodeKeys.includes(opencodeForm.opencodeProviderKey) + !isProviderKeyLocked && + additiveExistingProviderKeys.includes(opencodeForm.opencodeProviderKey) ) { toast.error(t("opencode.providerKeyDuplicate")); return; @@ -657,9 +748,17 @@ export function ProviderForm({ toast.error(t("openclaw.providerKeyInvalid")); return; } + if (isProviderKeyLockStateLoading) { + toast.error( + t("providerForm.providerKeyStatusLoading", { + defaultValue: "正在加载供应商标识状态,请稍后再试", + }), + ); + return; + } if ( - !isEditMode && - openclawForm.existingOpenclawKeys.includes( + !isProviderKeyLocked && + additiveExistingProviderKeys.includes( openclawForm.openclawProviderKey, ) ) { @@ -1240,12 +1339,12 @@ export function ProviderForm({ ) } placeholder={t("opencode.providerKeyPlaceholder")} - disabled={isEditMode} + disabled={isProviderKeyLocked || isProviderKeyLockStateLoading} className={ - (existingOpencodeKeys.includes( + (additiveExistingProviderKeys.includes( opencodeForm.opencodeProviderKey, ) && - !isEditMode) || + !isProviderKeyLocked) || (opencodeForm.opencodeProviderKey.trim() !== "" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test( opencodeForm.opencodeProviderKey, @@ -1254,10 +1353,10 @@ export function ProviderForm({ : "" } /> - {existingOpencodeKeys.includes( + {additiveExistingProviderKeys.includes( opencodeForm.opencodeProviderKey, ) && - !isEditMode && ( + !isProviderKeyLocked && (

{t("opencode.providerKeyDuplicate")}

@@ -1271,16 +1370,21 @@ export function ProviderForm({

)} {!( - existingOpencodeKeys.includes( + additiveExistingProviderKeys.includes( opencodeForm.opencodeProviderKey, - ) && !isEditMode + ) && !isProviderKeyLocked ) && (opencodeForm.opencodeProviderKey.trim() === "" || /^[a-z0-9]+(-[a-z0-9]+)*$/.test( opencodeForm.opencodeProviderKey, )) && (

- {t("opencode.providerKeyHint")} + {isProviderKeyLocked + ? t("opencode.providerKeyLockedHint", { + defaultValue: + "该供应商已添加到应用配置中,供应商标识不可修改", + }) + : t("opencode.providerKeyHint")}

)} @@ -1299,12 +1403,12 @@ export function ProviderForm({ ) } placeholder={t("openclaw.providerKeyPlaceholder")} - disabled={isEditMode} + disabled={isProviderKeyLocked || isProviderKeyLockStateLoading} className={ - (openclawForm.existingOpenclawKeys.includes( + (additiveExistingProviderKeys.includes( openclawForm.openclawProviderKey, ) && - !isEditMode) || + !isProviderKeyLocked) || (openclawForm.openclawProviderKey.trim() !== "" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test( openclawForm.openclawProviderKey, @@ -1313,10 +1417,10 @@ export function ProviderForm({ : "" } /> - {openclawForm.existingOpenclawKeys.includes( + {additiveExistingProviderKeys.includes( openclawForm.openclawProviderKey, ) && - !isEditMode && ( + !isProviderKeyLocked && (

{t("openclaw.providerKeyDuplicate")}

@@ -1330,16 +1434,21 @@ export function ProviderForm({

)} {!( - openclawForm.existingOpenclawKeys.includes( + additiveExistingProviderKeys.includes( openclawForm.openclawProviderKey, - ) && !isEditMode + ) && !isProviderKeyLocked ) && (openclawForm.openclawProviderKey.trim() === "" || /^[a-z0-9]+(-[a-z0-9]+)*$/.test( openclawForm.openclawProviderKey, )) && (

- {t("openclaw.providerKeyHint")} + {isProviderKeyLocked + ? t("openclaw.providerKeyLockedHint", { + defaultValue: + "该供应商已添加到应用配置中,供应商标识不可修改", + }) + : t("openclaw.providerKeyHint")}

)} diff --git a/src/hooks/useProviderActions.ts b/src/hooks/useProviderActions.ts index bc11722db..7d0a919a3 100644 --- a/src/hooks/useProviderActions.ts +++ b/src/hooks/useProviderActions.ts @@ -65,6 +65,7 @@ export function useProviderActions(activeApp: AppId) { provider: Omit & { providerKey?: string; suggestedDefaults?: OpenClawSuggestedDefaults; + addToLive?: boolean; }, ) => { await addProviderMutation.mutateAsync(provider); @@ -120,8 +121,8 @@ export function useProviderActions(activeApp: AppId) { // 更新供应商 const updateProvider = useCallback( - async (provider: Provider) => { - await updateProviderMutation.mutateAsync(provider); + async (provider: Provider, originalId?: string) => { + await updateProviderMutation.mutateAsync({ provider, originalId }); // 更新托盘菜单(失败不影响主操作) try { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 017e11260..df3e9eabc 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -908,7 +908,8 @@ "modelsRequired": "Please add at least one model", "providerKey": "Provider Key", "providerKeyPlaceholder": "my-provider", - "providerKeyHint": "Unique identifier in config file. Cannot be changed after creation. Use lowercase letters, numbers, and hyphens only.", + "providerKeyHint": "Unique identifier in config file. Use lowercase letters, numbers, and hyphens only.", + "providerKeyLockedHint": "This provider has already been added to the app config, so its key can no longer be changed.", "providerKeyRequired": "Provider key is required", "providerKeyDuplicate": "This key is already in use", "providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.", @@ -1367,7 +1368,8 @@ "backupCreated": "Backup created: {{path}}", "providerKey": "Provider Key", "providerKeyPlaceholder": "my-provider", - "providerKeyHint": "Unique identifier in config file. Cannot be changed after creation. Use lowercase letters, numbers, and hyphens only.", + "providerKeyHint": "Unique identifier in config file. Use lowercase letters, numbers, and hyphens only.", + "providerKeyLockedHint": "This provider has already been added to the app config, so its key can no longer be changed.", "providerKeyRequired": "Provider key is required", "providerKeyDuplicate": "This key is already in use", "providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 39917cf47..508034a7d 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -908,7 +908,8 @@ "modelsRequired": "モデルを少なくとも1つ追加してください", "providerKey": "プロバイダーキー", "providerKeyPlaceholder": "my-provider", - "providerKeyHint": "設定ファイルの一意の識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用できます。", + "providerKeyHint": "設定ファイルの一意の識別子です。小文字、数字、ハイフンのみ使用できます。", + "providerKeyLockedHint": "このプロバイダーは既にアプリ設定へ追加されているため、キーは変更できません。", "providerKeyRequired": "プロバイダーキーを入力してください", "providerKeyDuplicate": "このキーは既に使用されています", "providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用できます。", @@ -1367,7 +1368,8 @@ "backupCreated": "バックアップを作成しました: {{path}}", "providerKey": "プロバイダーキー", "providerKeyPlaceholder": "my-provider", - "providerKeyHint": "設定ファイル内のユニーク識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用可能。", + "providerKeyHint": "設定ファイル内のユニーク識別子。小文字、数字、ハイフンのみ使用可能。", + "providerKeyLockedHint": "このプロバイダーは既にアプリ設定へ追加されているため、キーは変更できません。", "providerKeyRequired": "プロバイダーキーを入力してください", "providerKeyDuplicate": "このキーは既に使用されています", "providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用可能。", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index ae0446b34..3fb72be83 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -908,7 +908,8 @@ "modelsRequired": "请至少添加一个模型配置", "providerKey": "供应商标识", "providerKeyPlaceholder": "my-provider", - "providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符", + "providerKeyHint": "配置文件中的唯一标识符,只能使用小写字母、数字和连字符", + "providerKeyLockedHint": "该供应商已添加到应用配置中,供应商标识不可修改", "providerKeyRequired": "请填写供应商标识", "providerKeyDuplicate": "此标识已被使用,请更换", "providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符", @@ -1367,7 +1368,8 @@ "backupCreated": "已创建备份:{{path}}", "providerKey": "供应商标识", "providerKeyPlaceholder": "my-provider", - "providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符", + "providerKeyHint": "配置文件中的唯一标识符,只能使用小写字母、数字和连字符", + "providerKeyLockedHint": "该供应商已添加到应用配置中,供应商标识不可修改", "providerKeyRequired": "请填写供应商标识", "providerKeyDuplicate": "此标识已被使用,请更换", "providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符", diff --git a/src/lib/api/providers.ts b/src/lib/api/providers.ts index 47a950630..d11448dcd 100644 --- a/src/lib/api/providers.ts +++ b/src/lib/api/providers.ts @@ -30,12 +30,20 @@ export const providersApi = { return await invoke("get_current_provider", { app: appId }); }, - async add(provider: Provider, appId: AppId): Promise { - return await invoke("add_provider", { provider, app: appId }); + async add( + provider: Provider, + appId: AppId, + addToLive?: boolean, + ): Promise { + return await invoke("add_provider", { provider, app: appId, addToLive }); }, - async update(provider: Provider, appId: AppId): Promise { - return await invoke("update_provider", { provider, app: appId }); + async update( + provider: Provider, + appId: AppId, + originalId?: string, + ): Promise { + return await invoke("update_provider", { provider, app: appId, originalId }); }, async delete(id: string, appId: AppId): Promise { diff --git a/src/lib/query/mutations.ts b/src/lib/query/mutations.ts index e87ab5c13..3cb334b59 100644 --- a/src/lib/query/mutations.ts +++ b/src/lib/query/mutations.ts @@ -15,7 +15,10 @@ export const useAddProviderMutation = (appId: AppId) => { return useMutation({ mutationFn: async ( - providerInput: Omit & { providerKey?: string }, + providerInput: Omit & { + providerKey?: string; + addToLive?: boolean; + }, ) => { let id: string; @@ -36,7 +39,11 @@ export const useAddProviderMutation = (appId: AppId) => { id = generateUUID(); } - const { providerKey: _providerKey, ...rest } = providerInput; + const { + providerKey: _providerKey, + addToLive, + ...rest + } = providerInput; const newProvider: Provider = { ...rest, @@ -45,7 +52,7 @@ export const useAddProviderMutation = (appId: AppId) => { }; delete (newProvider as any).providerKey; - await providersApi.add(newProvider, appId); + await providersApi.add(newProvider, appId, addToLive); return newProvider; }, onSuccess: async () => { @@ -107,8 +114,14 @@ export const useUpdateProviderMutation = (appId: AppId) => { const { t } = useTranslation(); return useMutation({ - mutationFn: async (provider: Provider) => { - await providersApi.update(provider, appId); + mutationFn: async ({ + provider, + originalId, + }: { + provider: Provider; + originalId?: string; + }) => { + await providersApi.update(provider, appId, originalId); return provider; }, onSuccess: async () => {