SettingsMonitoring.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useEffect, useState, useRef } from 'react';
  16. import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
  17. import {
  18. compareObjects,
  19. API,
  20. showError,
  21. showSuccess,
  22. showWarning,
  23. parseHttpStatusCodeRules,
  24. } from '../../../helpers';
  25. import { useTranslation } from 'react-i18next';
  26. import HttpStatusCodeRulesInput from '../../../components/settings/HttpStatusCodeRulesInput';
  27. export default function SettingsMonitoring(props) {
  28. const { t } = useTranslation();
  29. const [loading, setLoading] = useState(false);
  30. const [inputs, setInputs] = useState({
  31. ChannelDisableThreshold: '',
  32. QuotaRemindThreshold: '',
  33. AutomaticDisableChannelEnabled: false,
  34. AutomaticEnableChannelEnabled: false,
  35. AutomaticDisableKeywords: '',
  36. AutomaticDisableStatusCodes: '401',
  37. AutomaticRetryStatusCodes:
  38. '100-199,300-399,401-407,409-499,500-503,505-523,525-599',
  39. 'monitor_setting.auto_test_channel_enabled': false,
  40. 'monitor_setting.auto_test_channel_minutes': 10,
  41. });
  42. const refForm = useRef();
  43. const [inputsRow, setInputsRow] = useState(inputs);
  44. const parsedAutoDisableStatusCodes = parseHttpStatusCodeRules(
  45. inputs.AutomaticDisableStatusCodes || '',
  46. );
  47. const parsedAutoRetryStatusCodes = parseHttpStatusCodeRules(
  48. inputs.AutomaticRetryStatusCodes || '',
  49. );
  50. function onSubmit() {
  51. const updateArray = compareObjects(inputs, inputsRow);
  52. if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
  53. if (!parsedAutoDisableStatusCodes.ok) {
  54. const details =
  55. parsedAutoDisableStatusCodes.invalidTokens &&
  56. parsedAutoDisableStatusCodes.invalidTokens.length > 0
  57. ? `: ${parsedAutoDisableStatusCodes.invalidTokens.join(', ')}`
  58. : '';
  59. return showError(`${t('自动禁用状态码格式不正确')}${details}`);
  60. }
  61. if (!parsedAutoRetryStatusCodes.ok) {
  62. const details =
  63. parsedAutoRetryStatusCodes.invalidTokens &&
  64. parsedAutoRetryStatusCodes.invalidTokens.length > 0
  65. ? `: ${parsedAutoRetryStatusCodes.invalidTokens.join(', ')}`
  66. : '';
  67. return showError(`${t('自动重试状态码格式不正确')}${details}`);
  68. }
  69. const requestQueue = updateArray.map((item) => {
  70. let value = '';
  71. if (typeof inputs[item.key] === 'boolean') {
  72. value = String(inputs[item.key]);
  73. } else {
  74. const normalizedMap = {
  75. AutomaticDisableStatusCodes: parsedAutoDisableStatusCodes.normalized,
  76. AutomaticRetryStatusCodes: parsedAutoRetryStatusCodes.normalized,
  77. };
  78. value = normalizedMap[item.key] ?? inputs[item.key];
  79. }
  80. return API.put('/api/option/', {
  81. key: item.key,
  82. value,
  83. });
  84. });
  85. setLoading(true);
  86. Promise.all(requestQueue)
  87. .then((res) => {
  88. if (requestQueue.length === 1) {
  89. if (res.includes(undefined)) return;
  90. } else if (requestQueue.length > 1) {
  91. if (res.includes(undefined))
  92. return showError(t('部分保存失败,请重试'));
  93. }
  94. showSuccess(t('保存成功'));
  95. props.refresh();
  96. })
  97. .catch(() => {
  98. showError(t('保存失败,请重试'));
  99. })
  100. .finally(() => {
  101. setLoading(false);
  102. });
  103. }
  104. useEffect(() => {
  105. const currentInputs = {};
  106. for (let key in props.options) {
  107. if (Object.keys(inputs).includes(key)) {
  108. currentInputs[key] = props.options[key];
  109. }
  110. }
  111. setInputs(currentInputs);
  112. setInputsRow(structuredClone(currentInputs));
  113. refForm.current.setValues(currentInputs);
  114. }, [props.options]);
  115. return (
  116. <>
  117. <Spin spinning={loading}>
  118. <Form
  119. values={inputs}
  120. getFormApi={(formAPI) => (refForm.current = formAPI)}
  121. style={{ marginBottom: 15 }}
  122. >
  123. <Form.Section text={t('监控设置')}>
  124. <Row gutter={16}>
  125. <Col xs={24} sm={12} md={8} lg={8} xl={8}>
  126. <Form.Switch
  127. field={'monitor_setting.auto_test_channel_enabled'}
  128. label={t('定时测试所有通道')}
  129. size='default'
  130. checkedText='|'
  131. uncheckedText='〇'
  132. onChange={(value) =>
  133. setInputs({
  134. ...inputs,
  135. 'monitor_setting.auto_test_channel_enabled': value,
  136. })
  137. }
  138. />
  139. </Col>
  140. <Col xs={24} sm={12} md={8} lg={8} xl={8}>
  141. <Form.InputNumber
  142. label={t('自动测试所有通道间隔时间')}
  143. step={1}
  144. min={1}
  145. suffix={t('分钟')}
  146. extraText={t('每隔多少分钟测试一次所有通道')}
  147. placeholder={''}
  148. field={'monitor_setting.auto_test_channel_minutes'}
  149. onChange={(value) =>
  150. setInputs({
  151. ...inputs,
  152. 'monitor_setting.auto_test_channel_minutes':
  153. parseInt(value),
  154. })
  155. }
  156. />
  157. </Col>
  158. </Row>
  159. <Row gutter={16}>
  160. <Col xs={24} sm={12} md={8} lg={8} xl={8}>
  161. <Form.InputNumber
  162. label={t('测试所有渠道的最长响应时间')}
  163. step={1}
  164. min={0}
  165. suffix={t('秒')}
  166. extraText={t(
  167. '当运行通道全部测试时,超过此时间将自动禁用通道',
  168. )}
  169. placeholder={''}
  170. field={'ChannelDisableThreshold'}
  171. onChange={(value) =>
  172. setInputs({
  173. ...inputs,
  174. ChannelDisableThreshold: String(value),
  175. })
  176. }
  177. />
  178. </Col>
  179. <Col xs={24} sm={12} md={8} lg={8} xl={8}>
  180. <Form.InputNumber
  181. label={t('额度提醒阈值')}
  182. step={1}
  183. min={0}
  184. suffix={'Token'}
  185. extraText={t('低于此额度时将发送邮件提醒用户')}
  186. placeholder={''}
  187. field={'QuotaRemindThreshold'}
  188. onChange={(value) =>
  189. setInputs({
  190. ...inputs,
  191. QuotaRemindThreshold: String(value),
  192. })
  193. }
  194. />
  195. </Col>
  196. </Row>
  197. <Row gutter={16}>
  198. <Col xs={24} sm={12} md={8} lg={8} xl={8}>
  199. <Form.Switch
  200. field={'AutomaticDisableChannelEnabled'}
  201. label={t('失败时自动禁用通道')}
  202. size='default'
  203. checkedText='|'
  204. uncheckedText='〇'
  205. onChange={(value) => {
  206. setInputs({
  207. ...inputs,
  208. AutomaticDisableChannelEnabled: value,
  209. });
  210. }}
  211. />
  212. </Col>
  213. <Col xs={24} sm={12} md={8} lg={8} xl={8}>
  214. <Form.Switch
  215. field={'AutomaticEnableChannelEnabled'}
  216. label={t('成功时自动启用通道')}
  217. size='default'
  218. checkedText='|'
  219. uncheckedText='〇'
  220. onChange={(value) =>
  221. setInputs({
  222. ...inputs,
  223. AutomaticEnableChannelEnabled: value,
  224. })
  225. }
  226. />
  227. </Col>
  228. </Row>
  229. <Row gutter={16}>
  230. <Col xs={24} sm={16}>
  231. <HttpStatusCodeRulesInput
  232. label={t('自动禁用状态码')}
  233. placeholder={t('例如:401, 403, 429, 500-599')}
  234. extraText={t(
  235. '支持填写单个状态码或范围(含首尾),使用逗号分隔',
  236. )}
  237. field={'AutomaticDisableStatusCodes'}
  238. onChange={(value) =>
  239. setInputs({ ...inputs, AutomaticDisableStatusCodes: value })
  240. }
  241. parsed={parsedAutoDisableStatusCodes}
  242. invalidText={t('自动禁用状态码格式不正确')}
  243. />
  244. <HttpStatusCodeRulesInput
  245. label={t('自动重试状态码')}
  246. placeholder={t('例如:401, 403, 429, 500-599')}
  247. extraText={t(
  248. '支持填写单个状态码或范围(含首尾),使用逗号分隔;504 和 524 始终不重试,不受此处配置影响',
  249. )}
  250. field={'AutomaticRetryStatusCodes'}
  251. onChange={(value) =>
  252. setInputs({ ...inputs, AutomaticRetryStatusCodes: value })
  253. }
  254. parsed={parsedAutoRetryStatusCodes}
  255. invalidText={t('自动重试状态码格式不正确')}
  256. />
  257. <Form.TextArea
  258. label={t('自动禁用关键词')}
  259. placeholder={t('一行一个,不区分大小写')}
  260. extraText={t(
  261. '当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道',
  262. )}
  263. field={'AutomaticDisableKeywords'}
  264. autosize={{ minRows: 6, maxRows: 12 }}
  265. onChange={(value) =>
  266. setInputs({ ...inputs, AutomaticDisableKeywords: value })
  267. }
  268. />
  269. </Col>
  270. </Row>
  271. <Row>
  272. <Button size='default' onClick={onSubmit}>
  273. {t('保存监控设置')}
  274. </Button>
  275. </Row>
  276. </Form.Section>
  277. </Form>
  278. </Spin>
  279. </>
  280. );
  281. }