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 () => {