mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 19:12:04 +08:00
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:
@@ -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]
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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": "無効な形式です。小文字、数字、ハイフンのみ使用可能。",
|
||||||
|
|||||||
@@ -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": "标识格式无效,只能使用小写字母、数字和连字符",
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user