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

Merge pull request #248 from QuentinHsu/refactor-settings-operation

Refactor settings operation
Calcium-Ion 1 год назад
Родитель
Сommit
ced67b9bb3

+ 2 - 1
web/package.json

@@ -5,11 +5,12 @@
   "type": "module",
   "dependencies": {
     "@douyinfe/semi-icons": "^2.46.1",
-    "@douyinfe/semi-ui": "^2.46.1",
+    "@douyinfe/semi-ui": "^2.55.3",
     "@visactor/react-vchart": "~1.8.8",
     "@visactor/vchart": "~1.8.8",
     "@visactor/vchart-semi-theme": "~1.8.8",
     "axios": "^0.27.2",
+    "dayjs": "^1.11.11",
     "history": "^5.3.0",
     "marked": "^4.1.1",
     "react": "^18.2.0",

+ 83 - 583
web/src/components/OperationSetting.js

@@ -1,17 +1,17 @@
 import React, { useEffect, useState } from 'react';
-import { Divider, Form, Grid, Header } from 'semantic-ui-react';
-import {
-  API,
-  showError,
-  showSuccess,
-  timestamp2string,
-  verifyJSON,
-} from '../helpers';
+import { Card, Spin } 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';
+import SettingsLog from '../pages/Setting/Operation/SettingsLog.js';
+import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashboard.js';
+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 { useTheme } from '../context/Theme';
+import { API, showError, showSuccess } from '../helpers';
 
 const OperationSetting = () => {
-  let now = new Date();
   let [inputs, setInputs] = useState({
     QuotaForNewUser: 0,
     QuotaForInviter: 0,
@@ -27,39 +27,31 @@ const OperationSetting = () => {
     ChatLink: '',
     ChatLink2: '', // 添加的新状态变量
     QuotaPerUnit: 0,
-    AutomaticDisableChannelEnabled: '',
-    AutomaticEnableChannelEnabled: '',
+    AutomaticDisableChannelEnabled: false,
+    AutomaticEnableChannelEnabled: false,
     ChannelDisableThreshold: 0,
-    LogConsumeEnabled: '',
-    DisplayInCurrencyEnabled: '',
-    DisplayTokenStatEnabled: '',
-    CheckSensitiveEnabled: '',
-    CheckSensitiveOnPromptEnabled: '',
+    LogConsumeEnabled: false,
+    DisplayInCurrencyEnabled: false,
+    DisplayTokenStatEnabled: false,
+    CheckSensitiveEnabled: false,
+    CheckSensitiveOnPromptEnabled: false,
     CheckSensitiveOnCompletionEnabled: '',
     StopOnSensitiveEnabled: '',
     SensitiveWords: '',
-    MjNotifyEnabled: '',
-    MjAccountFilterEnabled: '',
-    MjModeClearEnabled: '',
-    MjForwardUrlEnabled: '',
-    DrawingEnabled: '',
-    DataExportEnabled: '',
+    MjNotifyEnabled: false,
+    MjAccountFilterEnabled: false,
+    MjModeClearEnabled: false,
+    MjForwardUrlEnabled: false,
+    DrawingEnabled: false,
+    DataExportEnabled: false,
     DataExportDefaultTime: 'hour',
     DataExportInterval: 5,
-    DefaultCollapseSidebar: '', // 默认折叠侧边栏
+    DefaultCollapseSidebar: false, // 默认折叠侧边栏
     RetryTimes: 0,
   });
-  const [originInputs, setOriginInputs] = useState({});
+
   let [loading, setLoading] = useState(false);
-  let [historyTimestamp, setHistoryTimestamp] = useState(
-    timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600),
-  ); // a month ago
-  // 精确时间选项(小时,天,周)
-  const timeOptions = [
-    { key: 'hour', text: '小时', value: 'hour' },
-    { key: 'day', text: '天', value: 'day' },
-    { key: 'week', text: '周', value: 'week' },
-  ];
+
   const getOptions = async () => {
     const res = await API.get('/api/option/');
     const { success, message, data } = res.data;
@@ -74,566 +66,74 @@ const OperationSetting = () => {
         ) {
           item.value = JSON.stringify(JSON.parse(item.value), null, 2);
         }
-        newInputs[item.key] = item.value;
+        if (
+          item.key.endsWith('Enabled') ||
+          ['DefaultCollapseSidebar'].includes(item.key)
+        ) {
+          newInputs[item.key] = item.value === 'true' ? true : false;
+        } else {
+          newInputs[item.key] = item.value;
+        }
       });
+
       setInputs(newInputs);
-      setOriginInputs(newInputs);
     } else {
       showError(message);
     }
   };
-
-  const theme = useTheme();
-  const isDark = theme === 'dark';
+  async function onRefresh() {
+    try {
+      setLoading(true);
+      await getOptions();
+      showSuccess('刷新成功');
+    } catch (error) {
+      showError('刷新失败');
+    } finally {
+      setLoading(false);
+    }
+  }
 
   useEffect(() => {
-    getOptions().then();
+    onRefresh();
   }, []);
 
