Просмотр исходного кода

feat: Enhance Operation Settings with Group and Model Ratio Management

- Added new components for GroupRatioSettings and ModelRatioSettings to manage group and model ratios.
- Integrated tabs in OperationSetting to switch between model and visual ratio settings.
- Updated translations for new settings and improved existing ones in the English locale file.
- Refactored ModelSettingsVisualEditor to support dynamic pricing and ratio configurations.

This update improves the user interface for managing operational settings, enhancing usability and localization support.
CalciumIon 1 год назад
Родитель
Сommit
a4c43bb83b

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

@@ -1,5 +1,5 @@
 import React, { useEffect, useState } from 'react';
-import { Card, Spin } from '@douyinfe/semi-ui';
+import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
 import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js';
 import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js';
 import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js';
@@ -9,11 +9,16 @@ 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 GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js';
+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';
 
 const OperationSetting = () => {
+  const { t } = useTranslation();
   let [inputs, setInputs] = useState({
     QuotaForNewUser: 0,
     QuotaForInviter: 0,
@@ -138,13 +143,20 @@ const OperationSetting = () => {
         <Card style={{ marginTop: '10px' }}>
           <SettingsChats options={inputs} refresh={onRefresh} />
         </Card>
-        {/* 倍率设置 */}
+        {/* 分组倍率设置 */}
         <Card style={{ marginTop: '10px' }}>
-          <SettingsMagnification options={inputs} refresh={onRefresh} />
+          <GroupRatioSettings options={inputs} refresh={onRefresh} />
         </Card>
-        {/*可视化倍率设置*/}
+        {/* 合并模型倍率设置和可视化倍率设置 */}
         <Card style={{ marginTop: '10px' }}>
-          <ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
+          <Tabs type="line">
+            <Tabs.TabPane tab={t('模型倍率设置')} itemKey="model">
+              <ModelRatioSettings options={inputs} refresh={onRefresh} />
+            </Tabs.TabPane>
+            <Tabs.TabPane tab={t('可视化倍率设置')} itemKey="visual">
+              <ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
+            </Tabs.TabPane>
+          </Tabs>
         </Card>
       </Spin>
     </>

+ 16 - 2
web/src/i18n/locales/en.json

@@ -1161,7 +1161,7 @@
   "默认折叠侧边栏": "Default collapse sidebar",
   "聊天链接功能已经弃用,请使用下方聊天设置功能": "Chat link function has been deprecated, please use the chat settings below",
   "你似乎并没有修改什么": "You seem to have not modified anything",
-  "令牌聊天设置": "Token chat settings",
+  "令牌聊天设置": "Chat settings",
   "必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能": "Must set all chat links above to empty to use the chat settings below",
   "链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "The {key} in the link will be automatically replaced with sk-xxxx, the {address} will be automatically replaced with the server address in system settings, and the end will not have / and /v1",
   "聊天配置": "Chat configuration",
@@ -1217,5 +1217,19 @@
   "确定要修改所有子渠道权重为 ": "Confirm to modify all sub-channel weights to ",
   " 吗?": "?",
   "修改子渠道优先级": "Modify sub-channel priority",
-  "确定要修改所有子渠道优先级为 ": "Confirm to modify all sub-channel priorities to "
+  "确定要修改所有子渠道优先级为 ": "Confirm to modify all sub-channel priorities to ",
+  "分组设置": "Group settings",
+  "用户可选分组": "User selectable groups",
+  "保存分组倍率设置": "Save group ratio settings",
+  "模型倍率设置": "Model ratio settings",
+  "可视化倍率设置": "Visual model ratio settings",
+  "确定重置模型倍率吗?": "Confirm to reset model ratio?",
+  "模型固定价格": "Model price per call",
+  "模型补全倍率(仅对自定义模型有效)": "Model completion ratio (only effective for custom models)",
+  "保存模型倍率设置": "Save model ratio settings",
+  "重置模型倍率": "Reset model ratio",
+  "一次调用消耗多少刀,优先级大于模型倍率": "How much USD one call costs, priority over model ratio",
+  "仅对自定义模型有效": "Only effective for custom models",
+  "添加模型": "Add model",
+  "应用更改": "Apply changes"
 }

+ 131 - 0
web/src/pages/Setting/Operation/GroupRatioSettings.js

@@ -0,0 +1,131 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
+import {
+  compareObjects,
+  API,
+  showError,
+  showSuccess,
+  showWarning,
+  verifyJSON,
+} from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+export default function GroupRatioSettings(props) {
+  const { t } = useTranslation();
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    GroupRatio: '',
+    UserUsableGroups: ''
+  });
+  const refForm = useRef();
+  const [inputsRow, setInputsRow] = useState(inputs);
+
+  async function onSubmit() {
+    try {
+      await refForm.current.validate().then(() => {
+        const updateArray = compareObjects(inputs, inputsRow);
+        if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
+        
+        const requestQueue = updateArray.map((item) => {
+          const value = typeof inputs[item.key] === 'boolean' 
+            ? String(inputs[item.key]) 
+            : inputs[item.key];
+          return API.put('/api/option/', { key: item.key, value });
+        });
+
+        setLoading(true);
+        Promise.all(requestQueue)
+          .then((res) => {
+            if (res.includes(undefined)) {
+              return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
+            }
+            
+            for (let i = 0; i < res.length; i++) {
+              if (!res[i].data.success) {
+                return showError(res[i].data.message);
+              }
+            }
+            
+            showSuccess(t('保存成功'));
+            props.refresh();
+          })
+          .catch(error => {
+            console.error('Unexpected error:', error);
+            showError(t('保存失败,请重试'));
+          })
+          .finally(() => {
+            setLoading(false);
+          });
+      }).catch(() => {
+        showError(t('请检查输入'));
+      });
+    } catch (error) {
+      showError(t('请检查输入'));
+      console.error(error);
+    }
+  }
+
+  useEffect(() => {
+    const currentInputs = {};
+    for (let key in props.options) {
+      if (Object.keys(inputs).includes(key)) {
+        currentInputs[key] = props.options[key];
+      }
+    }
+    setInputs(currentInputs);
+    setInputsRow(structuredClone(currentInputs));
+    refForm.current.setValues(currentInputs);
+  }, [props.options]);
+
+  return (
+    <Spin spinning={loading}>
+      <Form
+        values={inputs}
+        getFormApi={(formAPI) => (refForm.current = formAPI)}
+        style={{ marginBottom: 15 }}
+      >
+        <Form.Section text={t('分组设置')}>
+          <Row gutter={16}>
+            <Col span={16}>
+              <Form.TextArea
+                label={t('分组倍率')}
+                placeholder={t('为一个 JSON 文本,键为分组名称,值为倍率')}
+                field={'GroupRatio'}
+                autosize={{ minRows: 6, maxRows: 12 }}
+                trigger='blur'
+                stopValidateWithError
+                rules={[
+                  {
+                    validator: (rule, value) => verifyJSON(value),
+                    message: t('不是合法的 JSON 字符串')
+                  }
+                ]}
+                onChange={(value) => setInputs({ ...inputs, GroupRatio: value })}
+              />
+            </Col>
+          </Row>
+          <Row gutter={16}>
+            <Col span={16}>
+              <Form.TextArea
+                label={t('用户可选分组')}
+                placeholder={t('为一个 JSON 文本,键为分组名称,值为分组描述')}
+                field={'UserUsableGroups'}
+                autosize={{ minRows: 6, maxRows: 12 }}
+                trigger='blur'
+                stopValidateWithError
+                rules={[
+                  {
+                    validator: (rule, value) => verifyJSON(value),
+                    message: t('不是合法的 JSON 字符串')
+                  }
+                ]}
+                onChange={(value) => setInputs({ ...inputs, UserUsableGroups: value })}
+              />
+            </Col>
+          </Row>
+        </Form.Section>
+      </Form>
+      <Button onClick={onSubmit}>{t('保存分组倍率设置')}</Button>
+    </Spin>
+  );
+} 

+ 178 - 0
web/src/pages/Setting/Operation/ModelRatioSettings.js

@@ -0,0 +1,178 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Popconfirm, Row, Space, Spin } from '@douyinfe/semi-ui';
+import {
+  compareObjects,
+  API,
+  showError,
+  showSuccess,
+  showWarning,
+  verifyJSON,
+} from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+export default function ModelRatioSettings(props) {
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    ModelPrice: '',
+    ModelRatio: '',
+    CompletionRatio: '',
+  });
+  const refForm = useRef();
+  const [inputsRow, setInputsRow] = useState(inputs);
+  const { t } = useTranslation();
+
+  async function onSubmit() {
+    try {
+      await refForm.current.validate().then(() => {
+        const updateArray = compareObjects(inputs, inputsRow);
+        if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
+        
+        const requestQueue = updateArray.map((item) => {
+          const value = typeof inputs[item.key] === 'boolean' 
+            ? String(inputs[item.key]) 
+            : inputs[item.key];
+          return API.put('/api/option/', { key: item.key, value });
+        });
+
+        setLoading(true);
+        Promise.all(requestQueue)
+          .then((res) => {
+            if (res.includes(undefined)) {
+              return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
+            }
+            
+            for (let i = 0; i < res.length; i++) {
+              if (!res[i].data.success) {
+                return showError(res[i].data.message);
+              }
+            }
+            
+            showSuccess(t('保存成功'));
+            props.refresh();
+          })
+          .catch(error => {
+            console.error('Unexpected error:', error);
+            showError(t('保存失败,请重试'));
+          })
+          .finally(() => {
+            setLoading(false);
+          });
+      }).catch(() => {
+        showError(t('请检查输入'));
+      });
+    } catch (error) {
+      showError(t('请检查输入'));
+      console.error(error);
+    }
+  }
+
+  async function resetModelRatio() {
+    try {
+      let res = await API.post(`/api/option/rest_model_ratio`);
+      if (res.data.success) {
+        showSuccess(res.data.message);
+        props.refresh();
+      } else {
+        showError(res.data.message);
+      }
+    } catch (error) {
+      showError(error);
+    }
+  }
+
+  useEffect(() => {
+    const currentInputs = {};
+    for (let key in props.options) {
+      if (Object.keys(inputs).includes(key)) {
+        currentInputs[key] = props.options[key];
+      }
+    }
+    setInputs(currentInputs);
+    setInputsRow(structuredClone(currentInputs));
+    refForm.current.setValues(currentInputs);
+  }, [props.options]);
+
+  return (
+    <Spin spinning={loading}>
+      <Form
+        values={inputs}
+        getFormApi={(formAPI) => (refForm.current = formAPI)}
+        style={{ marginBottom: 15 }}
+      >
+        <Form.Section>
+          <Row gutter={16}>
+            <Col span={16}>
+              <Form.TextArea
+                label={t('模型固定价格')}
+                extraText={t('一次调用消耗多少刀,优先级大于模型倍率')}
+                placeholder={t('为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀')}
+                field={'ModelPrice'}
+                autosize={{ minRows: 6, maxRows: 12 }}
+                trigger='blur'
+                stopValidateWithError
+                rules={[
+                  {
+                    validator: (rule, value) => verifyJSON(value),
+                    message: '不是合法的 JSON 字符串'
+                  }
+                ]}
+                onChange={(value) => setInputs({ ...inputs, ModelPrice: value })}
+              />
+            </Col>
+          </Row>
+          <Row gutter={16}>
+            <Col span={16}>
+              <Form.TextArea
+                label={t('模型倍率')}
+                placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
+                field={'ModelRatio'}
+                autosize={{ minRows: 6, maxRows: 12 }}
+                trigger='blur'
+                stopValidateWithError
+                rules={[
+                  {
+                    validator: (rule, value) => verifyJSON(value),
+                    message: '不是合法的 JSON 字符串'
+                  }
+                ]}
+                onChange={(value) => setInputs({ ...inputs, ModelRatio: value })}
+              />
+            </Col>
+          </Row>
+          <Row gutter={16}>
+            <Col span={16}>
+              <Form.TextArea
+                label={t('模型补全倍率(仅对自定义模型有效)')}
+                extraText={t('仅对自定义模型有效')}
+                placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
+                field={'CompletionRatio'}
+                autosize={{ minRows: 6, maxRows: 12 }}
+                trigger='blur'
+                stopValidateWithError
+                rules={[
+                  {
+                    validator: (rule, value) => verifyJSON(value),
+                    message: '不是合法的 JSON 字符串'
+                  }
+                ]}
+                onChange={(value) => setInputs({ ...inputs, CompletionRatio: value })}
+              />
+            </Col>
+          </Row>
+        </Form.Section>
+      </Form>
+      <Space>
+        <Button onClick={onSubmit}>{t('保存模型倍率设置')}</Button>
+        <Popconfirm
+          title={t('确定重置模型倍率吗?')}
+          content={t('此修改将不可逆')}
+          okType={'danger'}
+          position={'top'}
+          onConfirm={resetModelRatio}
+        >
+          <Button type={'danger'}>{t('重置模型倍率')}</Button>
+        </Popconfirm>
+      </Space>
+    </Spin>
+  );
+} 

