From 18d3706ff8e407a786c57f78e5bdc8c3d707abca Mon Sep 17 00:00:00 2001 From: "1808837298@qq.com" <1808837298@qq.com> Date: Fri, 28 Feb 2025 21:13:30 +0800 Subject: [PATCH] feat: Add new model management features - Implement `/api/channel/models_enabled` endpoint to retrieve enabled models - Add `EnabledListModels` handler in controller - Create new `ModelRatioNotSetEditor` component for managing unset model ratios - Update router to include new models_enabled route - Add internationalization support for new model management UI - Include GPT-4.5 preview model in OpenAI model list --- controller/model.go | 7 + relay/channel/openai/constant.go | 1 + router/api-router.go | 1 + web/src/components/OperationSetting.js | 4 + web/src/i18n/locales/en.json | 39 +- .../Operation/ModelRationNotSetEditor.js | 550 ++++++++++++++++++ 6 files changed, 601 insertions(+), 1 deletion(-) create mode 100644 web/src/pages/Setting/Operation/ModelRationNotSetEditor.js diff --git a/controller/model.go b/controller/model.go index 8ec2c7c9..df7e59a6 100644 --- a/controller/model.go +++ b/controller/model.go @@ -216,6 +216,13 @@ func DashboardListModels(c *gin.Context) { }) } +func EnabledListModels(c *gin.Context) { + c.JSON(200, gin.H{ + "success": true, + "data": model.GetEnabledModels(), + }) +} + func RetrieveModel(c *gin.Context) { modelId := c.Param("model") if aiModel, ok := openAIModelsMap[modelId]; ok { diff --git a/relay/channel/openai/constant.go b/relay/channel/openai/constant.go index d55242ed..c703e414 100644 --- a/relay/channel/openai/constant.go +++ b/relay/channel/openai/constant.go @@ -11,6 +11,7 @@ var ModelList = []string{ "chatgpt-4o-latest", "gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06", "gpt-4o-2024-11-20", "gpt-4o-mini", "gpt-4o-mini-2024-07-18", + "gpt-4.5-preview", "gpt-4.5-preview-2025-02-27", "o1-preview", "o1-preview-2024-09-12", "o1-mini", "o1-mini-2024-09-12", "o3-mini", "o3-mini-2025-01-31", diff --git a/router/api-router.go b/router/api-router.go index bf88449a..bc3f5d9f 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -84,6 +84,7 @@ func SetApiRouter(router *gin.Engine) { channelRoute.GET("/", controller.GetAllChannels) channelRoute.GET("/search", controller.SearchChannels) channelRoute.GET("/models", controller.ChannelListModels) + channelRoute.GET("/models_enabled", controller.EnabledListModels) channelRoute.GET("/:id", controller.GetChannel) channelRoute.GET("/test", controller.TestAllChannels) channelRoute.GET("/test/:id", controller.TestChannel) diff --git a/web/src/components/OperationSetting.js b/web/src/components/OperationSetting.js index caa9cc2e..19f2dbe6 100644 --- a/web/src/components/OperationSetting.js +++ b/web/src/components/OperationSetting.js @@ -16,6 +16,7 @@ import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js import { API, showError, showSuccess } from '../helpers'; import SettingsChats from '../pages/Setting/Operation/SettingsChats.js'; import { useTranslation } from 'react-i18next'; +import ModelRatioNotSetEditor from '../pages/Setting/Operation/ModelRationNotSetEditor.js'; const OperationSetting = () => { const { t } = useTranslation(); @@ -158,6 +159,9 @@ const OperationSetting = () => { + + + diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 87b9e5cc..aa2fb2d5 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1280,5 +1280,42 @@ "频率限制的周期(分钟)": "Rate limit period (minutes)", "只包括请求成功的次数": "Only include successful request times", "保存模型速率限制": "Save model rate limit settings", - "速率限制设置": "Rate limit settings" + "速率限制设置": "Rate limit settings", + "获取启用模型失败:": "Failed to get enabled models:", + "获取启用模型失败": "Failed to get enabled models", + "JSON解析错误:": "JSON parsing error:", + "保存失败:": "Save failed:", + "输入模型倍率": "Enter model ratio", + "输入补全倍率": "Enter completion ratio", + "请输入数字": "Please enter a number", + "模型名称已存在": "Model name already exists", + "添加成功": "Added successfully", + "请先选择需要批量设置的模型": "Please select models for batch setting first", + "请输入模型倍率和补全倍率": "Please enter model ratio and completion ratio", + "请输入有效的数字": "Please enter a valid number", + "请输入填充值": "Please enter a value", + "批量设置成功": "Batch setting successful", + "已为 {{count}} 个模型设置{{type}}": "Set {{type}} for {{count}} models", + "固定价格": "Fixed Price", + "模型倍率和补全倍率": "Model Ratio and Completion Ratio", + "批量设置": "Batch Setting", + "搜索模型名称": "Search model name", + "此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除": "This page only shows models without price or ratio settings. After setting, they will be automatically removed from the list", + "没有未设置的模型": "No unconfigured models", + "定价模式": "Pricing Mode", + "固定价格(每次)": "Fixed Price (per use)", + "输入每次价格": "Enter per-use price", + "批量设置模型参数": "Batch Set Model Parameters", + "设置类型": "Setting Type", + "模型倍率值": "Model Ratio Value", + "补全倍率值": "Completion Ratio Value", + "请输入模型倍率": "Enter model ratio", + "请输入补全倍率": "Enter completion ratio", + "请输入数值": "Enter a value", + "将为选中的 ": "Will set for selected ", + " 个模型设置相同的值": " models with the same value", + "当前设置类型: ": "Current setting type: ", + "固定价格值": "Fixed Price Value", + "未设置倍率模型": "Models without ratio settings", + "模型倍率和补全倍率同时设置": "Both model ratio and completion ratio are set" } diff --git a/web/src/pages/Setting/Operation/ModelRationNotSetEditor.js b/web/src/pages/Setting/Operation/ModelRationNotSetEditor.js new file mode 100644 index 00000000..49172281 --- /dev/null +++ b/web/src/pages/Setting/Operation/ModelRationNotSetEditor.js @@ -0,0 +1,550 @@ +import React, { useEffect, useState } from 'react'; +import { Table, Button, Input, Modal, Form, Space, Typography, Radio, Notification } from '@douyinfe/semi-ui'; +import { IconDelete, IconPlus, IconSearch, IconSave, IconBolt } from '@douyinfe/semi-icons'; +import { showError, showSuccess } from '../../../helpers'; +import { API } from '../../../helpers'; +import { useTranslation } from 'react-i18next'; + +export default function ModelRatioNotSetEditor(props) { + const { t } = useTranslation(); + const [models, setModels] = useState([]); + const [visible, setVisible] = useState(false); + const [batchVisible, setBatchVisible] = useState(false); + const [currentModel, setCurrentModel] = useState(null); + const [searchText, setSearchText] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [loading, setLoading] = useState(false); + const [enabledModels, setEnabledModels] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [batchFillType, setBatchFillType] = useState('ratio'); + const [batchFillValue, setBatchFillValue] = useState(''); + const [batchRatioValue, setBatchRatioValue] = useState(''); + const [batchCompletionRatioValue, setBatchCompletionRatioValue] = useState(''); + const { Text } = Typography; + // 定义可选的每页显示条数 + const pageSizeOptions = [10, 20, 50, 100]; + + const getAllEnabledModels = async () => { + try { + const res = await API.get('/api/channel/models_enabled'); + const { success, message, data } = res.data; + if (success) { + setEnabledModels(data); + } else { + showError(message); + } + } catch (error) { + console.error(t('获取启用模型失败:'), error); + showError(t('获取启用模型失败')); + } + } + + useEffect(() => { + // 获取所有启用的模型 + getAllEnabledModels(); + }, []); + + useEffect(() => { + try { + const modelPrice = JSON.parse(props.options.ModelPrice || '{}'); + const modelRatio = JSON.parse(props.options.ModelRatio || '{}'); + const completionRatio = JSON.parse(props.options.CompletionRatio || '{}'); + + // 找出所有未设置价格和倍率的模型 + const unsetModels = enabledModels.filter(modelName => { + const hasPrice = modelPrice[modelName] !== undefined; + const hasRatio = modelRatio[modelName] !== undefined; + const hasCompletionRatio = completionRatio[modelName] !== undefined; + + // 如果模型既没有价格也没有倍率设置,则显示 + return !(hasPrice || (hasRatio && hasCompletionRatio)); + }); + + // 创建模型数据 + const modelData = unsetModels.map(name => ({ + name, + price: '', + ratio: '', + completionRatio: '' + })); + + setModels(modelData); + // 清空选择 + setSelectedRowKeys([]); + } catch (error) { + console.error(t('JSON解析错误:'), error); + } + }, [props.options, enabledModels]); + + // 首先声明分页相关的工具函数 + const getPagedData = (data, currentPage, pageSize) => { + const start = (currentPage - 1) * pageSize; + const end = start + pageSize; + return data.slice(start, end); + }; + + // 处理页面大小变化 + const handlePageSizeChange = (size) => { + setPageSize(size); + // 重新计算当前页,避免数据丢失 + const totalPages = Math.ceil(filteredModels.length / size); + if (currentPage > totalPages) { + setCurrentPage(totalPages || 1); + } + }; + + // 在 return 语句之前,先处理过滤和分页逻辑 + const filteredModels = models.filter(model => + searchText ? model.name.toLowerCase().includes(searchText.toLowerCase()) : true + ); + + // 然后基于过滤后的数据计算分页数据 + const pagedData = getPagedData(filteredModels, currentPage, pageSize); + + const SubmitData = async () => { + setLoading(true); + const output = { + ModelPrice: JSON.parse(props.options.ModelPrice || '{}'), + ModelRatio: JSON.parse(props.options.ModelRatio || '{}'), + CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}') + }; + + try { + // 数据转换 - 只处理已修改的模型 + models.forEach(model => { + // 只有当用户设置了值时才更新 + if (model.price !== '') { + // 如果价格不为空,则转换为浮点数,忽略倍率参数 + output.ModelPrice[model.name] = parseFloat(model.price); + } else { + if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio); + if (model.completionRatio !== '') output.CompletionRatio[model.name] = parseFloat(model.completionRatio); + } + }); + + // 准备API请求数组 + const finalOutput = { + ModelPrice: JSON.stringify(output.ModelPrice, null, 2), + ModelRatio: JSON.stringify(output.ModelRatio, null, 2), + CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2) + }; + + const requestQueue = Object.entries(finalOutput).map(([key, value]) => { + return API.put('/api/option/', { + key, + value + }); + }); + + // 批量处理请求 + const results = await Promise.all(requestQueue); + + // 验证结果 + if (requestQueue.length === 1) { + if (results.includes(undefined)) return; + } else if (requestQueue.length > 1) { + if (results.includes(undefined)) { + return showError(t('部分保存失败,请重试')); + } + } + + // 检查每个请求的结果 + for (const res of results) { + if (!res.data.success) { + return showError(res.data.message); + } + } + + showSuccess(t('保存成功')); + props.refresh(); + // 重新获取未设置的模型 + getAllEnabledModels(); + + } catch (error) { + console.error(t('保存失败:'), error); + showError(t('保存失败,请重试')); + } finally { + setLoading(false); + } + }; + + const columns = [ + { + title: t('模型名称'), + dataIndex: 'name', + key: 'name', + }, + { + title: t('模型固定价格'), + dataIndex: 'price', + key: 'price', + render: (text, record) => ( + updateModel(record.name, 'price', value)} + /> + ) + }, + { + title: t('模型倍率'), + dataIndex: 'ratio', + key: 'ratio', + render: (text, record) => ( + updateModel(record.name, 'ratio', value)} + /> + ) + }, + { + title: t('补全倍率'), + dataIndex: 'completionRatio', + key: 'completionRatio', + render: (text, record) => ( + updateModel(record.name, 'completionRatio', value)} + /> + ) + } + ]; + + const updateModel = (name, field, value) => { + if (value !== '' && isNaN(value)) { + showError(t('请输入数字')); + return; + } + setModels(prev => + prev.map(model => + model.name === name + ? { ...model, [field]: value } + : model + ) + ); + }; + + const addModel = (values) => { + // 检查模型名称是否存在, 如果存在则拒绝添加 + if (models.some(model => model.name === values.name)) { + showError(t('模型名称已存在')); + return; + } + setModels(prev => [{ + name: values.name, + price: values.price || '', + ratio: values.ratio || '', + completionRatio: values.completionRatio || '' + }, ...prev]); + setVisible(false); + showSuccess(t('添加成功')); + }; + + // 批量填充功能 + const handleBatchFill = () => { + if (selectedRowKeys.length === 0) { + showError(t('请先选择需要批量设置的模型')); + return; + } + + if (batchFillType === 'bothRatio') { + if (batchRatioValue === '' || batchCompletionRatioValue === '') { + showError(t('请输入模型倍率和补全倍率')); + return; + } + if (isNaN(batchRatioValue) || isNaN(batchCompletionRatioValue)) { + showError(t('请输入有效的数字')); + return; + } + } else { + if (batchFillValue === '') { + showError(t('请输入填充值')); + return; + } + if (isNaN(batchFillValue)) { + showError(t('请输入有效的数字')); + return; + } + } + + // 根据选择的类型批量更新模型 + setModels(prev => + prev.map(model => { + if (selectedRowKeys.includes(model.name)) { + if (batchFillType === 'price') { + return { + ...model, + price: batchFillValue, + ratio: '', + completionRatio: '' + }; + } else if (batchFillType === 'ratio') { + return { + ...model, + price: '', + ratio: batchFillValue + }; + } else if (batchFillType === 'completionRatio') { + return { + ...model, + price: '', + completionRatio: batchFillValue + }; + } else if (batchFillType === 'bothRatio') { + return { + ...model, + price: '', + ratio: batchRatioValue, + completionRatio: batchCompletionRatioValue + }; + } + } + return model; + }) + ); + + setBatchVisible(false); + Notification.success({ + title: t('批量设置成功'), + content: t('已为 {{count}} 个模型设置{{type}}', { + count: selectedRowKeys.length, + type: batchFillType === 'price' ? t('固定价格') : + batchFillType === 'ratio' ? t('模型倍率') : + batchFillType === 'completionRatio' ? t('补全倍率') : t('模型倍率和补全倍率') + }), + duration: 3, + }); + }; + + const handleBatchTypeChange = (value) => { + console.log(t('Changing batch type to:'), value); + setBatchFillType(value); + + // 切换类型时清空对应的值 + if (value !== 'bothRatio') { + setBatchFillValue(''); + } else { + setBatchRatioValue(''); + setBatchCompletionRatioValue(''); + } + }; + + const rowSelection = { + selectedRowKeys, + onChange: (selectedKeys) => { + setSelectedRowKeys(selectedKeys); + }, + }; + + return ( + <> + + + + + + } + placeholder={t('搜索模型名称')} + value={searchText} + onChange={value => { + setSearchText(value) + setCurrentPage(1); + }} + style={{ width: 200 }} + /> + + + {t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')} + + setCurrentPage(page), + onPageSizeChange: handlePageSizeChange, + pageSizeOptions: pageSizeOptions, + formatPageText: (page) => + t('第 {{start}} - {{end}} 条,共 {{total}} 条', { + start: page.currentStart, + end: page.currentEnd, + total: filteredModels.length + }), + showTotal: true, + showSizeChanger: true + }} + empty={ +
+ {t('没有未设置的模型')} +
+ } + /> + + + {/* 添加模型弹窗 */} + setVisible(false)} + onOk={() => { + currentModel && addModel(currentModel); + }} + > +
+ setCurrentModel(prev => ({ ...prev, name: value }))} + /> + {t('定价模式')}:{currentModel?.priceMode ? t("固定价格") : t("倍率模式")}} + onChange={checked => { + setCurrentModel(prev => ({ + ...prev, + price: '', + ratio: '', + completionRatio: '', + priceMode: checked + })); + }} + /> + {currentModel?.priceMode ? ( + setCurrentModel(prev => ({ ...prev, price: value }))} + /> + ) : ( + <> + setCurrentModel(prev => ({ ...prev, ratio: value }))} + /> + setCurrentModel(prev => ({ ...prev, completionRatio: value }))} + /> + + )} + +
+ + {/* 批量设置弹窗 */} + setBatchVisible(false)} + onOk={handleBatchFill} + width={500} + > +
+ +
+ + handleBatchTypeChange('price')} + > + {t('固定价格')} + + handleBatchTypeChange('ratio')} + > + {t('模型倍率')} + + handleBatchTypeChange('completionRatio')} + > + {t('补全倍率')} + + handleBatchTypeChange('bothRatio')} + > + {t('模型倍率和补全倍率同时设置')} + + +
+
+ + {batchFillType === 'bothRatio' ? ( + <> + setBatchRatioValue(value)} + /> + setBatchCompletionRatioValue(value)} + /> + + ) : ( + setBatchFillValue(value)} + /> + )} + + + {t('将为选中的 ')} {selectedRowKeys.length} {t(' 个模型设置相同的值')} + +
+ + {t('当前设置类型: ')} { + batchFillType === 'price' ? t('固定价格') : + batchFillType === 'ratio' ? t('模型倍率') : + batchFillType === 'completionRatio' ? t('补全倍率') : t('模型倍率和补全倍率') + } + +
+ +
+ + ); +}