-  const updateOption = async (key, value) => {
-    setLoading(true);
-    if (key.endsWith('Enabled')) {
-      value = inputs[key] === 'true' ? 'false' : 'true';
-    }
-    if (key === 'DefaultCollapseSidebar') {
-      value = inputs[key] === 'true' ? 'false' : 'true';
-    }
-    console.log(key, value);
-    const res = await API.put('/api/option/', {
-      key,
-      value,
-    });
-    const { success, message } = res.data;
-    if (success) {
-      setInputs((inputs) => ({ ...inputs, [key]: value }));
-    } else {
-      showError(message);
-    }
-    setLoading(false);
-  };
-
-  const handleInputChange = async (e, { name, value }) => {
-    if (
-      name.endsWith('Enabled') ||
-      name === 'DataExportInterval' ||
-      name === 'DataExportDefaultTime' ||
-      name === 'DefaultCollapseSidebar'
-    ) {
-      if (name === 'DataExportDefaultTime') {
-        localStorage.setItem('data_export_default_time', value);
-      } else if (name === 'MjNotifyEnabled') {
-        localStorage.setItem('mj_notify_enabled', value);
-      }
-      await updateOption(name, value);
-    } else {
-      setInputs((inputs) => ({ ...inputs, [name]: value }));
-    }
-  };
-
-  const submitConfig = async (group) => {
-    switch (group) {
-      case 'monitor':
-        if (
-          originInputs['ChannelDisableThreshold'] !==
-          inputs.ChannelDisableThreshold
-        ) {
-          await updateOption(
-            'ChannelDisableThreshold',
-            inputs.ChannelDisableThreshold,
-          );
-        }
-        if (
-          originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold
-        ) {
-          await updateOption(
-            'QuotaRemindThreshold',
-            inputs.QuotaRemindThreshold,
-          );
-        }
-        break;
-      case 'ratio':
-        if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
-          if (!verifyJSON(inputs.ModelRatio)) {
-            showError('模型倍率不是合法的 JSON 字符串');
-            return;
-          }
-          await updateOption('ModelRatio', inputs.ModelRatio);
-        }
-        if (originInputs['CompletionRatio'] !== inputs.CompletionRatio) {
-          if (!verifyJSON(inputs.CompletionRatio)) {
-            showError('模型补全倍率不是合法的 JSON 字符串');
-            return;
-          }
-          await updateOption('CompletionRatio', inputs.CompletionRatio);
-        }
-        if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
-          if (!verifyJSON(inputs.GroupRatio)) {
-            showError('分组倍率不是合法的 JSON 字符串');
-            return;
-          }
-          await updateOption('GroupRatio', inputs.GroupRatio);
-        }
-        if (originInputs['ModelPrice'] !== inputs.ModelPrice) {
-          if (!verifyJSON(inputs.ModelPrice)) {
-            showError('模型固定价格不是合法的 JSON 字符串');
-            return;
-          }
-          await updateOption('ModelPrice', inputs.ModelPrice);
-        }
-        break;
-      case 'words':
-        if (originInputs['SensitiveWords'] !== inputs.SensitiveWords) {
-          await updateOption('SensitiveWords', inputs.SensitiveWords);
-        }
-        break;
-      case 'quota':
-        if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
-          await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
-        }
-        if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
-          await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
-        }
-        if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
-          await updateOption('QuotaForInviter', inputs.QuotaForInviter);
-        }
-        if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
-          await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
-        }
-        break;
-      case 'general':
-        if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
-          await updateOption('TopUpLink', inputs.TopUpLink);
-        }
-        if (originInputs['ChatLink'] !== inputs.ChatLink) {
-          await updateOption('ChatLink', inputs.ChatLink);
-        }
-        if (originInputs['ChatLink2'] !== inputs.ChatLink2) {
-          await updateOption('ChatLink2', inputs.ChatLink2);
-        }
-        if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
-          await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
-        }
-        if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
-          await updateOption('RetryTimes', inputs.RetryTimes);
-        }
-        break;
-    }
-  };
-
-  const deleteHistoryLogs = async () => {
-    console.log(inputs);
-    const res = await API.delete(
-      `/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`,
-    );
-    const { success, message, data } = res.data;
-    if (success) {
-      showSuccess(`${data} 条日志已清理!`);
-      return;
-    }
-    showError('日志清理失败:' + message);
-  };
   return (
-    <Grid columns={1}>
-      <Grid.Column>
-        <Form loading={loading} inverted={isDark}>
-          <Header as='h3' inverted={isDark}>
-            通用设置
-          </Header>
-          <Form.Group widths={4}>
-            <Form.Input
-              label='充值链接'
-              name='TopUpLink'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.TopUpLink}
-              type='link'
-              placeholder='例如发卡网站的购买链接'
-            />
-            <Form.Input
-              label='默认聊天页面链接'
-              name='ChatLink'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.ChatLink}
-              type='link'
-              placeholder='例如 ChatGPT Next Web 的部署地址'
-            />
-            <Form.Input
-              label='聊天页面2链接'
-              name='ChatLink2'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.ChatLink2}
-              type='link'
-              placeholder='例如 ChatGPT Web & Midjourney 的部署地址'
-            />
-            <Form.Input
-              label='单位美元额度'
-              name='QuotaPerUnit'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.QuotaPerUnit}
-              type='number'
-              step='0.01'
-              placeholder='一单位货币能兑换的额度'
-            />
-            <Form.Input
-              label='失败重试次数'
-              name='RetryTimes'
-              type={'number'}
-              step='1'
-              min='0'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.RetryTimes}
-              placeholder='失败重试次数'
-            />
-          </Form.Group>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.DisplayInCurrencyEnabled === 'true'}
-              label='以货币形式显示额度'
-              name='DisplayInCurrencyEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.DisplayTokenStatEnabled === 'true'}
-              label='Billing 相关 API 显示令牌额度而非用户额度'
-              name='DisplayTokenStatEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.DefaultCollapseSidebar === 'true'}
-              label='默认折叠侧边栏'
-              name='DefaultCollapseSidebar'
-              onChange={handleInputChange}
-            />
-          </Form.Group>
-          <Form.Button
-            onClick={() => {
-              submitConfig('general').then();
-            }}
-          >
-            保存通用设置
-          </Form.Button>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            绘图设置
-          </Header>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.DrawingEnabled === 'true'}
-              label='启用绘图功能'
-              name='DrawingEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.MjNotifyEnabled === 'true'}
-              label='允许回调(会泄露服务器ip地址)'
-              name='MjNotifyEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.MjAccountFilterEnabled === 'true'}
-              label='允许AccountFilter参数'
-              name='MjAccountFilterEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.MjForwardUrlEnabled === 'true'}
-              label='开启之后将上游地址替换为服务器地址'
-              name='MjForwardUrlEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.MjModeClearEnabled === 'true'}
-              label='开启之后会清除用户提示词中的--fast、--relax以及--turbo参数'
-              name='MjModeClearEnabled'
-              onChange={handleInputChange}
-            />
-          </Form.Group>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            屏蔽词过滤设置
-          </Header>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.CheckSensitiveEnabled === 'true'}
-              label='启用屏蔽词过滤功能'
-              name='CheckSensitiveEnabled'
-              onChange={handleInputChange}
-            />
-          </Form.Group>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.CheckSensitiveOnPromptEnabled === 'true'}
-              label='启用prompt检查'
-              name='CheckSensitiveOnPromptEnabled'
-              onChange={handleInputChange}
-            />
-            {/*<Form.Checkbox*/}
-            {/*  checked={inputs.CheckSensitiveOnCompletionEnabled === 'true'}*/}
-            {/*  label='启用生成内容检查'*/}
-            {/*  name='CheckSensitiveOnCompletionEnabled'*/}
-            {/*  onChange={handleInputChange}*/}
-            {/*/>*/}
-          </Form.Group>
-          {/*<Form.Group inline>*/}
-          {/*  <Form.Checkbox*/}
-          {/*    checked={inputs.StopOnSensitiveEnabled === 'true'}*/}
-          {/*    label='在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词'*/}
-          {/*    name='StopOnSensitiveEnabled'*/}
-          {/*    onChange={handleInputChange}*/}
-          {/*  />*/}
-          {/*</Form.Group>*/}
-          {/*<Form.Group>*/}
-          {/*  <Form.Input*/}
-          {/*    label="流模式下缓存队列,默认不缓存,设置越大检测越准确,但是回复会有卡顿感"*/}
-          {/*    name="StreamCacheTextLength"*/}
-          {/*    onChange={handleInputChange}*/}
-          {/*    value={inputs.StreamCacheQueueLength}*/}
-          {/*    type="number"*/}
-          {/*    min="0"*/}
-          {/*    placeholder="例如:10"*/}
-          {/*  />*/}
-          {/*</Form.Group>*/}
-          <Form.Group widths='equal'>
-            <Form.TextArea
-              label='屏蔽词列表,一行一个屏蔽词,不需要符号分割'
-              name='SensitiveWords'
-              onChange={handleInputChange}
-              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
-              value={inputs.SensitiveWords}
-              placeholder='一行一个屏蔽词'
-            />
-          </Form.Group>
-          <Form.Button
-            onClick={() => {
-              submitConfig('words').then();
-            }}
-          >
-            保存屏蔽词设置
-          </Form.Button>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            日志设置
-          </Header>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.LogConsumeEnabled === 'true'}
-              label='启用额度消费日志记录'
-              name='LogConsumeEnabled'
-              onChange={handleInputChange}
-            />
-          </Form.Group>
-          <Form.Group widths={4}>
-            <Form.Input
-              label='目标时间'
-              value={historyTimestamp}
-              type='datetime-local'
-              name='history_timestamp'
-              onChange={(e, { name, value }) => {
-                setHistoryTimestamp(value);
-              }}
-            />
-          </Form.Group>
-          <Form.Button
-            onClick={() => {
-              deleteHistoryLogs().then();
-            }}
-          >
-            清理历史日志
-          </Form.Button>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            数据看板
-          </Header>
-          <Form.Checkbox
-            checked={inputs.DataExportEnabled === 'true'}
-            label='启用数据看板(实验性)'
-            name='DataExportEnabled'
-            onChange={handleInputChange}
-          />
-          <Form.Group>
-            <Form.Input
-              label='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
-              name='DataExportInterval'
-              type={'number'}
-              step='1'
-              min='1'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.DataExportInterval}
-              placeholder='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
-            />
-            <Form.Select
-              label='数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)'
-              options={timeOptions}
-              name='DataExportDefaultTime'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.DataExportDefaultTime}
-              placeholder='数据看板默认时间粒度'
-            />
-          </Form.Group>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            监控设置
-          </Header>
-          <Form.Group widths={3}>
-            <Form.Input
-              label='最长响应时间'
-              name='ChannelDisableThreshold'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.ChannelDisableThreshold}
-              type='number'
-              min='0'
-              placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
-            />
-            <Form.Input
-              label='额度提醒阈值'
-              name='QuotaRemindThreshold'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.QuotaRemindThreshold}
-              type='number'
-              min='0'
-              placeholder='低于此额度时将发送邮件提醒用户'
-            />
-          </Form.Group>
-          <Form.Group inline>
-            <Form.Checkbox
-              checked={inputs.AutomaticDisableChannelEnabled === 'true'}
-              label='失败时自动禁用通道'
-              name='AutomaticDisableChannelEnabled'
-              onChange={handleInputChange}
-            />
-            <Form.Checkbox
-              checked={inputs.AutomaticEnableChannelEnabled === 'true'}
-              label='成功时自动启用通道'
-              name='AutomaticEnableChannelEnabled'
-              onChange={handleInputChange}
-            />
-          </Form.Group>
-          <Form.Button
-            onClick={() => {
-              submitConfig('monitor').then();
-            }}
-          >
-            保存监控设置
-          </Form.Button>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            额度设置
-          </Header>
-          <Form.Group widths={4}>
-            <Form.Input
-              label='新用户初始额度'
-              name='QuotaForNewUser'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.QuotaForNewUser}
-              type='number'
-              min='0'
-              placeholder='例如:100'
-            />
-            <Form.Input
-              label='请求预扣费额度'
-              name='PreConsumedQuota'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.PreConsumedQuota}
-              type='number'
-              min='0'
-              placeholder='请求结束后多退少补'
-            />
-            <Form.Input
-              label='邀请新用户奖励额度'
-              name='QuotaForInviter'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.QuotaForInviter}
-              type='number'
-              min='0'
-              placeholder='例如:2000'
-            />
-            <Form.Input
-              label='新用户使用邀请码奖励额度'
-              name='QuotaForInvitee'
-              onChange={handleInputChange}
-              autoComplete='new-password'
-              value={inputs.QuotaForInvitee}
-              type='number'
-              min='0'
-              placeholder='例如:1000'
-            />
-          </Form.Group>
-          <Form.Button
-            onClick={() => {
-              submitConfig('quota').then();
-            }}
-          >
-            保存额度设置
-          </Form.Button>
-          <Divider />
-          <Header as='h3' inverted={isDark}>
-            倍率设置
-          </Header>
-          <Form.Group widths='equal'>
-            <Form.TextArea
-              label='模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)'
-              name='ModelPrice'
-              onChange={handleInputChange}
-              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
-              autoComplete='new-password'
-              value={inputs.ModelPrice}
-              placeholder='为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀'
-            />
-          </Form.Group>
-          <Form.Group widths='equal'>
-            <Form.TextArea
-              label='模型倍率'
-              name='ModelRatio'
-              onChange={handleInputChange}
-              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
-              autoComplete='new-password'
-              value={inputs.ModelRatio}
-              placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
-            />
-          </Form.Group>
-          <Form.Group widths='equal'>
-            <Form.TextArea
-              label='模型补全倍率(仅对自定义模型有效)'
-              name='CompletionRatio'
-              onChange={handleInputChange}
-              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
-              autoComplete='new-password'
-              value={inputs.CompletionRatio}
-              placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
-            />
-          </Form.Group>
-          <Form.Group widths='equal'>
-            <Form.TextArea
-              label='分组倍率'
-              name='GroupRatio'
-              onChange={handleInputChange}
-              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
-              autoComplete='new-password'
-              value={inputs.GroupRatio}
-              placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
-            />
-          </Form.Group>
-          <Form.Button
-            onClick={() => {
-              submitConfig('ratio').then();
-            }}
-          >
-            保存倍率设置
-          </Form.Button>
-        </Form>
-      </Grid.Column>
-    </Grid>
+    <>
+      <Spin spinning={loading} size='large'>
+        {/* 通用设置 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsGeneral options={inputs} refresh={onRefresh} />
+        </Card>
+        {/* 绘图设置 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsDrawing options={inputs} refresh={onRefresh} />
+        </Card>
+        {/* 屏蔽词过滤设置 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsSensitiveWords options={inputs} refresh={onRefresh} />
+        </Card>
+        {/* 日志设置 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsLog options={inputs} refresh={onRefresh} />
+        </Card>
+        {/* 数据看板 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsDataDashboard options={inputs} refresh={onRefresh} />
+        </Card>
+        {/* 监控设置 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsMonitoring options={inputs} refresh={onRefresh} />
+        </Card>
+        {/* 额度设置 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsCreditLimit options={inputs} refresh={onRefresh} />
+        </Card>
+        {/* 倍率设置 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsMagnification options={inputs} refresh={onRefresh} />
+        </Card>
+      </Spin>
+    </>
   );
 };
 

+ 25 - 0
web/src/helpers/utils.js

@@ -220,3 +220,28 @@ export function shouldShowPrompt(id) {
 export function setPromptShown(id) {
   localStorage.setItem(`prompt-${id}`, 'true');
 }
+
+/**
+ * 比较两个对象的属性,找出有变化的属性,并返回包含变化属性信息的数组
+ * @param {Object} oldObject - 旧对象
+ * @param {Object} newObject - 新对象
+ * @return {Array} 包含变化属性信息的数组,每个元素是一个对象,包含 key, oldValue 和 newValue
+ */
+export function compareObjects(oldObject, newObject) {
+  const changedProperties = [];
+
+  // 比较两个对象的属性
+  for (const key in oldObject) {
+    if (oldObject.hasOwnProperty(key) && newObject.hasOwnProperty(key)) {
+      if (oldObject[key] !== newObject[key]) {
+        changedProperties.push({
+          key: key,
+          oldValue: oldObject[key],
+          newValue: newObject[key],
+        });
+      }
+    }
+  }
+
+  return changedProperties;
+}

+ 156 - 0
web/src/pages/Setting/Operation/SettingsCreditLimit.js

@@ -0,0 +1,156 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
+import {
+  compareObjects,
+  API,
+  showError,
+  showSuccess,
+  showWarning,
+} from '../../../helpers';
+
+export default function SettingsCreditLimit(props) {
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    QuotaForNewUser: '',
+    PreConsumedQuota: '',
+    QuotaForInviter: '',
+    QuotaForInvitee: '',
+  });
+  const refForm = useRef();
+  const [inputsRow, setInputsRow] = useState(inputs);
+
+  function onSubmit() {
+    const updateArray = compareObjects(inputs, inputsRow);
+    if (!updateArray.length) return showWarning('你似乎并没有修改什么');
+    const requestQueue = updateArray.map((item) => {
+      let value = '';
+      if (typeof inputs[item.key] === 'boolean') {
+        value = String(inputs[item.key]);
+      } else {
+        value = inputs[item.key];
+      }
+      return API.put('/api/option/', {
+        key: item.key,
+        value,
+      });
+    });
+    setLoading(true);
+    Promise.all(requestQueue)
+      .then((res) => {
+        if (requestQueue.length === 1) {
+          if (res.includes(undefined)) return;
+        } else if (requestQueue.length > 1) {
+          if (res.includes(undefined)) return showError('部分保存失败,请重试');
+        }
+        showSuccess('保存成功');
+        props.refresh();
+      })
+      .catch(() => {
+        showError('保存失败,请重试');
+      })
+      .finally(() => {
+        setLoading(false);
+      });
+  }
+
+  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={'额度设置'}>
+            <Row gutter={16}>
+              <Col span={6}>
+                <Form.InputNumber
+                  label={'新用户初始额度'}
+                  field={'QuotaForNewUser'}
+                  step={1}
+                  min={0}
+                  suffix={'Token'}
+                  placeholder={''}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      QuotaForNewUser: String(value),
+                    })
+                  }
+                />
+              </Col>
+              <Col span={6}>
+                <Form.InputNumber
+                  label={'请求预扣费额度'}
+                  field={'PreConsumedQuota'}
+                  step={1}
+                  min={0}
+                  suffix={'Token'}
+                  extraText={'请求结束后多退少补'}
+                  placeholder={''}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      PreConsumedQuota: String(value),
+                    })
+                  }
+                />
+              </Col>
+              <Col span={6}>
+                <Form.InputNumber
+                  label={'邀请新用户奖励额度'}
+                  field={'QuotaForInviter'}
+                  step={1}
+                  min={0}
+                  suffix={'Token'}
+                  extraText={''}
+                  placeholder={'例如:2000'}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      QuotaForInviter: String(value),
+                    })
+                  }
+                />
+              </Col>
+              <Col span={6}>
+                <Form.InputNumber
+                  label={'新用户使用邀请码奖励额度'}
+                  field={'QuotaForInvitee'}
+                  step={1}
+                  min={0}
+                  suffix={'Token'}
+                  extraText={''}
+                  placeholder={'例如:1000'}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      QuotaForInvitee: String(value),
+                    })
+                  }
+                />
+              </Col>
+            </Row>
+
+            <Row>
+              <Button size='large' onClick={onSubmit}>
+                保存额度设置
+              </Button>
+            </Row>
+          </Form.Section>
+        </Form>
+      </Spin>
+    </>
+  );
+}

