修复“提供商管理”无法编辑

This commit is contained in:
2025-11-08 21:05:06 +08:00
parent 322a15b037
commit fbf3518ebb
11 changed files with 432 additions and 37 deletions

View File

@@ -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

Binary file not shown.

View File

@@ -41,6 +41,30 @@ func GetProviders(db *gorm.DB) ([]models.Provider, error) {
return providers, nil
}
// GetProviderByID 通过ID获取服务商
func GetProviderByID(db *gorm.DB, id uint) (*models.Provider, error) {
var provider models.Provider
if err := db.First(&provider, id).Error; err != nil {
return nil, err
}
return &provider, nil
}
// CreateProvider 创建一个新的服务商
func CreateProvider(db *gorm.DB, provider *models.Provider) error {
return db.Create(provider).Error
}
// UpdateProvider 更新一个服务商
func UpdateProvider(db *gorm.DB, provider *models.Provider) error {
return db.Save(provider).Error
}
// DeleteProvider 删除一个服务商
func DeleteProvider(db *gorm.DB, id uint) error {
return db.Delete(&models.Provider{}, id).Error
}
// CreateVirtualModel 创建一个新的虚拟模型
func CreateVirtualModel(db *gorm.DB, virtualModel *models.VirtualModel) error {
return db.Create(virtualModel).Error

View File

@@ -29,23 +29,24 @@ type APIKey struct {
// VirtualModel 用户与之交互的虚拟模型
type VirtualModel struct {
gorm.Model
Name string `gorm:"uniqueIndex;not null"` // 虚拟模型名称,唯一索引
Name string `gorm:"uniqueIndex;not null"` // 虚拟模型名称,唯一索引
BackendModels []BackendModel `gorm:"foreignKey:VirtualModelID"` // 关联的后端模型列表
}
// BackendModel 实际的后端AI模型
type BackendModel struct {
gorm.Model
VirtualModelID uint `gorm:"index;not null"` // 关联的虚拟模型ID
ProviderID uint `gorm:"index;not null"` // 关联的服务商ID
Provider Provider `gorm:"foreignKey:ProviderID"` // GORM关联
Name string `gorm:"not null"` // 后端模型名称
Priority int `gorm:"not null"` // 优先级(数字越小优先级越高)
MaxContextLength int `gorm:"not null"` // 最大上下文长度
BillingMethod string `gorm:"not null"` // 计费方式
PromptTokenPrice float64 `gorm:"type:decimal(10,6)"` // 输入token单价
CompletionTokenPrice float64 `gorm:"type:decimal(10,6)"` // 输出token单价
FixedPrice float64 `gorm:"type:decimal(10,2)"` // 固定价格(按次计费)
VirtualModelID uint `gorm:"index;not null"` // 关联的虚拟模型ID
ProviderID uint `gorm:"index;not null"` // 关联的服务商ID
Provider Provider `gorm:"foreignKey:ProviderID"` // GORM关联
Name string `gorm:"not null"` // 后端模型名称
Priority int `gorm:"not null"` // 优先级(数字越小优先级越高)
MaxContextLength int `gorm:"not null"` // 最大上下文长度
BillingMethod string `gorm:"not null"` // 计费方式
PromptTokenPrice float64 `gorm:"type:decimal(10,6)"` // 输入token单价
CompletionTokenPrice float64 `gorm:"type:decimal(10,6)"` // 输出token单价
FixedPrice float64 `gorm:"type:decimal(10,2)"` // 固定价格(按次计费)
CostThreshold float64 `gorm:"type:decimal(10,6)"` // 成本阈值
}
// RequestLog 记录每次API请求的详细信息
@@ -61,4 +62,4 @@ type RequestLog struct {
Cost float64 `gorm:"type:decimal(10,6)"` // 成本
RequestBody string `gorm:"type:text"` // 请求体
ResponseBody string `gorm:"type:text"` // 响应体
}
}

View File

@@ -47,6 +47,44 @@ func SelectBackendModel(db *gorm.DB, virtualModelName string, requestTokenCount
return suitableModels[i].Priority < suitableModels[j].Priority
})
// 返回优先级最高的模型
return &suitableModels[0], nil
}
// 选择合适的模型(考虑每个后端模型的成本阈值)
// 估算响应token数假设等于请求token数
estimatedResponseTokens := requestTokenCount
var selectedModel *models.BackendModel
// 按优先级遍历模型,选择第一个满足成本阈值的模型
for i := range suitableModels {
model := &suitableModels[i]
// 计算估算成本
var estimatedCost float64
switch model.BillingMethod {
case models.BillingMethodToken:
estimatedCost = float64(requestTokenCount)*model.PromptTokenPrice +
float64(estimatedResponseTokens)*model.CompletionTokenPrice
case models.BillingMethodRequest:
estimatedCost = model.FixedPrice
}
// 如果该模型设置了成本阈值,检查成本是否超过阈值
if model.CostThreshold > 0 {
// 如果成本超过该模型的阈值,跳过该模型
if estimatedCost > model.CostThreshold {
continue
}
}
// 找到第一个满足条件的模型(未设置阈值或成本在阈值内)
selectedModel = model
break
}
// 如果所有模型都超过了各自的阈值,返回最后一个模型作为兜底
if selectedModel == nil {
selectedModel = &suitableModels[len(suitableModels)-1]
}
// 返回选中的模型
return selectedModel, nil
}

