diff --git a/web/src/locales/en.json b/web/src/locales/en.json index f8f815a..c7e9315 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -1,4 +1,4 @@ -{ +{ "common": { "save": "Save", "cancel": "Cancel", @@ -918,6 +918,10 @@ "createError": "Failed to create provider. Please check your connection and try again.", "updateError": "Failed to update provider. Please check your connection and try again.", "saveChanges": "Save Changes", + "clone": "Clone", + "cloning": "Cloning...", + "cloneSuffix": " (Copy)", + "cloneSuccess": "Cloned \"{{name}}\" successfully.", "delete": "Delete", "cancel": "Cancel", "none": "None", @@ -1083,3 +1087,4 @@ "emptyHint": "No model mappings configured. Add mappings to transform request models before sending to upstream." } } + diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index 98ed6e5..a0e9243 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -918,6 +918,10 @@ "createError": "创建提供商失败。请检查您的连接并重试。", "updateError": "更新提供商失败。请检查您的连接并重试。", "saveChanges": "保存更改", + "clone": "克隆", + "cloning": "克隆中...", + "cloneSuffix": "(副本)", + "cloneSuccess": "已成功克隆「{{name}}」。", "delete": "删除", "cancel": "取消", "none": "无", @@ -1082,3 +1086,4 @@ "emptyHint": "暂无模型映射。添加映射以在发送到上游前转换请求模型。" } } + diff --git a/web/src/pages/providers/components/provider-edit-flow.tsx b/web/src/pages/providers/components/provider-edit-flow.tsx index 7a0cb1b..800eda3 100644 --- a/web/src/pages/providers/components/provider-edit-flow.tsx +++ b/web/src/pages/providers/components/provider-edit-flow.tsx @@ -5,6 +5,7 @@ import { Key, Check, Trash2, + Copy, Plus, ArrowRight, Zap, @@ -20,6 +21,7 @@ import { DialogFooter, } from '@/components/ui/dialog'; import { + useCreateProvider, useUpdateProvider, useDeleteProvider, useModelMappings, @@ -280,11 +282,16 @@ type EditFormData = { export function ProviderEditFlow({ provider, onClose }: ProviderEditFlowProps) { const { t } = useTranslation(); const [saving, setSaving] = useState(false); + const [cloning, setCloning] = useState(false); + const [cloneToastMessage, setCloneToastMessage] = useState(null); const [deleting, setDeleting] = useState(false); const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const createProvider = useCreateProvider(); const updateProvider = useUpdateProvider(); const deleteProvider = useDeleteProvider(); + const createModelMapping = useCreateModelMapping(); + const { data: allMappings } = useModelMappings(); const initClients = (): ClientConfig[] => { const supportedTypes = provider.supportedClientTypes || []; @@ -323,6 +330,13 @@ export function ProviderEditFlow({ provider, onClose }: ProviderEditFlowProps) { return hasEnabledClient && hasUrl; }; + const parseSensitiveWords = (value: string): string[] => { + return value + .split(/[\n,]/) + .map((item) => item.trim()) + .filter(Boolean); + }; + const handleSave = async () => { if (!isValid()) return; @@ -330,13 +344,6 @@ export function ProviderEditFlow({ provider, onClose }: ProviderEditFlowProps) { setSaveStatus('idle'); try { - const parseSensitiveWords = (value: string): string[] => { - return value - .split(/[\n,]/) - .map((item) => item.trim()) - .filter(Boolean); - }; - const supportedClientTypes = formData.clients.filter((c) => c.enabled).map((c) => c.id); const clientBaseURL: Partial> = {}; const clientMultiplier: Partial> = {}; @@ -387,6 +394,89 @@ export function ProviderEditFlow({ provider, onClose }: ProviderEditFlowProps) { } }; + const handleClone = async () => { + if (!isValid() || cloning || cloneToastMessage) return; + + setCloning(true); + + try { + const supportedClientTypes = formData.clients.filter((c) => c.enabled).map((c) => c.id); + const clientBaseURL: Partial> = {}; + const clientMultiplier: Partial> = {}; + formData.clients.forEach((c) => { + if (c.enabled && c.urlOverride) { + clientBaseURL[c.id] = c.urlOverride; + } + if (c.enabled && c.multiplier !== 10000) { + clientMultiplier[c.id] = c.multiplier; + } + }); + + const baseName = formData.name.trim() || provider.name; + const suffix = t('provider.cloneSuffix'); + const cloneName = baseName.endsWith(suffix) ? baseName : `${baseName}${suffix}`; + + const data: CreateProviderData = { + type: provider.type || 'custom', + name: cloneName, + logo: provider.logo, + config: { + disableErrorCooldown: !!formData.disableErrorCooldown, + custom: { + baseURL: formData.baseURL, + apiKey: formData.apiKey || provider.config?.custom?.apiKey || '', + clientBaseURL: Object.keys(clientBaseURL).length > 0 ? clientBaseURL : undefined, + clientMultiplier: + Object.keys(clientMultiplier).length > 0 ? clientMultiplier : undefined, + cloak: + formData.cloakMode !== 'auto' || + formData.cloakStrictMode || + parseSensitiveWords(formData.cloakSensitiveWords || '').length > 0 + ? { + mode: formData.cloakMode, + strictMode: formData.cloakStrictMode, + sensitiveWords: parseSensitiveWords(formData.cloakSensitiveWords || ''), + } + : undefined, + }, + }, + supportedClientTypes, + supportModels: formData.supportModels.length > 0 ? formData.supportModels : undefined, + }; + + const newProvider = await createProvider.mutateAsync(data); + + const providerMappings = (allMappings || []).filter( + (mapping) => mapping.scope === 'provider' && mapping.providerID === provider.id, + ); + + if (providerMappings.length > 0) { + for (const mapping of providerMappings) { + await createModelMapping.mutateAsync({ + scope: mapping.scope, + clientType: mapping.clientType, + providerType: mapping.providerType, + providerID: newProvider.id, + projectID: mapping.projectID, + routeID: mapping.routeID, + apiTokenID: mapping.apiTokenID, + pattern: mapping.pattern, + target: mapping.target, + priority: mapping.priority, + isEnabled: mapping.isEnabled, + }); + } + } + + setCloneToastMessage(t('provider.cloneSuccess', { name: cloneName })); + setTimeout(() => onClose(), 800); + } catch (error) { + console.error('Failed to clone provider:', error); + } finally { + setCloning(false); + } + }; + const handleDelete = async () => { setDeleting(true); try { @@ -472,6 +562,14 @@ export function ProviderEditFlow({ provider, onClose }: ProviderEditFlowProps) { {t('provider.delete')} + @@ -616,6 +714,12 @@ export function ProviderEditFlow({ provider, onClose }: ProviderEditFlowProps) { + {cloneToastMessage && ( +
+
{cloneToastMessage}
+
+ )} +