+ 147 - 0
web/src/pages/Setting/Operation/SettingsDataDashboard.js

@@ -0,0 +1,147 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Row, Spin, Tag } from '@douyinfe/semi-ui';
+import {
+  compareObjects,
+  API,
+  showError,
+  showSuccess,
+  showWarning,
+} from '../../../helpers';
+
+export default function DataDashboard(props) {
+  const optionsDataExportDefaultTime = [
+    { key: 'hour', label: '小时', value: 'hour' },
+    { key: 'day', label: '天', value: 'day' },
+    { key: 'week', label: '周', value: 'week' },
+  ];
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    DataExportEnabled: false,
+    DataExportInterval: '',
+    DataExportDefaultTime: '',
+  });
+  const refForm = useRef();
+  const [inputsRow, setInputsRow] = useState(inputs);
+
+  function onSubmit() {
+    const updateArray = compareObjects(inputs, inputsRow);
+    if (!updateArray.length) return showWarning('你似乎并没有修改什么');
+    const requestQueue = updateArray.map((item) => {
+      let value = '';
+      if (typeof inputs[item.key] === 'boolean') {
+        value = String(inputs[item.key]);
+      } else {
+        value = inputs[item.key];
+      }
+      return API.put('/api/option/', {
+        key: item.key,
+        value,
+      });
+    });
+    setLoading(true);
+    Promise.all(requestQueue)
+      .then((res) => {
+        if (requestQueue.length === 1) {
+          if (res.includes(undefined)) return;
+        } else if (requestQueue.length > 1) {
+          if (res.includes(undefined)) return showError('部分保存失败,请重试');
+        }
+        showSuccess('保存成功');
+        props.refresh();
+      })
+      .catch(() => {
+        showError('保存失败,请重试');
+      })
+      .finally(() => {
+        setLoading(false);
+      });
+  }
+
+  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);
+    localStorage.setItem(
+      'data_export_default_time',
+      String(inputs.DataExportDefaultTime),
+    );
+  }, [props.options]);
+
+  return (
+    <>
+      <Spin spinning={loading}>
+        <Form
+          values={inputs}
+          getFormApi={(formAPI) => (refForm.current = formAPI)}
+          style={{ marginBottom: 15 }}
+        >
+          <Form.Section text={'数据看板设置'}>
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.Switch
+                  field={'DataExportEnabled'}
+                  label={'启用数据看板(实验性)'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) => {
+                    setInputs({
+                      ...inputs,
+                      DataExportEnabled: value,
+                    });
+                  }}
+                />
+              </Col>
+            </Row>
+            <Row>
+              <Col span={8}>
+                <Form.InputNumber
+                  label={'数据看板更新间隔 '}
+                  step={1}
+                  min={1}
+                  suffix={'分钟'}
+                  extraText={'设置过短会影响数据库性能'}
+                  placeholder={'数据看板更新间隔'}
+                  field={'DataExportInterval'}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      DataExportInterval: String(value),
+                    })
+                  }
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Select
+                  label='数据看板默认时间粒度'
+                  optionList={optionsDataExportDefaultTime}
+                  field={'DataExportDefaultTime'}
+                  extraText={'仅修改展示粒度,统计精确到小时'}
+                  placeholder={'数据看板默认时间粒度'}
+                  style={{ width: 180 }}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      DataExportDefaultTime: String(value),
+                    })
+                  }
+                />
+              </Col>
+            </Row>
+            <Row>
+              <Button size='large' onClick={onSubmit}>
+                保存数据看板设置
+              </Button>
+            </Row>
+          </Form.Section>
+        </Form>
+      </Spin>
+    </>
+  );
+}

