Explorar o código

Merge pull request #618 from HynoR/feat/modeledit

feat: 可视化模型定价编辑器
Calcium-Ion hai 1 ano
pai
achega
6ca68651ff

+ 5 - 0
web/src/components/OperationSetting.js

@@ -8,6 +8,7 @@ import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashbo
 import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js';
 import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
 import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
+import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js';
 
 import { API, showError, showSuccess } from '../helpers';
 import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
@@ -141,6 +142,10 @@ const OperationSetting = () => {
         <Card style={{ marginTop: '10px' }}>
           <SettingsMagnification options={inputs} refresh={onRefresh} />
         </Card>
+        {/*可视化倍率设置*/}
+        <Card style={{ marginTop: '10px' }}>
+          <ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
+        </Card>
       </Spin>
     </>
   );

+ 296 - 0
web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js

@@ -0,0 +1,296 @@
+// ModelSettingsVisualEditor.js
+import React, { useEffect, useState } from 'react';
+import { Table, Button, Input, Modal, Form, Space } from '@douyinfe/semi-ui';
+import { IconDelete, IconPlus, IconSearch, IconSave } from '@douyinfe/semi-icons';
+import { showError, showSuccess } from '../../../helpers';
+import { API } from '../../../helpers';
+export default function ModelSettingsVisualEditor(props) {
+  const [models, setModels] = useState([]);
+  const [visible, setVisible] = useState(false);
+  const [currentModel, setCurrentModel] = useState(null);
+  const [searchText, setSearchText] = useState('');
+  const [currentPage, setCurrentPage] = useState(1);
+  const [loading, setLoading] = useState(false);
+  const pageSize = 10;
+
+  useEffect(() => {
+    try {
+      const modelPrice = JSON.parse(props.options.ModelPrice || '{}');
+      const modelRatio = JSON.parse(props.options.ModelRatio || '{}');
+      const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
+
+      // 合并所有模型名称
+      const modelNames = new Set([
+        ...Object.keys(modelPrice),
+        ...Object.keys(modelRatio),
+        ...Object.keys(completionRatio)
+      ]);
+
+      const modelData = Array.from(modelNames).map(name => ({
+        name,
+        price: modelPrice[name] === undefined ? '' : modelPrice[name],
+        ratio: modelRatio[name] === undefined ? '' : modelRatio[name],
+        completionRatio: completionRatio[name] === undefined ? '' : completionRatio[name]
+      }));
+
+      setModels(modelData);
+    } catch (error) {
+      console.error('JSON解析错误:', error);
+    }
+  }, [props.options]);
+
+  // 首先声明分页相关的工具函数
+  const getPagedData = (data, currentPage, pageSize) => {
+    const start = (currentPage - 1) * pageSize;
+    const end = start + pageSize;
+    return data.slice(start, end);
+  };
+
+  // 在 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: {},
+      ModelRatio: {},
+      CompletionRatio: {}
+    };
+    let currentConvertModelName = '';
+
+    try {
+      // 数据转换
+      models.forEach(model => {
+        currentConvertModelName = model.name;
+        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('部分保存失败,请重试');
+        }
+      }
+
+      // 检查每个请求的结果
+      for (const res of results) {
+        if (!res.data.success) {
+          return showError(res.data.message);
+        }
+      }
+
+      showSuccess('保存成功');
+      props.refresh();
+
+    } catch (error) {
+      console.error('保存失败:', error);
+      showError('保存失败,请重试');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const columns = [
+    {
+      title: '模型名称',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '固定价格',
+      dataIndex: 'price',
+      key: 'price',
+      render: (text, record) => (
+        <Input
+          value={text}
+          placeholder="按量计价"
+          onChange={value => updateModel(record.name, 'price', value)}
+        />
+      )
+    },
+    {
+      title: '模型倍率',
+      dataIndex: 'ratio',
+      key: 'ratio',
+      render: (text, record) => (
+        <Input
+          value={text}
+
+          placeholder={record.price !== '' ? '固定价格' : '默认补全倍率'}
+          disabled={record.price !== ''}
+          onChange={value => updateModel(record.name, 'ratio', value)}
+        />
+      )
+    },
+    {
+      title: '补全倍率',
+      dataIndex: 'completionRatio',
+      key: 'completionRatio',
+      render: (text, record) => (
+        <Input
+          value={text}
+          placeholder={record.price !== '' ? '固定价格' : '默认补全倍率'}
+          disabled={record.price !== ''}
+          onChange={value => updateModel(record.name, 'completionRatio', value)}
+        />
+      )
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_, record) => (
+        <Button
+          icon={<IconDelete />}
+          type="danger"
+          onClick={() => deleteModel(record.name)}
+        />
+      )
+    }
+  ];
+
+  const updateModel = (name, field, value) => {
+    if (isNaN(value)) {
+      showError('请输入数字');
+      return;
+    }
+    setModels(prev =>
+      prev.map(model =>
+        model.name === name
+          ? { ...model, [field]: value }
+          : model
+      )
+    );
+  };
+
+  const deleteModel = (name) => {
+    setModels(prev => prev.filter(model => model.name !== name));
+  };
+  const addModel = (values) => {
+    // 检查模型名称是否存在, 如果存在则拒绝添加
+    if (models.some(model => model.name === values.name)) {
+      showError('模型名称已存在');
+      return;
+    }
+    // 不允许同时添加固定价格和倍率
+    if (values.price !== '' && (values.ratio !== '' || values.completionRatio !== '')) {
+      showError('固定价格和倍率不能同时存在');
+      return;
+    }
+    setModels(prev => [{
+      name: values.name,
+      price: values.price || '',
+      ratio: values.ratio || '',
+      completionRatio: values.completionRatio || ''
+    }, ...prev]);
+    setVisible(false);
+    showSuccess('添加成功');
+  };
+
+
+  return (
+    <>
+      <h3>模型价格</h3>
+      <Space vertical align="start" style={{ width: '100%' }}>
+        <Space>
+          <Button icon={<IconPlus />} onClick={() => setVisible(true)}>
+            添加模型
+          </Button>
+          <Button type="primary" icon={<IconSave />} onClick={SubmitData}>
+            应用更改
+          </Button>
+          <Input
+            prefix={<IconSearch />}
+            placeholder="搜索模型名称"
+            value={searchText}
+            onChange={value => {
+              setSearchText(value)
+              // 搜索时重置页码
+              setCurrentPage(1);
+            }}
+            style={{ width: 200 }}
+          />
+        </Space>
+        <Table
+          columns={columns}
+          dataSource={pagedData} // 使用分页后的数据
+          pagination={{
+            currentPage: currentPage,
+            pageSize: pageSize,
+            total: filteredModels.length,
+            onPageChange: page => setCurrentPage(page),
+            showTotal: true,
+            showSizeChanger: false
+          }}
+        />
+      </Space>
+
+      <Modal
+        title="添加模型"
+        visible={visible}
+        onCancel={() => setVisible(false)}
+        onOk={() => {
+          currentModel && addModel(currentModel);
+        }}
+      >
+        <Form>
+          <p>请输入固定价格或者模型倍率+补全倍率</p>
+          <Form.Input
+            field="name"
+            label="模型名称"
+            placeholder="strawberry"
+            required
+            onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))}
+          />
+          <Form.Input
+            field="price"
+            label="固定价格(每次)"
+            placeholder="输入每次价格"
+            onChange={value => setCurrentModel(prev => ({ ...prev, price: value }))}
+          />
+          <Form.Input
+            field="ratio"
+            label="模型倍率"
+            placeholder="输入模型倍率"
+            onChange={value => setCurrentModel(prev => ({ ...prev, ratio: value }))}
+          />
+          <Form.Input
+            field="completionRatio"
+            label="补全倍率"
+            placeholder="输入补全价格"
+            onChange={value => setCurrentModel(prev => ({ ...prev, completionRatio: value }))}
+          />
+        </Form>
+      </Modal>
+    </>
+  );
+}