From fbf3518ebb29575d98f4086c2348bcc55fb6f155 Mon Sep 17 00:00:00 2001 From: nanako <469449812@qq.com> Date: Sat, 8 Nov 2025 21:05:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E2=80=9C=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E5=95=86=E7=AE=A1=E7=90=86=E2=80=9D=E6=97=A0=E6=B3=95=E7=BC=96?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/handlers.go | 96 +++++++++++++++ backend/gateway.db | Bin 98304 -> 98304 bytes backend/internal/db/database.go | 24 ++++ backend/internal/models/schema.go | 25 ++-- backend/internal/router/selector.go | 44 ++++++- backend/main.go | 5 + frontend/src/features/providers/api/index.js | 39 ++++++ .../providers/components/ProviderForm.jsx | 94 ++++++++++++++ .../providers/components/ProviderList.jsx | 115 ++++++++++++++---- .../components/VirtualModelForm.jsx | 11 ++ .../components/VirtualModelList.jsx | 16 +++ 11 files changed, 432 insertions(+), 37 deletions(-) create mode 100644 frontend/src/features/providers/components/ProviderForm.jsx diff --git a/backend/api/handlers.go b/backend/api/handlers.go index 084573d..e29a844 100644 --- a/backend/api/handlers.go +++ b/backend/api/handlers.go @@ -57,6 +57,27 @@ type ResponsesRequest struct { Stream bool `json:"stream,omitempty"` } +// BackendModelAssociation 代表一个后端模型关联及其配置 +type BackendModelAssociation struct { + BackendModelID uint `json:"backend_model_id" binding:"required"` + Priority int `json:"priority" binding:"required"` + CostThreshold float64 `json:"cost_threshold"` +} + +// CreateVirtualModelRequest 创建虚拟模型的请求结构 +type CreateVirtualModelRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + BackendModels []BackendModelAssociation `json:"backend_models"` +} + +// UpdateVirtualModelRequest 更新虚拟模型的请求结构 +type UpdateVirtualModelRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + BackendModels []BackendModelAssociation `json:"backend_models"` +} + // ListModels 处理 GET /models 请求 func (h *APIHandler) ListModels(c *gin.Context) { var virtualModels []models.VirtualModel @@ -465,6 +486,81 @@ func (h *APIHandler) GetProvidersHandler(c *gin.Context) { c.JSON(http.StatusOK, providers) } +// GetProviderHandler 处理 GET /api/providers/:id 请求 +func (h *APIHandler) GetProviderHandler(c *gin.Context) { + id := c.Param("id") + var providerID uint + if _, err := fmt.Sscanf(id, "%d", &providerID); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"}) + return + } + + provider, err := db.GetProviderByID(h.DB, providerID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Provider not found"}) + return + } + + c.JSON(http.StatusOK, provider) +} + +// CreateProviderHandler 处理 POST /api/providers 请求 +func (h *APIHandler) CreateProviderHandler(c *gin.Context) { + var provider models.Provider + if err := c.ShouldBindJSON(&provider); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := db.CreateProvider(h.DB, &provider); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create provider"}) + return + } + + c.JSON(http.StatusCreated, provider) +} + +// UpdateProviderHandler 处理 PUT /api/providers/:id 请求 +func (h *APIHandler) UpdateProviderHandler(c *gin.Context) { + id := c.Param("id") + var providerID uint + if _, err := fmt.Sscanf(id, "%d", &providerID); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"}) + return + } + + var provider models.Provider + if err := c.ShouldBindJSON(&provider); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + provider.ID = providerID + + if err := db.UpdateProvider(h.DB, &provider); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update provider"}) + return + } + + c.JSON(http.StatusOK, provider) +} + +// DeleteProviderHandler 处理 DELETE /api/providers/:id 请求 +func (h *APIHandler) DeleteProviderHandler(c *gin.Context) { + id := c.Param("id") + var providerID uint + if _, err := fmt.Sscanf(id, "%d", &providerID); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"}) + return + } + + if err := db.DeleteProvider(h.DB, providerID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider"}) + return + } + + c.JSON(http.StatusNoContent, nil) +} + // CreateVirtualModelHandler 处理 POST /api/virtual-models 请求 func (h *APIHandler) CreateVirtualModelHandler(c *gin.Context) { var vm models.VirtualModel diff --git a/backend/gateway.db b/backend/gateway.db index d09b8110acdde449132ad6a4ca8c50d847f051c7..b07dd9214478c119f9f88bb3512d11b17bfb9f87 100644 GIT binary patch delta 2452 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 V4#eOD&?V(-L2G2wRE@$__y 0 { + // 如果成本超过该模型的阈值,跳过该模型 + if estimatedCost > model.CostThreshold { + continue + } + } + + // 找到第一个满足条件的模型(未设置阈值或成本在阈值内) + selectedModel = model + break + } + + // 如果所有模型都超过了各自的阈值,返回最后一个模型作为兜底 + if selectedModel == nil { + selectedModel = &suitableModels[len(suitableModels)-1] + } + + // 返回选中的模型 + return selectedModel, nil +} diff --git a/backend/main.go b/backend/main.go index 19b38c4..1c7d073 100644 --- a/backend/main.go +++ b/backend/main.go @@ -52,7 +52,12 @@ func main() { api_ := router.Group("/api") api_.Use(middleware.AuthMiddleware(database)) { + // Providers api_.GET("/providers", handler.GetProvidersHandler) + api_.GET("/providers/:id", handler.GetProviderHandler) + api_.POST("/providers", handler.CreateProviderHandler) + api_.PUT("/providers/:id", handler.UpdateProviderHandler) + api_.DELETE("/providers/:id", handler.DeleteProviderHandler) // Virtual Models api_.POST("/virtual-models", handler.CreateVirtualModelHandler) diff --git a/frontend/src/features/providers/api/index.js b/frontend/src/features/providers/api/index.js index b5aa637..6d571c2 100644 --- a/frontend/src/features/providers/api/index.js +++ b/frontend/src/features/providers/api/index.js @@ -13,4 +13,43 @@ export const getProviders = async () => { '获取服务商列表失败'; throw new Error(errorMessage); } +}; + +export const getProvider = async (id) => { + try { + const response = await apiClient.get(`/api/providers/${id}`); + return response.data; + } catch (error) { + console.error('获取服务商详情失败:', error); + throw new Error(error.response?.data?.error || '获取服务商详情失败'); + } +}; + +export const createProvider = async (providerData) => { + try { + const response = await apiClient.post('/api/providers', providerData); + return response.data; + } catch (error) { + console.error('创建服务商失败:', error); + throw new Error(error.response?.data?.error || '创建服务商失败'); + } +}; + +export const updateProvider = async (id, providerData) => { + try { + const response = await apiClient.put(`/api/providers/${id}`, providerData); + return response.data; + } catch (error) { + console.error('更新服务商失败:', error); + throw new Error(error.response?.data?.error || '更新服务商失败'); + } +}; + +export const deleteProvider = async (id) => { + try { + await apiClient.delete(`/api/providers/${id}`); + } catch (error) { + console.error('删除服务商失败:', error); + throw new Error(error.response?.data?.error || '删除服务商失败'); + } }; \ No newline at end of file diff --git a/frontend/src/features/providers/components/ProviderForm.jsx b/frontend/src/features/providers/components/ProviderForm.jsx new file mode 100644 index 0000000..7d17cb7 --- /dev/null +++ b/frontend/src/features/providers/components/ProviderForm.jsx @@ -0,0 +1,94 @@ +import React, { useState, useEffect } from 'react'; + +const ProviderForm = ({ provider, onSave, onCancel }) => { + const [name, setName] = useState(''); + const [baseURL, setBaseURL] = useState(''); + const [apiKey, setAPIKey] = useState(''); + const [apiVersion, setAPIVersion] = useState(''); + + useEffect(() => { + if (provider) { + setName(provider.Name || ''); + setBaseURL(provider.BaseURL || ''); + setAPIKey(provider.APIKey || ''); + setAPIVersion(provider.APIVersion || ''); + } + }, [provider]); + + const handleSubmit = (e) => { + e.preventDefault(); + onSave({ + Name: name, + BaseURL: baseURL, + APIKey: apiKey, + APIVersion: apiVersion, + }); + }; + + return ( +
+
+ + setName(e.target.value)} + required + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border" + /> +
+ +
+ + setBaseURL(e.target.value)} + required + placeholder="https://api.example.com" + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border" + /> +
+ +
+ + setAPIKey(e.target.value)} + required + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border" + /> +
+ +
+ + setAPIVersion(e.target.value)} + placeholder="v1" + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border" + /> +
+ +
+ + +
+
+ ); +}; + +export default ProviderForm; \ No newline at end of file diff --git a/frontend/src/features/providers/components/ProviderList.jsx b/frontend/src/features/providers/components/ProviderList.jsx index 761639b..e5e0acc 100644 --- a/frontend/src/features/providers/components/ProviderList.jsx +++ b/frontend/src/features/providers/components/ProviderList.jsx @@ -1,29 +1,64 @@ import React, { useState, useEffect } from 'react'; -import { getProviders } from '../api'; +import { getProviders, createProvider, updateProvider, deleteProvider } from '../api'; +import ProviderForm from './ProviderForm'; const ProviderList = () => { const [providers, setProviders] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [editingProvider, setEditingProvider] = useState(null); + const [showCreateForm, setShowCreateForm] = useState(false); + + const fetchProviders = async () => { + try { + setLoading(true); + const data = await getProviders(); + setProviders(data); + setError(null); + } catch (err) { + setError(err.message || '获取提供商列表失败'); + console.error('Error fetching providers:', err); + } finally { + setLoading(false); + } + }; useEffect(() => { - const fetchProviders = async () => { - try { - setLoading(true); - const data = await getProviders(); - setProviders(data); - setError(null); - } catch (err) { - setError(err.message || '获取提供商列表失败'); - console.error('Error fetching providers:', err); - } finally { - setLoading(false); - } - }; - fetchProviders(); }, []); + const handleCreate = async (providerData) => { + try { + await createProvider(providerData); + setShowCreateForm(false); + await fetchProviders(); + } catch (err) { + setError(err.message || '创建提供商失败'); + } + }; + + const handleUpdate = async (providerData) => { + try { + await updateProvider(editingProvider.ID, providerData); + setEditingProvider(null); + await fetchProviders(); + } catch (err) { + setError(err.message || '更新提供商失败'); + } + }; + + const handleDelete = async (id) => { + if (!window.confirm('确定要删除这个提供商吗?')) { + return; + } + try { + await deleteProvider(id); + await fetchProviders(); + } catch (err) { + setError(err.message || '删除提供商失败'); + } + }; + if (loading) { return (
@@ -40,8 +75,43 @@ const ProviderList = () => { ); } + if (showCreateForm) { + return ( +
+

创建新提供商

+ setShowCreateForm(false)} + /> +
+ ); + } + + if (editingProvider) { + return ( +
+

编辑提供商

+ setEditingProvider(null)} + /> +
+ ); + } + return ( -
+
+
+ +
+ +
@@ -49,7 +119,7 @@ const ProviderList = () => { Name ) : ( providers.map((provider, index) => ( - + @@ -95,6 +165,7 @@ const ProviderList = () => { )}
- Type + Base URL Actions @@ -65,7 +135,7 @@ const ProviderList = () => {
{provider.Name || 'N/A'} @@ -79,15 +149,15 @@ const ProviderList = () => {
+
); }; diff --git a/frontend/src/features/virtual-models/components/VirtualModelForm.jsx b/frontend/src/features/virtual-models/components/VirtualModelForm.jsx index 3018aba..7231ff1 100644 --- a/frontend/src/features/virtual-models/components/VirtualModelForm.jsx +++ b/frontend/src/features/virtual-models/components/VirtualModelForm.jsx @@ -36,6 +36,7 @@ const VirtualModelForm = ({ model, onSave, onCancel }) => { PromptTokenPrice: 0, CompletionTokenPrice: 0, FixedPrice: 0, + CostThreshold: 0, }, ]); }; @@ -103,6 +104,16 @@ const VirtualModelForm = ({ model, onSave, onCancel }) => { className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
+
+ + handleBackendModelChange(index, 'CostThreshold', parseFloat(e.target.value) || 0)} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" + /> +
{ 名称 + + 后端模型 + 操作 @@ -56,6 +59,19 @@ const VirtualModelList = ({ onEdit }) => { {vm.Name} + + {vm.BackendModels && vm.BackendModels.length > 0 ? ( +
+ {vm.BackendModels.map((bm, index) => ( +
+ {bm.Name} (阈值: {bm.CostThreshold || 0}) +
+ ))} +
+ ) : ( + + )} +