+ 170 - 0
web/src/pages/Setting/Operation/SettingsDrawing.js

@@ -0,0 +1,170 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Row, Spin, Tag } from '@douyinfe/semi-ui';
+import {
+  compareObjects,
+  API,
+  showError,
+  showSuccess,
+  showWarning,
+} from '../../../helpers';
+
+export default function SettingsDrawing(props) {
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    DrawingEnabled: false,
+    MjNotifyEnabled: false,
+    MjAccountFilterEnabled: false,
+    MjForwardUrlEnabled: false,
+    MjModeClearEnabled: false,
+  });
+  const refForm = useRef();
+  const [inputsRow, setInputsRow] = useState(inputs);
+
+  function onSubmit() {
+    const updateArray = compareObjects(inputs, inputsRow);
+    if (!updateArray.length) return showWarning('你似乎并没有修改什么');
+    const requestQueue = updateArray.map((item) => {
+      let value = '';
+      if (typeof inputs[item.key] === 'boolean') {
+        value = String(inputs[item.key]);
+      } else {
+        value = inputs[item.key];
+      }
+      return API.put('/api/option/', {
+        key: item.key,
+        value,
+      });
+    });
+    setLoading(true);
+    Promise.all(requestQueue)
+      .then((res) => {
+        if (requestQueue.length === 1) {
+          if (res.includes(undefined)) return;
+        } else if (requestQueue.length > 1) {
+          if (res.includes(undefined)) return showError('部分保存失败,请重试');
+        }
+        showSuccess('保存成功');
+        props.refresh();
+      })
+      .catch(() => {
+        showError('保存失败,请重试');
+      })
+      .finally(() => {
+        setLoading(false);
+      });
+  }
+
+  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);
+    localStorage.setItem('mj_notify_enabled', String(inputs.MjNotifyEnabled));
+  }, [props.options]);
+  return (
+    <>
+      <Spin spinning={loading}>
+        <Form
+          values={inputs}
+          getFormApi={(formAPI) => (refForm.current = formAPI)}
+          style={{ marginBottom: 15 }}
+        >
+          <Form.Section text={'绘图设置'}>
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.Switch
+                  field={'DrawingEnabled'}
+                  label={'启用绘图功能'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) => {
+                    setInputs({
+                      ...inputs,
+                      DrawingEnabled: value,
+                    });
+                  }}
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Switch
+                  field={'MjNotifyEnabled'}
+                  label={'允许回调(会泄露服务器 IP 地址)'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      MjNotifyEnabled: value,
+                    })
+                  }
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Switch
+                  field={'MjAccountFilterEnabled'}
+                  label={'允许 AccountFilter 参数'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      MjAccountFilterEnabled: value,
+                    })
+                  }
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Switch
+                  field={'MjForwardUrlEnabled'}
+                  label={'开启之后将上游地址替换为服务器地址'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      MjForwardUrlEnabled: value,
+                    })
+                  }
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Switch
+                  field={'MjModeClearEnabled'}
+                  label={
+                    <>
+                      开启之后会清除用户提示词中的 <Tag>--fast</Tag> 、
+                      <Tag>--relax</Tag> 以及 <Tag>--turbo</Tag> 参数
+                    </>
+                  }
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      MjModeClearEnabled: value,
+                    })
+                  }
+                />
+              </Col>
+            </Row>
+            <Row>
+              <Button size='large' onClick={onSubmit}>
+                保存绘图设置
+              </Button>
+            </Row>
+          </Form.Section>
+        </Form>
+      </Spin>
+    </>
+  );
+}