+ 58 - 35
web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js

@@ -4,7 +4,10 @@ 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';
+import { useTranslation } from 'react-i18next';
+
 export default function ModelSettingsVisualEditor(props) {
+  const { t } = useTranslation();
   const [models, setModels] = useState([]);
   const [visible, setVisible] = useState(false);
   const [currentModel, setCurrentModel] = useState(null);
@@ -122,51 +125,50 @@ export default function ModelSettingsVisualEditor(props) {
 
   const columns = [
     {
-      title: '模型名称',
+      title: t('模型名称'),
       dataIndex: 'name',
       key: 'name',
     },
     {
-      title: '固定价格',
+      title: t('模型固定价格'),
       dataIndex: 'price',
       key: 'price',
       render: (text, record) => (
         <Input
           value={text}
-          placeholder="按量计价"
+          placeholder={t('按量计费')}
           onChange={value => updateModel(record.name, 'price', value)}
         />
       )
     },
     {
-      title: '模型倍率',
+      title: t('模型倍率'),
       dataIndex: 'ratio',
       key: 'ratio',
       render: (text, record) => (
         <Input
           value={text}
-
-          placeholder={record.price !== '' ? '固定价格' : '默认补全倍率'}
+          placeholder={record.price !== '' ? t('模型倍率') : t('默认补全倍率')}
           disabled={record.price !== ''}
           onChange={value => updateModel(record.name, 'ratio', value)}
         />
       )
     },
     {
-      title: '补全倍率',
+      title: t('补全倍率'),
       dataIndex: 'completionRatio',
       key: 'completionRatio',
       render: (text, record) => (
         <Input
           value={text}
-          placeholder={record.price !== '' ? '固定价格' : '默认补全倍率'}
+          placeholder={record.price !== '' ? t('补全倍率') : t('默认补全倍率')}
           disabled={record.price !== ''}
           onChange={value => updateModel(record.name, 'completionRatio', value)}
         />
       )
     },
     {
-      title: '操作',
+      title: t('操作'),
       key: 'action',
       render: (_, record) => (
         <Button
@@ -219,22 +221,20 @@ export default function ModelSettingsVisualEditor(props) {
 
   return (
     <>
-      <h3>模型价格</h3>
       <Space vertical align="start" style={{ width: '100%' }}>
         <Space>
           <Button icon={<IconPlus />} onClick={() => setVisible(true)}>
-            添加模型
+            {t('添加模型')}
           </Button>
           <Button type="primary" icon={<IconSave />} onClick={SubmitData}>
-            应用更改
+            {t('应用更改')}
           </Button>
           <Input
             prefix={<IconSearch />}
-            placeholder="搜索模型名称"
+            placeholder={t('搜索模型名称')}
             value={searchText}
             onChange={value => {
               setSearchText(value)
-              // 搜索时重置页码
               setCurrentPage(1);
             }}
             style={{ width: 200 }}
@@ -242,12 +242,18 @@ export default function ModelSettingsVisualEditor(props) {
         </Space>
         <Table
           columns={columns}
-          dataSource={pagedData} // 使用分页后的数据
+          dataSource={pagedData}
           pagination={{
             currentPage: currentPage,
             pageSize: pageSize,
             total: filteredModels.length,
             onPageChange: page => setCurrentPage(page),
+            formatPageText: (page) =>
+              t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
+                start: page.currentStart,
+                end: page.currentEnd,
+                total: filteredModels.length
+              }),
             showTotal: true,
             showSizeChanger: false
           }}
@@ -255,7 +261,7 @@ export default function ModelSettingsVisualEditor(props) {
       </Space>
 
       <Modal
-        title="添加模型"
+        title={t('添加模型')}
         visible={visible}
         onCancel={() => setVisible(false)}
         onOk={() => {
@@ -263,32 +269,49 @@ export default function ModelSettingsVisualEditor(props) {
         }}
       >
         <Form>
-          <p>请输入固定价格或者模型倍率+补全倍率</p>
           <Form.Input
             field="name"
-            label="模型名称"
+            label={t('模型名称')}
             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.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>
     </>