View File

@@ -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)

View File

@@ -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 || '删除服务商失败');
}
};

View File

@@ -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 (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">服务商名称</label>
<input
type="text"
value={name}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Base URL</label>
<input
type="text"
value={baseURL}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">API Key</label>
<input
type="password"
value={apiKey}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">API Version可选</label>
<input
type="text"
value={apiVersion}
onChange={(e) => 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"
/>
</div>
<div className="flex justify-end space-x-2 pt-4">
<button
type="button"
onClick={onCancel}
className="rounded bg-gray-200 px-4 py-2 hover:bg-gray-300 transition-colors"
>
取消
</button>
<button
type="submit"
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 transition-colors"
>
保存
</button>
</div>
</form>
);
};
export default ProviderForm;

View File

@@ -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 (
<div className="flex justify-center items-center py-8">
@@ -40,8 +75,43 @@ const ProviderList = () => {
);
}
if (showCreateForm) {
return (
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-medium mb-4">创建新提供商</h3>
<ProviderForm
onSave={handleCreate}
onCancel={() => setShowCreateForm(false)}
/>
</div>
);
}
if (editingProvider) {
return (
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-medium mb-4">编辑提供商</h3>
<ProviderForm
provider={editingProvider}
onSave={handleUpdate}
onCancel={() => setEditingProvider(null)}
/>
</div>
);
}
return (
<div className="overflow-x-auto">
<div className="space-y-4">
<div className="flex justify-end">
<button
onClick={() => setShowCreateForm(true)}
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 transition-colors"
>
创建提供商
</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full bg-white border border-gray-200 rounded-lg shadow-sm">
<thead className="bg-gray-50">
<tr>
@@ -49,7 +119,7 @@ const ProviderList = () => {
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
Base URL
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
@@ -65,7 +135,7 @@ const ProviderList = () => {
</tr>
) : (
providers.map((provider, index) => (
<tr key={provider.Model?.ID || index} className="hover:bg-gray-50 transition-colors">
<tr key={provider.ID || index} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{provider.Name || 'N/A'}
@@ -79,15 +149,15 @@ const ProviderList = () => {
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
className="text-indigo-600 hover:text-indigo-900 mr-4 transition-colors"
onClick={() => console.log('Edit', provider.Model?.ID)}
onClick={() => setEditingProvider(provider)}
>
Edit
编辑
</button>
<button
className="text-red-600 hover:text-red-900 transition-colors"
onClick={() => console.log('Delete', provider.Model?.ID)}
onClick={() => handleDelete(provider.ID)}
>
Delete
删除
</button>
</td>
</tr>
@@ -95,6 +165,7 @@ const ProviderList = () => {
)}
</tbody>
</table>
</div>
</div>
);
};

View File

@@ -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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">成本阈值</label>
<input
type="number"
step="0.01"
value={bm.CostThreshold || 0}
onChange={(e) => handleBackendModelChange(index, 'CostThreshold', parseFloat(e.target.value) || 0)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">最大上下文长度</label>
<input

View File

@@ -45,6 +45,9 @@ const VirtualModelList = ({ onEdit }) => {
<th className="px-6 py-3 border-b-2 border-gray-200 bg-gray-50 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
名称
</th>
<th className="px-6 py-3 border-b-2 border-gray-200 bg-gray-50 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
后端模型
</th>
<th className="px-6 py-3 border-b-2 border-gray-200 bg-gray-50 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
操作
</th>
@@ -56,6 +59,19 @@ const VirtualModelList = ({ onEdit }) => {
<td className="px-6 py-4 whitespace-nowrap border-b border-gray-200">
{vm.Name}
</td>
<td className="px-6 py-4 border-b border-gray-200">
{vm.BackendModels && vm.BackendModels.length > 0 ? (
<div className="space-y-1">
{vm.BackendModels.map((bm, index) => (
<div key={index} className="text-sm">
{bm.Name} (阈值: {bm.CostThreshold || 0})
</div>
))}
</div>
) : (
<span className="text-gray-400"></span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap border-b border-gray-200">
<button
className="text-indigo-600 hover:text-indigo-900 mr-4"