+ 192 - 0
web/src/pages/Setting/Operation/SettingsGeneral.js

@@ -0,0 +1,192 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
+import {
+  compareObjects,
+  API,
+  showError,
+  showSuccess,
+  showWarning,
+} from '../../../helpers';
+
+export default function GeneralSettings(props) {
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    TopUpLink: '',
+    ChatLink: '',
+    ChatLink2: '',
+    QuotaPerUnit: '',
+    RetryTimes: '',
+    DisplayInCurrencyEnabled: false,
+    DisplayTokenStatEnabled: false,
+    DefaultCollapseSidebar: false,
+  });
+  const refForm = useRef();
+  const [inputsRow, setInputsRow] = useState(inputs);
+  function onChange(value, e) {
+    const name = e.target.id;
+    setInputs((inputs) => ({ ...inputs, [name]: value }));
+  }
+  function onSubmit() {
+    const updateArray = compareObjects(inputs, inputsRow);
+    if (!updateArray.length) return showWarning('你似乎并没有修改什么');
+    const requestQueue = updateArray.map((item) => {
+      let value = '';
+      if (typeof inputs[item.key] === 'boolean') {
+        value = String(inputs[item.key]);
+      } else {
+        value = inputs[item.key];
+      }
+      return API.put('/api/option/', {
+        key: item.key,
+        value,
+      });
+    });
+    setLoading(true);
+    Promise.all(requestQueue)
+      .then((res) => {
+        if (requestQueue.length === 1) {
+          if (res.includes(undefined)) return;
+        } else if (requestQueue.length > 1) {
+          if (res.includes(undefined)) return showError('部分保存失败,请重试');
+        }
+        showSuccess('保存成功');
+        props.refresh();
+      })
+      .catch(() => {
+        showError('保存失败,请重试');
+      })
+      .finally(() => {
+        setLoading(false);
+      });
+  }
+
+  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={'通用设置'}>
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.Input
+                  field={'TopUpLink'}
+                  label={'充值链接'}
+                  initValue={''}
+                  placeholder={'例如发卡网站的购买链接'}
+                  onChange={onChange}
+                  showClear
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Input
+                  field={'ChatLink'}
+                  label={'默认聊天页面链接'}
+                  initValue={''}
+                  placeholder='例如 ChatGPT Next Web 的部署地址'
+                  onChange={onChange}
+                  showClear
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Input
+                  field={'ChatLink2'}
+                  label={'聊天页面 2 链接'}
+                  initValue={''}
+                  placeholder='例如 ChatGPT Next Web 的部署地址'
+                  onChange={onChange}
+                  showClear
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Input
+                  field={'QuotaPerUnit'}
+                  label={'单位美元额度'}
+                  initValue={''}
+                  placeholder='一单位货币能兑换的额度'
+                  onChange={onChange}
+                  showClear
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Input
+                  field={'RetryTimes'}
+                  label={'失败重试次数'}
+                  initValue={''}
+                  placeholder='失败重试次数'
+                  onChange={onChange}
+                  showClear
+                />
+              </Col>
+            </Row>
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.Switch
+                  field={'DisplayInCurrencyEnabled'}
+                  label={'以货币形式显示额度'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) => {
+                    setInputs({
+                      ...inputs,
+                      DisplayInCurrencyEnabled: value,
+                    });
+                  }}
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Switch
+                  field={'DisplayTokenStatEnabled'}
+                  label={'Billing 相关 API 显示令牌额度而非用户额度'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      DisplayTokenStatEnabled: value,
+                    })
+                  }
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Switch
+                  field={'DefaultCollapseSidebar'}
+                  label={'默认折叠侧边栏'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      DefaultCollapseSidebar: value,
+                    })
+                  }
+                />
+              </Col>
+            </Row>
+            <Row>
+              <Button size='large' onClick={onSubmit}>
+                保存通用设置
+              </Button>
+            </Row>
+          </Form.Section>
+        </Form>
+      </Spin>
+    </>
+  );
+}

