feat(provider): support additive provider key lifecycle management

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.
This commit is contained in:
YoVinchen
2026-03-28 01:49:07 +08:00
parent eaf83f4fbe
commit e9ead2841d
13 changed files with 282 additions and 53 deletions
+6 -2
View File
@@ -36,9 +36,11 @@ pub fn add_provider(
state: State<'_, AppState>, state: State<'_, AppState>,
app: String, app: String,
provider: Provider, provider: Provider,
#[allow(non_snake_case)] addToLive: Option<bool>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; 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] #[tauri::command]
@@ -46,9 +48,11 @@ pub fn update_provider(
state: State<'_, AppState>, state: State<'_, AppState>,
app: String, app: String,
provider: Provider, provider: Provider,
#[allow(non_snake_case)] originalId: Option<String>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; 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] #[tauri::command]
+1 -1
View File
@@ -110,7 +110,7 @@ pub fn import_provider_from_deeplink(
let provider_id = provider.id.clone(); let provider_id = provider.id.clone();
// Use ProviderService to add the provider // 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) // Add extra endpoints as custom endpoints (skip first one as it's the primary)
for ep in all_endpoints.iter().skip(1) { for ep in all_endpoints.iter().skip(1) {
+15
View File
@@ -33,6 +33,21 @@ pub(crate) fn sanitize_claude_settings_for_live(settings: &Value) -> Value {
v v
} }
pub(crate) fn provider_exists_in_live_config(
app_type: &AppType,
provider_id: &str,
) -> Result<bool, AppError> {
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 { fn json_is_subset(target: &Value, source: &Value) -> bool {
match source { match source {
Value::Object(source_map) => { Value::Object(source_map) => {
+57 -4
View File
@@ -29,7 +29,8 @@ pub use live::{
pub(crate) use live::sanitize_claude_settings_for_live; pub(crate) use live::sanitize_claude_settings_for_live;
pub(crate) use live::{ pub(crate) use live::{
build_effective_settings_with_common_config, normalize_provider_common_config_for_storage, 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, write_live_with_common_config,
}; };
@@ -166,7 +167,12 @@ impl ProviderService {
} }
/// Add a new provider /// Add a new provider
pub fn add(state: &AppState, app_type: AppType, provider: Provider) -> Result<bool, AppError> { pub fn add(
state: &AppState,
app_type: AppType,
provider: Provider,
add_to_live: bool,
) -> Result<bool, AppError> {
let mut provider = provider; let mut provider = provider;
// Normalize Claude model keys // Normalize Claude model keys
Self::normalize_provider_if_claude(&app_type, &mut provider); Self::normalize_provider_if_claude(&app_type, &mut provider);
@@ -176,7 +182,7 @@ impl ProviderService {
// Save to database // Save to database
state.db.save_provider(app_type.as_str(), &provider)?; 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() { if app_type.is_additive_mode() {
// OMO / OMO Slim providers use exclusive mode and write to dedicated config file. // OMO / OMO Slim providers use exclusive mode and write to dedicated config file.
if matches!(app_type, AppType::OpenCode) if matches!(app_type, AppType::OpenCode)
@@ -186,6 +192,9 @@ impl ProviderService {
// Users must explicitly switch/apply an OMO provider to activate it. // Users must explicitly switch/apply an OMO provider to activate it.
return Ok(true); return Ok(true);
} }
if !add_to_live {
return Ok(true);
}
write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?; write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?;
return Ok(true); return Ok(true);
} }
@@ -207,18 +216,59 @@ impl ProviderService {
pub fn update( pub fn update(
state: &AppState, state: &AppState,
app_type: AppType, app_type: AppType,
original_id: Option<&str>,
provider: Provider, provider: Provider,
) -> Result<bool, AppError> { ) -> Result<bool, AppError> {
let mut provider = provider; 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 // Normalize Claude model keys
Self::normalize_provider_if_claude(&app_type, &mut provider); Self::normalize_provider_if_claude(&app_type, &mut provider);
Self::validate_provider_settings(&app_type, &provider)?; Self::validate_provider_settings(&app_type, &provider)?;
normalize_provider_common_config_for_storage(state.db.as_ref(), &app_type, &mut 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 // Save to database
state.db.save_provider(app_type.as_str(), &provider)?; 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 app_type.is_additive_mode() {
if matches!(app_type, AppType::OpenCode) && provider.category.as_deref() == Some("omo") if matches!(app_type, AppType::OpenCode) && provider.category.as_deref() == Some("omo")
{ {
@@ -250,6 +300,9 @@ impl ProviderService {
} }
return Ok(true); 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)?; write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?;
return Ok(true); return Ok(true);
} }
+13 -5
View File
@@ -533,8 +533,14 @@ function App() {
} }
}; };
const handleEditProvider = async (provider: Provider) => { const handleEditProvider = async ({
await updateProvider(provider); provider,
originalId,
}: {
provider: Provider;
originalId?: string;
}) => {
await updateProvider(provider, originalId);
setEditingProvider(null); setEditingProvider(null);
}; };
@@ -571,7 +577,7 @@ function App() {
setConfirmAction(null); setConfirmAction(null);
}; };
const generateUniqueOpencodeKey = ( const generateUniqueProviderCopyKey = (
originalKey: string, originalKey: string,
existingKeys: string[], existingKeys: string[],
): string => { ): string => {
@@ -594,6 +600,7 @@ function App() {
const duplicatedProvider: Omit<Provider, "id" | "createdAt"> & { const duplicatedProvider: Omit<Provider, "id" | "createdAt"> & {
providerKey?: string; providerKey?: string;
addToLive?: boolean;
} = { } = {
name: `${provider.name} copy`, name: `${provider.name} copy`,
settingsConfig: JSON.parse(JSON.stringify(provider.settingsConfig)), // 深拷贝 settingsConfig: JSON.parse(JSON.stringify(provider.settingsConfig)), // 深拷贝
@@ -607,12 +614,13 @@ function App() {
iconColor: provider.iconColor, iconColor: provider.iconColor,
}; };
if (activeApp === "opencode") { if (activeApp === "opencode" || activeApp === "openclaw") {
const existingKeys = Object.keys(providers); const existingKeys = Object.keys(providers);
duplicatedProvider.providerKey = generateUniqueOpencodeKey( duplicatedProvider.providerKey = generateUniqueProviderCopyKey(
provider.id, provider.id,
existingKeys, existingKeys,
); );
duplicatedProvider.addToLive = false;
} }
if (provider.sortIndex !== undefined) { if (provider.sortIndex !== undefined) {
@@ -14,7 +14,10 @@ interface EditProviderDialogProps {
open: boolean; open: boolean;
provider: Provider | null; provider: Provider | null;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onSubmit: (provider: Provider) => Promise<void> | void; onSubmit: (payload: {
provider: Provider;
originalId?: string;
}) => Promise<void> | void;
appId: AppId; appId: AppId;
isProxyTakeover?: boolean; // 代理接管模式下不读取 live(避免显示被接管后的代理配置) isProxyTakeover?: boolean; // 代理接管模式下不读取 live(避免显示被接管后的代理配置)
} }
@@ -165,9 +168,15 @@ export function EditProviderDialog({
string, string,
unknown unknown
>; >;
const nextProviderId =
(appId === "opencode" || appId === "openclaw") &&
values.providerKey?.trim()
? values.providerKey.trim()
: provider.id;
const updatedProvider: Provider = { const updatedProvider: Provider = {
...provider, ...provider,
id: nextProviderId,
name: values.name.trim(), name: values.name.trim(),
notes: values.notes?.trim() || undefined, notes: values.notes?.trim() || undefined,
websiteUrl: values.websiteUrl?.trim() || undefined, websiteUrl: values.websiteUrl?.trim() || undefined,
@@ -179,10 +188,13 @@ export function EditProviderDialog({
...(values.meta ? { meta: values.meta } : {}), ...(values.meta ? { meta: values.meta } : {}),
}; };
await onSubmit(updatedProvider); await onSubmit({
provider: updatedProvider,
originalId: provider.id,
});
onOpenChange(false); onOpenChange(false);
}, },
[onSubmit, onOpenChange, provider], [appId, onSubmit, onOpenChange, provider],
); );
if (!provider || !initialData) { if (!provider || !initialData) {
+130 -21
View File
@@ -1,13 +1,14 @@
import { useEffect, useMemo, useState, useCallback } from "react"; import { useEffect, useMemo, useState, useCallback } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form"; import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
import type { AppId } from "@/lib/api"; import { providersApi, type AppId } from "@/lib/api";
import type { import type {
ProviderCategory, ProviderCategory,
ProviderMeta, ProviderMeta,
@@ -91,6 +92,7 @@ import {
normalizePricingSource, normalizePricingSource,
} from "./helpers/opencodeFormUtils"; } from "./helpers/opencodeFormUtils";
import { resolveManagedAccountId } from "@/lib/authBinding"; import { resolveManagedAccountId } from "@/lib/authBinding";
import { useOpenClawLiveProviderIds } from "@/hooks/useOpenClaw";
type PresetEntry = { type PresetEntry = {
id: string; id: string;
@@ -569,6 +571,15 @@ export function ProviderForm({
existingOpencodeKeys, existingOpencodeKeys,
} = useOmoModelSource({ isOmoCategory: isAnyOmoCategory, providerId }); } = useOmoModelSource({ isOmoCategory: isAnyOmoCategory, providerId });
const {
data: opencodeLiveProviderIds = [],
isLoading: isOpencodeLiveProviderIdsLoading,
} = useQuery({
queryKey: ["opencodeLiveProviderIds"],
queryFn: () => providersApi.getOpenCodeLiveProviderIds(),
enabled: appId === "opencode" && !isAnyOmoCategory,
});
const opencodeForm = useOpencodeFormState({ const opencodeForm = useOpencodeFormState({
initialData, initialData,
appId, appId,
@@ -597,6 +608,78 @@ export function ProviderForm({
onSettingsConfigChange: (config) => form.setValue("settingsConfig", config), onSettingsConfigChange: (config) => form.setValue("settingsConfig", config),
getSettingsConfig: () => form.getValues("settingsConfig"), 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); const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
@@ -633,9 +716,17 @@ export function ProviderForm({
toast.error(t("opencode.providerKeyInvalid")); toast.error(t("opencode.providerKeyInvalid"));
return; return;
} }
if (isProviderKeyLockStateLoading) {
toast.error(
t("providerForm.providerKeyStatusLoading", {
defaultValue: "正在加载供应商标识状态,请稍后再试",
}),
);
return;
}
if ( if (
!isEditMode && !isProviderKeyLocked &&
existingOpencodeKeys.includes(opencodeForm.opencodeProviderKey) additiveExistingProviderKeys.includes(opencodeForm.opencodeProviderKey)
) { ) {
toast.error(t("opencode.providerKeyDuplicate")); toast.error(t("opencode.providerKeyDuplicate"));
return; return;
@@ -657,9 +748,17 @@ export function ProviderForm({
toast.error(t("openclaw.providerKeyInvalid")); toast.error(t("openclaw.providerKeyInvalid"));
return; return;
} }
if (isProviderKeyLockStateLoading) {
toast.error(
t("providerForm.providerKeyStatusLoading", {
defaultValue: "正在加载供应商标识状态,请稍后再试",
}),
);
return;
}
if ( if (
!isEditMode && !isProviderKeyLocked &&
openclawForm.existingOpenclawKeys.includes( additiveExistingProviderKeys.includes(
openclawForm.openclawProviderKey, openclawForm.openclawProviderKey,
) )
) { ) {
@@ -1240,12 +1339,12 @@ export function ProviderForm({
) )
} }
placeholder={t("opencode.providerKeyPlaceholder")} placeholder={t("opencode.providerKeyPlaceholder")}
disabled={isEditMode} disabled={isProviderKeyLocked || isProviderKeyLockStateLoading}
className={ className={
(existingOpencodeKeys.includes( (additiveExistingProviderKeys.includes(
opencodeForm.opencodeProviderKey, opencodeForm.opencodeProviderKey,
) && ) &&
!isEditMode) || !isProviderKeyLocked) ||
(opencodeForm.opencodeProviderKey.trim() !== "" && (opencodeForm.opencodeProviderKey.trim() !== "" &&
!/^[a-z0-9]+(-[a-z0-9]+)*$/.test( !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(
opencodeForm.opencodeProviderKey, opencodeForm.opencodeProviderKey,
@@ -1254,10 +1353,10 @@ export function ProviderForm({
: "" : ""
} }
/> />
{existingOpencodeKeys.includes( {additiveExistingProviderKeys.includes(
opencodeForm.opencodeProviderKey, opencodeForm.opencodeProviderKey,
) && ) &&
!isEditMode && ( !isProviderKeyLocked && (
<p className="text-xs text-destructive"> <p className="text-xs text-destructive">
{t("opencode.providerKeyDuplicate")} {t("opencode.providerKeyDuplicate")}
</p> </p>
@@ -1271,16 +1370,21 @@ export function ProviderForm({
</p> </p>
)} )}
{!( {!(
existingOpencodeKeys.includes( additiveExistingProviderKeys.includes(
opencodeForm.opencodeProviderKey, opencodeForm.opencodeProviderKey,
) && !isEditMode ) && !isProviderKeyLocked
) && ) &&
(opencodeForm.opencodeProviderKey.trim() === "" || (opencodeForm.opencodeProviderKey.trim() === "" ||
/^[a-z0-9]+(-[a-z0-9]+)*$/.test( /^[a-z0-9]+(-[a-z0-9]+)*$/.test(
opencodeForm.opencodeProviderKey, opencodeForm.opencodeProviderKey,
)) && ( )) && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t("opencode.providerKeyHint")} {isProviderKeyLocked
? t("opencode.providerKeyLockedHint", {
defaultValue:
"该供应商已添加到应用配置中,供应商标识不可修改",
})
: t("opencode.providerKeyHint")}
</p> </p>
)} )}
</div> </div>
@@ -1299,12 +1403,12 @@ export function ProviderForm({
) )
} }
placeholder={t("openclaw.providerKeyPlaceholder")} placeholder={t("openclaw.providerKeyPlaceholder")}
disabled={isEditMode} disabled={isProviderKeyLocked || isProviderKeyLockStateLoading}
className={ className={
(openclawForm.existingOpenclawKeys.includes( (additiveExistingProviderKeys.includes(
openclawForm.openclawProviderKey, openclawForm.openclawProviderKey,
) && ) &&
!isEditMode) || !isProviderKeyLocked) ||
(openclawForm.openclawProviderKey.trim() !== "" && (openclawForm.openclawProviderKey.trim() !== "" &&
!/^[a-z0-9]+(-[a-z0-9]+)*$/.test( !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(
openclawForm.openclawProviderKey, openclawForm.openclawProviderKey,
@@ -1313,10 +1417,10 @@ export function ProviderForm({
: "" : ""
} }
/> />
{openclawForm.existingOpenclawKeys.includes( {additiveExistingProviderKeys.includes(
openclawForm.openclawProviderKey, openclawForm.openclawProviderKey,
) && ) &&
!isEditMode && ( !isProviderKeyLocked && (
<p className="text-xs text-destructive"> <p className="text-xs text-destructive">
{t("openclaw.providerKeyDuplicate")} {t("openclaw.providerKeyDuplicate")}
</p> </p>
@@ -1330,16 +1434,21 @@ export function ProviderForm({
</p> </p>
)} )}
{!( {!(
openclawForm.existingOpenclawKeys.includes( additiveExistingProviderKeys.includes(
openclawForm.openclawProviderKey, openclawForm.openclawProviderKey,
) && !isEditMode ) && !isProviderKeyLocked
) && ) &&
(openclawForm.openclawProviderKey.trim() === "" || (openclawForm.openclawProviderKey.trim() === "" ||
/^[a-z0-9]+(-[a-z0-9]+)*$/.test( /^[a-z0-9]+(-[a-z0-9]+)*$/.test(
openclawForm.openclawProviderKey, openclawForm.openclawProviderKey,
)) && ( )) && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t("openclaw.providerKeyHint")} {isProviderKeyLocked
? t("openclaw.providerKeyLockedHint", {
defaultValue:
"该供应商已添加到应用配置中,供应商标识不可修改",
})
: t("openclaw.providerKeyHint")}
</p> </p>
)} )}
</div> </div>
+3 -2
View File
@@ -65,6 +65,7 @@ export function useProviderActions(activeApp: AppId) {
provider: Omit<Provider, "id"> & { provider: Omit<Provider, "id"> & {
providerKey?: string; providerKey?: string;
suggestedDefaults?: OpenClawSuggestedDefaults; suggestedDefaults?: OpenClawSuggestedDefaults;
addToLive?: boolean;
}, },
) => { ) => {
await addProviderMutation.mutateAsync(provider); await addProviderMutation.mutateAsync(provider);
@@ -120,8 +121,8 @@ export function useProviderActions(activeApp: AppId) {
// 更新供应商 // 更新供应商
const updateProvider = useCallback( const updateProvider = useCallback(
async (provider: Provider) => { async (provider: Provider, originalId?: string) => {
await updateProviderMutation.mutateAsync(provider); await updateProviderMutation.mutateAsync({ provider, originalId });
// 更新托盘菜单(失败不影响主操作) // 更新托盘菜单(失败不影响主操作)
try { try {
+4 -2
View File
@@ -908,7 +908,8 @@
"modelsRequired": "Please add at least one model", "modelsRequired": "Please add at least one model",
"providerKey": "Provider Key", "providerKey": "Provider Key",
"providerKeyPlaceholder": "my-provider", "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", "providerKeyRequired": "Provider key is required",
"providerKeyDuplicate": "This key is already in use", "providerKeyDuplicate": "This key is already in use",
"providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.", "providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.",
@@ -1367,7 +1368,8 @@
"backupCreated": "Backup created: {{path}}", "backupCreated": "Backup created: {{path}}",
"providerKey": "Provider Key", "providerKey": "Provider Key",
"providerKeyPlaceholder": "my-provider", "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", "providerKeyRequired": "Provider key is required",
"providerKeyDuplicate": "This key is already in use", "providerKeyDuplicate": "This key is already in use",
"providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.", "providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.",
+4 -2
View File
@@ -908,7 +908,8 @@
"modelsRequired": "モデルを少なくとも1つ追加してください", "modelsRequired": "モデルを少なくとも1つ追加してください",
"providerKey": "プロバイダーキー", "providerKey": "プロバイダーキー",
"providerKeyPlaceholder": "my-provider", "providerKeyPlaceholder": "my-provider",
"providerKeyHint": "設定ファイルの一意の識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用できます。", "providerKeyHint": "設定ファイルの一意の識別子です。小文字、数字、ハイフンのみ使用できます。",
"providerKeyLockedHint": "このプロバイダーは既にアプリ設定へ追加されているため、キーは変更できません。",
"providerKeyRequired": "プロバイダーキーを入力してください", "providerKeyRequired": "プロバイダーキーを入力してください",
"providerKeyDuplicate": "このキーは既に使用されています", "providerKeyDuplicate": "このキーは既に使用されています",
"providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用できます。", "providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用できます。",
@@ -1367,7 +1368,8 @@
"backupCreated": "バックアップを作成しました: {{path}}", "backupCreated": "バックアップを作成しました: {{path}}",
"providerKey": "プロバイダーキー", "providerKey": "プロバイダーキー",
"providerKeyPlaceholder": "my-provider", "providerKeyPlaceholder": "my-provider",
"providerKeyHint": "設定ファイル内のユニーク識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用可能。", "providerKeyHint": "設定ファイル内のユニーク識別子。小文字、数字、ハイフンのみ使用可能。",
"providerKeyLockedHint": "このプロバイダーは既にアプリ設定へ追加されているため、キーは変更できません。",
"providerKeyRequired": "プロバイダーキーを入力してください", "providerKeyRequired": "プロバイダーキーを入力してください",
"providerKeyDuplicate": "このキーは既に使用されています", "providerKeyDuplicate": "このキーは既に使用されています",
"providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用可能。", "providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用可能。",
+4 -2
View File
@@ -908,7 +908,8 @@
"modelsRequired": "请至少添加一个模型配置", "modelsRequired": "请至少添加一个模型配置",
"providerKey": "供应商标识", "providerKey": "供应商标识",
"providerKeyPlaceholder": "my-provider", "providerKeyPlaceholder": "my-provider",
"providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符", "providerKeyHint": "配置文件中的唯一标识符,只能使用小写字母、数字和连字符",
"providerKeyLockedHint": "该供应商已添加到应用配置中,供应商标识不可修改",
"providerKeyRequired": "请填写供应商标识", "providerKeyRequired": "请填写供应商标识",
"providerKeyDuplicate": "此标识已被使用,请更换", "providerKeyDuplicate": "此标识已被使用,请更换",
"providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符", "providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符",
@@ -1367,7 +1368,8 @@
"backupCreated": "已创建备份:{{path}}", "backupCreated": "已创建备份:{{path}}",
"providerKey": "供应商标识", "providerKey": "供应商标识",
"providerKeyPlaceholder": "my-provider", "providerKeyPlaceholder": "my-provider",
"providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符", "providerKeyHint": "配置文件中的唯一标识符,只能使用小写字母、数字和连字符",
"providerKeyLockedHint": "该供应商已添加到应用配置中,供应商标识不可修改",
"providerKeyRequired": "请填写供应商标识", "providerKeyRequired": "请填写供应商标识",
"providerKeyDuplicate": "此标识已被使用,请更换", "providerKeyDuplicate": "此标识已被使用,请更换",
"providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符", "providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符",
+12 -4
View File
@@ -30,12 +30,20 @@ export const providersApi = {
return await invoke("get_current_provider", { app: appId }); return await invoke("get_current_provider", { app: appId });
}, },
async add(provider: Provider, appId: AppId): Promise<boolean> { async add(
return await invoke("add_provider", { provider, app: appId }); provider: Provider,
appId: AppId,
addToLive?: boolean,
): Promise<boolean> {
return await invoke("add_provider", { provider, app: appId, addToLive });
}, },
async update(provider: Provider, appId: AppId): Promise<boolean> { async update(
return await invoke("update_provider", { provider, app: appId }); provider: Provider,
appId: AppId,
originalId?: string,
): Promise<boolean> {
return await invoke("update_provider", { provider, app: appId, originalId });
}, },
async delete(id: string, appId: AppId): Promise<boolean> { async delete(id: string, appId: AppId): Promise<boolean> {
+18 -5
View File
@@ -15,7 +15,10 @@ export const useAddProviderMutation = (appId: AppId) => {
return useMutation({ return useMutation({
mutationFn: async ( mutationFn: async (
providerInput: Omit<Provider, "id"> & { providerKey?: string }, providerInput: Omit<Provider, "id"> & {
providerKey?: string;
addToLive?: boolean;
},
) => { ) => {
let id: string; let id: string;
@@ -36,7 +39,11 @@ export const useAddProviderMutation = (appId: AppId) => {
id = generateUUID(); id = generateUUID();
} }
const { providerKey: _providerKey, ...rest } = providerInput; const {
providerKey: _providerKey,
addToLive,
...rest
} = providerInput;
const newProvider: Provider = { const newProvider: Provider = {
...rest, ...rest,
@@ -45,7 +52,7 @@ export const useAddProviderMutation = (appId: AppId) => {
}; };
delete (newProvider as any).providerKey; delete (newProvider as any).providerKey;
await providersApi.add(newProvider, appId); await providersApi.add(newProvider, appId, addToLive);
return newProvider; return newProvider;
}, },
onSuccess: async () => { onSuccess: async () => {
@@ -107,8 +114,14 @@ export const useUpdateProviderMutation = (appId: AppId) => {
const { t } = useTranslation(); const { t } = useTranslation();
return useMutation({ return useMutation({
mutationFn: async (provider: Provider) => { mutationFn: async ({
await providersApi.update(provider, appId); provider,
originalId,
}: {
provider: Provider;
originalId?: string;
}) => {
await providersApi.update(provider, appId, originalId);
return provider; return provider;
}, },
onSuccess: async () => { onSuccess: async () => {