Pārlūkot izejas kodu

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
[email protected] 10 mēneši atpakaļ
vecāks
revīzija
18d3706ff8

+ 7 - 0
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 {

+ 1 - 0
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",

+ 1 - 0
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)

+ 4 - 0
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 = () => {
             <Tabs.TabPane tab={t('可视化倍率设置')} itemKey="visual">
               <ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
             </Tabs.TabPane>
+            <Tabs.TabPane tab={t('未设置倍率模型')} itemKey="unset_models">
+              <ModelRatioNotSetEditor options={inputs} refresh={onRefresh} />
+            </Tabs.TabPane>
           </Tabs>
         </Card>
       </Spin>

+ 38 - 1
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"
 }

+ 550 - 0
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) => (
+        <Input
+          value={text}
+          placeholder={t('按量计费')}
+          onChange={value => updateModel(record.name, 'price', value)}
+        />
+      )
+    },
+    {
+      title: t('模型倍率'),
+      dataIndex: 'ratio',
+      key: 'ratio',
+      render: (text, record) => (
+        <Input
+          value={text}
+          placeholder={record.price !== '' ? t('模型倍率') : t('输入模型倍率')}
+          disabled={record.price !== ''}
+          onChange={value => updateModel(record.name, 'ratio', value)}
+        />
+      )
+    },
+    {
+      title: t('补全倍率'),
+      dataIndex: 'completionRatio',
+      key: 'completionRatio',
+      render: (text, record) => (
+        <Input
+          value={text}
+          placeholder={record.price !== '' ? t('补全倍率') : t('输入补全倍率')}
+          disabled={record.price !== ''}
+          onChange={value => 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 (
+    <>
+      <Space vertical align="start" style={{ width: '100%' }}>
+        <Space>
+          <Button icon={<IconPlus />} onClick={() => setVisible(true)}>
+            {t('添加模型')}
+          </Button>
+          <Button 
+            icon={<IconBolt />} 
+            type="secondary"
+            onClick={() => setBatchVisible(true)}
+            disabled={selectedRowKeys.length === 0}
+          >
+            {t('批量设置')} ({selectedRowKeys.length})
+          </Button>
+          <Button type="primary" icon={<IconSave />} onClick={SubmitData} loading={loading}>
+            {t('应用更改')}
+          </Button>
+          <Input
+            prefix={<IconSearch />}
+            placeholder={t('搜索模型名称')}
+            value={searchText}
+            onChange={value => {
+              setSearchText(value)
+              setCurrentPage(1);
+            }}
+            style={{ width: 200 }}
+          />
+        </Space>
+
+        <Text>{t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')}</Text>
+        
+        <Table
+          columns={columns}
+          dataSource={pagedData}
+          rowSelection={rowSelection}
+          rowKey="name"
+          pagination={{
+            currentPage: currentPage,
+            pageSize: pageSize,
+            total: filteredModels.length,
+            onPageChange: page => 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={
+            <div style={{ textAlign: 'center', padding: '20px' }}>
+              {t('没有未设置的模型')}
+            </div>
+          }
+        />
+      </Space>
+
+      {/* 添加模型弹窗 */}
+      <Modal
+        title={t('添加模型')}
+        visible={visible}
+        onCancel={() => setVisible(false)}
+        onOk={() => {
+          currentModel && addModel(currentModel);
+        }}
+      >
+        <Form>
+          <Form.Input
+            field="name"
+            label={t('模型名称')}
+            placeholder="strawberry"
+            required
+            onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))}
+          />
+          <Form.Switch
+            field="priceMode"
+            label={<>{t('定价模式')}:{currentModel?.priceMode ? t("固定价格") : t("倍率模式")}</>}
+            onChange={checked => {
+              setCurrentModel(prev => ({
+                ...prev,
+                price: '',
+                ratio: '',
+                completionRatio: '',
+                priceMode: checked
+              }));
+            }}
+          />
+          {currentModel?.priceMode ? (
+            <Form.Input
+              field="price"
+              label={t('固定价格(每次)')}
+              placeholder={t('输入每次价格')}
+              onChange={value => setCurrentModel(prev => ({ ...prev, price: value }))}
+            />
+          ) : (
+            <>
+              <Form.Input
+                field="ratio"
+                label={t('模型倍率')}
+                placeholder={t('输入模型倍率')}
+                onChange={value => setCurrentModel(prev => ({ ...prev, ratio: value }))}
+              />
+              <Form.Input
+                field="completionRatio"
+                label={t('补全倍率')}
+                placeholder={t('输入补全价格')}
+                onChange={value => setCurrentModel(prev => ({ ...prev, completionRatio: value }))}
+              />
+            </>
+          )}
+        </Form>
+      </Modal>
+
+      {/* 批量设置弹窗 */}
+      <Modal
+        title={t('批量设置模型参数')}
+        visible={batchVisible}
+        onCancel={() => setBatchVisible(false)}
+        onOk={handleBatchFill}
+        width={500}
+      >
+        <Form>
+          <Form.Section text={t('设置类型')}>
+            <div style={{ marginBottom: '16px' }}>
+              <Space>
+                <Radio
+                  checked={batchFillType === 'price'}
+                  onChange={() => handleBatchTypeChange('price')}
+                >
+                  {t('固定价格')}
+                </Radio>
+                <Radio
+                  checked={batchFillType === 'ratio'}
+                  onChange={() => handleBatchTypeChange('ratio')}
+                >
+                  {t('模型倍率')}
+                </Radio>
+                <Radio
+                  checked={batchFillType === 'completionRatio'}
+                  onChange={() => handleBatchTypeChange('completionRatio')}
+                >
+                  {t('补全倍率')}
+                </Radio>
+                <Radio
+                  checked={batchFillType === 'bothRatio'}
+                  onChange={() => handleBatchTypeChange('bothRatio')}
+                >
+                  {t('模型倍率和补全倍率同时设置')}
+                </Radio>
+              </Space>
+            </div>
+          </Form.Section>
+          
+          {batchFillType === 'bothRatio' ? (
+            <>
+              <Form.Input
+                field="batchRatioValue"
+                label={t('模型倍率值')}
+                placeholder={t('请输入模型倍率')}
+                value={batchRatioValue}
+                onChange={value => setBatchRatioValue(value)}
+              />
+              <Form.Input
+                field="batchCompletionRatioValue"
+                label={t('补全倍率值')}
+                placeholder={t('请输入补全倍率')}
+                value={batchCompletionRatioValue}
+                onChange={value => setBatchCompletionRatioValue(value)}
+              />
+            </>
+          ) : (
+            <Form.Input
+              field="batchFillValue"
+              label={
+                batchFillType === 'price' 
+                  ? t('固定价格值') 
+                  : batchFillType === 'ratio'
+                    ? t('模型倍率值')
+                    : t('补全倍率值')
+              }
+              placeholder={t('请输入数值')}
+              value={batchFillValue}
+              onChange={value => setBatchFillValue(value)}
+            />
+          )}
+          
+          <Text type="tertiary">
+            {t('将为选中的 ')} <Text strong>{selectedRowKeys.length}</Text> {t(' 个模型设置相同的值')}
+          </Text>
+          <div style={{ marginTop: '8px' }}>
+            <Text type="tertiary">
+              {t('当前设置类型: ')} <Text strong>{
+                batchFillType === 'price' ? t('固定价格') : 
+                batchFillType === 'ratio' ? t('模型倍率') : 
+                batchFillType === 'completionRatio' ? t('补全倍率') : t('模型倍率和补全倍率')
+              }</Text>
+            </Text>
+          </div>
+        </Form>
+      </Modal>
+    </>
+  );
+}