+ 147 - 0
web/src/pages/Setting/Operation/SettingsLog.js

@@ -0,0 +1,147 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Row, Spin, DatePicker } from '@douyinfe/semi-ui';
+import dayjs from 'dayjs';
+import {
+  compareObjects,
+  API,
+  showError,
+  showSuccess,
+  showWarning,
+} from '../../../helpers';
+
+export default function SettingsLog(props) {
+  const [loading, setLoading] = useState(false);
+  const [loadingCleanHistoryLog, setLoadingCleanHistoryLog] = useState(false);
+  const [inputs, setInputs] = useState({
+    LogConsumeEnabled: false,
+    historyTimestamp: dayjs().subtract(1, 'month').toDate(),
+  });
+  const refForm = useRef();
+  const [inputsRow, setInputsRow] = useState(inputs);
+
+  function onSubmit() {
+    const updateArray = compareObjects(inputs, inputsRow).filter(
+      (item) => item.key !== 'historyTimestamp',
+    );
+
+    if (!updateArray.length) return showWarning('你似乎并没有修改什么');
+    const requestQueue = updateArray.map((item) => {
+      let value = '';
+      if (typeof inputs[item.key] === 'boolean') {
+        value = String(inputs[item.key]);
+      } else {
+        value = inputs[item.key];
+      }
+      return API.put('/api/option/', {
+        key: item.key,
+        value,
+      });
+    });
+    setLoading(true);
+    Promise.all(requestQueue)
+      .then((res) => {
+        if (requestQueue.length === 1) {
+          if (res.includes(undefined)) return;
+        } else if (requestQueue.length > 1) {
+          if (res.includes(undefined)) return showError('部分保存失败,请重试');
+        }
+        showSuccess('保存成功');
+        props.refresh();
+      })
+      .catch(() => {
+        showError('保存失败,请重试');
+      })
+      .finally(() => {
+        setLoading(false);
+      });
+  }
+  async function onCleanHistoryLog() {
+    try {
+      setLoadingCleanHistoryLog(true);
+      if (!inputs.historyTimestamp) throw new Error('请选择日志记录时间');
+      const res = await API.delete(
+        `/api/log/?target_timestamp=${Date.parse(inputs.historyTimestamp) / 1000}`,
+      );
+      const { success, message, data } = res.data;
+      if (success) {
+        showSuccess(`${data} 条日志已清理!`);
+        return;
+      } else {
+        throw new Error('日志清理失败:' + message);
+      }
+    } catch (error) {
+      showError(error.message);
+    } finally {
+      setLoadingCleanHistoryLog(false);
+    }
+  }
+
+  useEffect(() => {
+    const currentInputs = {};
+    for (let key in props.options) {
+      if (Object.keys(inputs).includes(key)) {
+        currentInputs[key] = props.options[key];
+      }
+    }
+    currentInputs['historyTimestamp'] = inputs.historyTimestamp;
+    setInputs(Object.assign(inputs, 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={'日志设置'}>
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.Switch
+                  field={'LogConsumeEnabled'}
+                  label={'启用额度消费日志记录'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) => {
+                    setInputs({
+                      ...inputs,
+                      LogConsumeEnabled: value,
+                    });
+                  }}
+                />
+              </Col>
+              <Col span={8}>
+                <Spin spinning={loadingCleanHistoryLog}>
+                  <Form.DatePicker
+                    label='日志记录时间'
+                    field={'historyTimestamp'}
+                    type='dateTime'
+                    inputReadOnly={true}
+                    onChange={(value) => {
+                      setInputs({
+                        ...inputs,
+                        historyTimestamp: value,
+                      });
+                    }}
+                  />
+                  <Button size='default' onClick={onCleanHistoryLog}>
+                    清除历史日志
+                  </Button>
+                </Spin>
+              </Col>
+            </Row>
+
+            <Row>
+              <Button size='large' onClick={onSubmit}>
+                保存日志设置
+              </Button>
+            </Row>
+          </Form.Section>
+        </Form>
+      </Spin>
+    </>
+  );
+}

+ 194 - 0
web/src/pages/Setting/Operation/SettingsMagnification.js

@@ -0,0 +1,194 @@
+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';
+
+export default function SettingsMagnification(props) {
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    ModelPrice: '',
+    ModelRatio: '',
+    CompletionRatio: '',
+    GroupRatio: '',
+  });
+  const refForm = useRef();
+  const [inputsRow, setInputsRow] = useState(inputs);
+
+  async function onSubmit() {
+    try {
+      await refForm.current.validate();
+      const updateArray = compareObjects(inputs, inputsRow);
+      if (!updateArray.length) return showWarning('你似乎并没有修改什么');
+      const requestQueue = updateArray.map((item) => {
+        let value = '';
+        if (typeof inputs[item.key] === 'boolean') {
+          value = String(inputs[item.key]);
+        } else {
+          value = inputs[item.key];
+        }
+        return API.put('/api/option/', {
+          key: item.key,
+          value,
+        });
+      });
+      setLoading(true);
+      Promise.all(requestQueue)
+        .then((res) => {
+          if (requestQueue.length === 1) {
+            if (res.includes(undefined)) return;
+          } else if (requestQueue.length > 1) {
+            if (res.includes(undefined))
+              return showError('部分保存失败,请重试');
+          }
+          showSuccess('保存成功');
+          props.refresh();
+        })
+        .catch(() => {
+          showError('保存失败,请重试');
+        })
+        .finally(() => {
+          setLoading(false);
+        });
+    } catch (error) {
+      showError('请检查输入');
+      console.error(error);
+    } finally {
+    }
+  }
+
+  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={'倍率设置'}>
+            <Row gutter={16}>
+              <Col span={16}>
+                <Form.TextArea
+                  label={'模型固定价格'}
+                  extraText={'一次调用消耗多少刀,优先级大于模型倍率'}
+                  placeholder={
+                    '为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀'
+                  }
+                  field={'ModelPrice'}
+                  autosize={{ minRows: 6, maxRows: 12 }}
+                  trigger='blur'
+                  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={'模型倍率'}
+                  extraText={''}
+                  placeholder={'为一个 JSON 文本,键为模型名称,值为倍率'}
+                  field={'ModelRatio'}
+                  autosize={{ minRows: 6, maxRows: 12 }}
+                  trigger='blur'
+                  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={'模型补全倍率'}
+                  extraText={'仅对自定义模型有效'}
+                  placeholder={'为一个 JSON 文本,键为模型名称,值为倍率'}
+                  field={'CompletionRatio'}
+                  autosize={{ minRows: 6, maxRows: 12 }}
+                  trigger='blur'
+                  rules={[
+                    {
+                      validator: (rule, value) => verifyJSON(value),
+                      message: '不是合法的 JSON 字符串',
+                    },
+                  ]}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      CompletionRatio: value,
+                    })
+                  }
+                />
+              </Col>
+            </Row>
+            <Row gutter={16}>
+              <Col span={16}>
+                <Form.TextArea
+                  label={'分组倍率'}
+                  extraText={''}
+                  placeholder={'为一个 JSON 文本,键为分组名称,值为倍率'}
+                  field={'GroupRatio'}
+                  autosize={{ minRows: 6, maxRows: 12 }}
+                  trigger='blur'
+                  rules={[
+                    {
+                      validator: (rule, value) => verifyJSON(value),
+                      message: '不是合法的 JSON 字符串',
+                    },
+                  ]}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      GroupRatio: value,
+                    })
+                  }
+                />
+              </Col>
+            </Row>
+
+            <Row>
+              <Button size='large' onClick={onSubmit}>
+                保存倍率设置
+              </Button>
+            </Row>
+          </Form.Section>
+        </Form>
+      </Spin>
+    </>
+  );
+}

+ 154 - 0
web/src/pages/Setting/Operation/SettingsMonitoring.js

@@ -0,0 +1,154 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
+import {
+  compareObjects,
+  API,
+  showError,
+  showSuccess,
+  showWarning,
+} from '../../../helpers';
+
+export default function SettingsMonitoring(props) {
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    ChannelDisableThreshold: '',
+    QuotaRemindThreshold: '',
+    AutomaticDisableChannelEnabled: false,
+    AutomaticEnableChannelEnabled: false,
+  });
+  const refForm = useRef();
+  const [inputsRow, setInputsRow] = useState(inputs);
+
+  function onSubmit() {
+    const updateArray = compareObjects(inputs, inputsRow);
+    if (!updateArray.length) return showWarning('你似乎并没有修改什么');
+    const requestQueue = updateArray.map((item) => {
+      let value = '';
+      if (typeof inputs[item.key] === 'boolean') {
+        value = String(inputs[item.key]);
+      } else {
+        value = inputs[item.key];
+      }
+      return API.put('/api/option/', {
+        key: item.key,
+        value,
+      });
+    });
+    setLoading(true);
+    Promise.all(requestQueue)
+      .then((res) => {
+        if (requestQueue.length === 1) {
+          if (res.includes(undefined)) return;
+        } else if (requestQueue.length > 1) {
+          if (res.includes(undefined)) return showError('部分保存失败,请重试');
+        }
+        showSuccess('保存成功');
+        props.refresh();
+      })
+      .catch(() => {
+        showError('保存失败,请重试');
+      })
+      .finally(() => {
+        setLoading(false);
+      });
+  }
+
+  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={'监控设置'}>
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.InputNumber
+                  label={'最长响应时间'}
+                  step={1}
+                  min={0}
+                  suffix={'秒'}
+                  extraText={'当运行通道全部测试时,超过此时间将自动禁用通道'}
+                  placeholder={''}
+                  field={'ChannelDisableThreshold'}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      ChannelDisableThreshold: String(value),
+                    })
+                  }
+                />
+              </Col>
+              <Col span={8}>
+                <Form.InputNumber
+                  label={'额度提醒阈值'}
+                  step={1}
+                  min={0}
+                  suffix={'Token'}
+                  extraText={'低于此额度时将发送邮件提醒用户'}
+                  placeholder={''}
+                  field={'QuotaRemindThreshold'}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      QuotaRemindThreshold: String(value),
+                    })
+                  }
+                />
+              </Col>
+            </Row>
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.Switch
+                  field={'AutomaticDisableChannelEnabled'}
+                  label={'失败时自动禁用通道'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) => {
+                    setInputs({
+                      ...inputs,
+                      AutomaticDisableChannelEnabled: value,
+                    });
+                  }}
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Switch
+                  field={'AutomaticEnableChannelEnabled'}
+                  label={'成功时自动启用通道'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      AutomaticEnableChannelEnabled: value,
+                    })
+                  }
+                />
+              </Col>
+            </Row>
+            <Row>
+              <Button size='large' onClick={onSubmit}>
+                保存监控设置
+              </Button>
+            </Row>
+          </Form.Section>
+        </Form>
+      </Spin>
+    </>
+  );
+}

+ 135 - 0
web/src/pages/Setting/Operation/SettingsSensitiveWords.js

@@ -0,0 +1,135 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Row, Spin, Tag } from '@douyinfe/semi-ui';
+import {
+  compareObjects,
+  API,
+  showError,
+  showSuccess,
+  showWarning,
+} from '../../../helpers';
+
+export default function SettingsSensitiveWords(props) {
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    CheckSensitiveEnabled: false,
+    CheckSensitiveOnPromptEnabled: false,
+    SensitiveWords: '',
+  });
+  const refForm = useRef();
+  const [inputsRow, setInputsRow] = useState(inputs);
+
+  function onSubmit() {
+    const updateArray = compareObjects(inputs, inputsRow);
+    if (!updateArray.length) return showWarning('你似乎并没有修改什么');
+    const requestQueue = updateArray.map((item) => {
+      let value = '';
+      if (typeof inputs[item.key] === 'boolean') {
+        value = String(inputs[item.key]);
+      } else {
+        value = inputs[item.key];
+      }
+      return API.put('/api/option/', {
+        key: item.key,
+        value,
+      });
+    });
+    setLoading(true);
+    Promise.all(requestQueue)
+      .then((res) => {
+        if (requestQueue.length === 1) {
+          if (res.includes(undefined)) return;
+        } else if (requestQueue.length > 1) {
+          if (res.includes(undefined)) return showError('部分保存失败,请重试');
+        }
+        showSuccess('保存成功');
+        props.refresh();
+      })
+      .catch(() => {
+        showError('保存失败,请重试');
+      })
+      .finally(() => {
+        setLoading(false);
+      });
+  }
+
+  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={'屏蔽词过滤设置'}>
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.Switch
+                  field={'CheckSensitiveEnabled'}
+                  label={'启用屏蔽词过滤功能'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) => {
+                    setInputs({
+                      ...inputs,
+                      CheckSensitiveEnabled: value,
+                    });
+                  }}
+                />
+              </Col>
+              <Col span={8}>
+                <Form.Switch
+                  field={'CheckSensitiveOnPromptEnabled'}
+                  label={'启用 Prompt 检查'}
+                  size='large'
+                  checkedText='|'
+                  uncheckedText='〇'
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      CheckSensitiveOnPromptEnabled: value,
+                    })
+                  }
+                />
+              </Col>
+            </Row>
+            <Row>
+              <Col span={16}>
+                <Form.TextArea
+                  label={'屏蔽词列表'}
+                  extraText={'一行一个屏蔽词,不需要符号分割'}
+                  placeholder={'一行一个屏蔽词,不需要符号分割'}
+                  field={'SensitiveWords'}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      SensitiveWords: value,
+                    })
+                  }
+                  style={{ fontFamily: 'JetBrains Mono, Consolas' }}
+                  autosize={{ minRows: 6, maxRows: 12 }}
+                />
+              </Col>
+            </Row>
+            <Row>
+              <Button size='large' onClick={onSubmit}>
+                保存屏蔽词过滤设置
+              </Button>
+            </Row>
+          </Form.Section>
+        </Form>
+      </Spin>
+    </>
+  );
+}