OperationSetting.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import React, { useEffect, useState } from 'react';
  2. import { Divider, Form, Grid, Header } from 'semantic-ui-react';
  3. import { Card } from '@douyinfe/semi-ui';
  4. import GeneralSettings from '../pages/Setting/Operation/GeneralSettings.js';
  5. import DrawingSettings from '../pages/Setting/Operation/DrawingSettings.js';
  6. import {
  7. API,
  8. showError,
  9. showSuccess,
  10. timestamp2string,
  11. verifyJSON,
  12. } from '../helpers';
  13. import { useTheme } from '../context/Theme';
  14. const OperationSetting = () => {
  15. let now = new Date();
  16. let [inputs, setInputs] = useState({
  17. QuotaForNewUser: 0,
  18. QuotaForInviter: 0,
  19. QuotaForInvitee: 0,
  20. QuotaRemindThreshold: 0,
  21. PreConsumedQuota: 0,
  22. StreamCacheQueueLength: 0,
  23. ModelRatio: '',
  24. ModelPrice: '',
  25. GroupRatio: '',
  26. TopUpLink: '',
  27. ChatLink: '',
  28. ChatLink2: '', // 添加的新状态变量
  29. QuotaPerUnit: 0,
  30. AutomaticDisableChannelEnabled: '',
  31. AutomaticEnableChannelEnabled: '',
  32. ChannelDisableThreshold: 0,
  33. LogConsumeEnabled: '',
  34. DisplayInCurrencyEnabled: false,
  35. DisplayTokenStatEnabled: false,
  36. CheckSensitiveEnabled: '',
  37. CheckSensitiveOnPromptEnabled: '',
  38. CheckSensitiveOnCompletionEnabled: '',
  39. StopOnSensitiveEnabled: '',
  40. SensitiveWords: '',
  41. MjNotifyEnabled: '',
  42. MjAccountFilterEnabled: '',
  43. MjModeClearEnabled: '',
  44. MjForwardUrlEnabled: '',
  45. DrawingEnabled: '',
  46. DataExportEnabled: '',
  47. DataExportDefaultTime: 'hour',
  48. DataExportInterval: 5,
  49. DefaultCollapseSidebar: false, // 默认折叠侧边栏
  50. RetryTimes: 0,
  51. });
  52. const [originInputs, setOriginInputs] = useState({});
  53. let [loading, setLoading] = useState(false);
  54. let [historyTimestamp, setHistoryTimestamp] = useState(
  55. timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600),
  56. ); // a month ago
  57. // 精确时间选项(小时,天,周)
  58. const timeOptions = [
  59. { key: 'hour', text: '小时', value: 'hour' },
  60. { key: 'day', text: '天', value: 'day' },
  61. { key: 'week', text: '周', value: 'week' },
  62. ];
  63. const getOptions = async () => {
  64. const res = await API.get('/api/option/');
  65. const { success, message, data } = res.data;
  66. if (success) {
  67. let newInputs = {};
  68. data.forEach((item) => {
  69. if (
  70. item.key === 'ModelRatio' ||
  71. item.key === 'GroupRatio' ||
  72. item.key === 'ModelPrice'
  73. ) {
  74. item.value = JSON.stringify(JSON.parse(item.value), null, 2);
  75. }
  76. if (
  77. item.key.endsWith('Enabled') ||
  78. ['DefaultCollapseSidebar'].includes(item.key)
  79. ) {
  80. newInputs[item.key] = item.value === 'true' ? true : false;
  81. } else {
  82. newInputs[item.key] = item.value;
  83. }
  84. });
  85. setInputs(newInputs);
  86. setOriginInputs(newInputs);
  87. } else {
  88. showError(message);
  89. }
  90. };
  91. const theme = useTheme();
  92. const isDark = theme === 'dark';
  93. useEffect(() => {
  94. getOptions().then();
  95. }, []);
  96. const updateOption = async (key, value) => {
  97. setLoading(true);
  98. if (key.endsWith('Enabled')) {
  99. value = inputs[key] === 'true' ? 'false' : 'true';
  100. }
  101. if (key === 'DefaultCollapseSidebar') {
  102. value = inputs[key] === 'true' ? 'false' : 'true';
  103. }
  104. console.log(key, value);
  105. const res = await API.put('/api/option/', {
  106. key,
  107. value,
  108. });
  109. const { success, message } = res.data;
  110. if (success) {
  111. setInputs((inputs) => ({ ...inputs, [key]: value }));
  112. } else {
  113. showError(message);
  114. }
  115. setLoading(false);
  116. };
  117. const handleInputChange = async (e, { name, value }) => {
  118. if (
  119. name.endsWith('Enabled') ||
  120. name === 'DataExportInterval' ||
  121. name === 'DataExportDefaultTime' ||
  122. name === 'DefaultCollapseSidebar'
  123. ) {
  124. if (name === 'DataExportDefaultTime') {
  125. localStorage.setItem('data_export_default_time', value);
  126. } else if (name === 'MjNotifyEnabled') {
  127. localStorage.setItem('mj_notify_enabled', value);
  128. }
  129. await updateOption(name, value);
  130. } else {
  131. setInputs((inputs) => ({ ...inputs, [name]: value }));
  132. }
  133. };
  134. const submitConfig = async (group) => {
  135. switch (group) {
  136. case 'monitor':
  137. if (
  138. originInputs['ChannelDisableThreshold'] !==
  139. inputs.ChannelDisableThreshold
  140. ) {
  141. await updateOption(
  142. 'ChannelDisableThreshold',
  143. inputs.ChannelDisableThreshold,
  144. );
  145. }
  146. if (
  147. originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold
  148. ) {
  149. await updateOption(
  150. 'QuotaRemindThreshold',
  151. inputs.QuotaRemindThreshold,
  152. );
  153. }
  154. break;
  155. case 'ratio':
  156. if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
  157. if (!verifyJSON(inputs.ModelRatio)) {
  158. showError('模型倍率不是合法的 JSON 字符串');
  159. return;
  160. }
  161. await updateOption('ModelRatio', inputs.ModelRatio);
  162. }
  163. if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
  164. if (!verifyJSON(inputs.GroupRatio)) {
  165. showError('分组倍率不是合法的 JSON 字符串');
  166. return;
  167. }
  168. await updateOption('GroupRatio', inputs.GroupRatio);
  169. }
  170. if (originInputs['ModelPrice'] !== inputs.ModelPrice) {
  171. if (!verifyJSON(inputs.ModelPrice)) {
  172. showError('模型固定价格不是合法的 JSON 字符串');
  173. return;
  174. }
  175. await updateOption('ModelPrice', inputs.ModelPrice);
  176. }
  177. break;
  178. case 'words':
  179. if (originInputs['SensitiveWords'] !== inputs.SensitiveWords) {
  180. await updateOption('SensitiveWords', inputs.SensitiveWords);
  181. }
  182. break;
  183. case 'quota':
  184. if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
  185. await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
  186. }
  187. if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
  188. await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
  189. }
  190. if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
  191. await updateOption('QuotaForInviter', inputs.QuotaForInviter);
  192. }
  193. if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
  194. await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
  195. }
  196. break;
  197. case 'general':
  198. if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
  199. await updateOption('TopUpLink', inputs.TopUpLink);
  200. }
  201. if (originInputs['ChatLink'] !== inputs.ChatLink) {
  202. await updateOption('ChatLink', inputs.ChatLink);
  203. }
  204. if (originInputs['ChatLink2'] !== inputs.ChatLink2) {
  205. await updateOption('ChatLink2', inputs.ChatLink2);
  206. }
  207. if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
  208. await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
  209. }
  210. if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
  211. await updateOption('RetryTimes', inputs.RetryTimes);
  212. }
  213. break;
  214. }
  215. };
  216. const deleteHistoryLogs = async () => {
  217. console.log(inputs);
  218. const res = await API.delete(
  219. `/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`,
  220. );
  221. const { success, message, data } = res.data;
  222. if (success) {
  223. showSuccess(`${data} 条日志已清理!`);
  224. return;
  225. }
  226. showError('日志清理失败:' + message);
  227. };
  228. return (
  229. <>
  230. {/* 通用设置 */}
  231. <Card>
  232. <GeneralSettings options={inputs} />
  233. </Card>
  234. {/* 绘图设置 */}
  235. <Card style={{ marginTop: '10px' }}>
  236. <DrawingSettings options={inputs} />
  237. </Card>
  238. <Grid columns={1}>
  239. <Grid.Column>
  240. <Form loading={loading} inverted={isDark}>
  241. <Header as='h3' inverted={isDark}>
  242. 屏蔽词过滤设置
  243. </Header>
  244. <Form.Group inline>
  245. <Form.Checkbox
  246. checked={inputs.CheckSensitiveEnabled === 'true'}
  247. label='启用屏蔽词过滤功能'
  248. name='CheckSensitiveEnabled'
  249. onChange={handleInputChange}
  250. />
  251. </Form.Group>
  252. <Form.Group inline>
  253. <Form.Checkbox
  254. checked={inputs.CheckSensitiveOnPromptEnabled === 'true'}
  255. label='启用prompt检查'
  256. name='CheckSensitiveOnPromptEnabled'
  257. onChange={handleInputChange}
  258. />
  259. {/*<Form.Checkbox*/}
  260. {/* checked={inputs.CheckSensitiveOnCompletionEnabled === 'true'}*/}
  261. {/* label='启用生成内容检查'*/}
  262. {/* name='CheckSensitiveOnCompletionEnabled'*/}
  263. {/* onChange={handleInputChange}*/}
  264. {/*/>*/}
  265. </Form.Group>
  266. {/*<Form.Group inline>*/}
  267. {/* <Form.Checkbox*/}
  268. {/* checked={inputs.StopOnSensitiveEnabled === 'true'}*/}
  269. {/* label='在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词'*/}
  270. {/* name='StopOnSensitiveEnabled'*/}
  271. {/* onChange={handleInputChange}*/}
  272. {/* />*/}
  273. {/*</Form.Group>*/}
  274. {/*<Form.Group>*/}
  275. {/* <Form.Input*/}
  276. {/* label="流模式下缓存队列,默认不缓存,设置越大检测越准确,但是回复会有卡顿感"*/}
  277. {/* name="StreamCacheTextLength"*/}
  278. {/* onChange={handleInputChange}*/}
  279. {/* value={inputs.StreamCacheQueueLength}*/}
  280. {/* type="number"*/}
  281. {/* min="0"*/}
  282. {/* placeholder="例如:10"*/}
  283. {/* />*/}
  284. {/*</Form.Group>*/}
  285. <Form.Group widths='equal'>
  286. <Form.TextArea
  287. label='屏蔽词列表,一行一个屏蔽词,不需要符号分割'
  288. name='SensitiveWords'
  289. onChange={handleInputChange}
  290. style={{
  291. minHeight: 250,
  292. fontFamily: 'JetBrains Mono, Consolas',
  293. }}
  294. value={inputs.SensitiveWords}
  295. placeholder='一行一个屏蔽词'
  296. />
  297. </Form.Group>
  298. <Form.Button
  299. onClick={() => {
  300. submitConfig('words').then();
  301. }}
  302. >
  303. 保存屏蔽词设置
  304. </Form.Button>
  305. <Divider />
  306. <Header as='h3' inverted={isDark}>
  307. 日志设置
  308. </Header>
  309. <Form.Group inline>
  310. <Form.Checkbox
  311. checked={inputs.LogConsumeEnabled === 'true'}
  312. label='启用额度消费日志记录'
  313. name='LogConsumeEnabled'
  314. onChange={handleInputChange}
  315. />
  316. </Form.Group>
  317. <Form.Group widths={4}>
  318. <Form.Input
  319. label='目标时间'
  320. value={historyTimestamp}
  321. type='datetime-local'
  322. name='history_timestamp'
  323. onChange={(e, { name, value }) => {
  324. setHistoryTimestamp(value);
  325. }}
  326. />
  327. </Form.Group>
  328. <Form.Button
  329. onClick={() => {
  330. deleteHistoryLogs().then();
  331. }}
  332. >
  333. 清理历史日志
  334. </Form.Button>
  335. <Divider />
  336. <Header as='h3' inverted={isDark}>
  337. 数据看板
  338. </Header>
  339. <Form.Checkbox
  340. checked={inputs.DataExportEnabled === 'true'}
  341. label='启用数据看板(实验性)'
  342. name='DataExportEnabled'
  343. onChange={handleInputChange}
  344. />
  345. <Form.Group>
  346. <Form.Input
  347. label='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
  348. name='DataExportInterval'
  349. type={'number'}
  350. step='1'
  351. min='1'
  352. onChange={handleInputChange}
  353. autoComplete='new-password'
  354. value={inputs.DataExportInterval}
  355. placeholder='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
  356. />
  357. <Form.Select
  358. label='数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)'
  359. options={timeOptions}
  360. name='DataExportDefaultTime'
  361. onChange={handleInputChange}
  362. autoComplete='new-password'
  363. value={inputs.DataExportDefaultTime}
  364. placeholder='数据看板默认时间粒度'
  365. />
  366. </Form.Group>
  367. <Divider />
  368. <Header as='h3' inverted={isDark}>
  369. 监控设置
  370. </Header>
  371. <Form.Group widths={3}>
  372. <Form.Input
  373. label='最长响应时间'
  374. name='ChannelDisableThreshold'
  375. onChange={handleInputChange}
  376. autoComplete='new-password'
  377. value={inputs.ChannelDisableThreshold}
  378. type='number'
  379. min='0'
  380. placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
  381. />
  382. <Form.Input
  383. label='额度提醒阈值'
  384. name='QuotaRemindThreshold'
  385. onChange={handleInputChange}
  386. autoComplete='new-password'
  387. value={inputs.QuotaRemindThreshold}
  388. type='number'
  389. min='0'
  390. placeholder='低于此额度时将发送邮件提醒用户'
  391. />
  392. </Form.Group>
  393. <Form.Group inline>
  394. <Form.Checkbox
  395. checked={inputs.AutomaticDisableChannelEnabled === 'true'}
  396. label='失败时自动禁用通道'
  397. name='AutomaticDisableChannelEnabled'
  398. onChange={handleInputChange}
  399. />
  400. <Form.Checkbox
  401. checked={inputs.AutomaticEnableChannelEnabled === 'true'}
  402. label='成功时自动启用通道'
  403. name='AutomaticEnableChannelEnabled'
  404. onChange={handleInputChange}
  405. />
  406. </Form.Group>
  407. <Form.Button
  408. onClick={() => {
  409. submitConfig('monitor').then();
  410. }}
  411. >
  412. 保存监控设置
  413. </Form.Button>
  414. <Divider />
  415. <Header as='h3' inverted={isDark}>
  416. 额度设置
  417. </Header>
  418. <Form.Group widths={4}>
  419. <Form.Input
  420. label='新用户初始额度'
  421. name='QuotaForNewUser'
  422. onChange={handleInputChange}
  423. autoComplete='new-password'
  424. value={inputs.QuotaForNewUser}
  425. type='number'
  426. min='0'
  427. placeholder='例如:100'
  428. />
  429. <Form.Input
  430. label='请求预扣费额度'
  431. name='PreConsumedQuota'
  432. onChange={handleInputChange}
  433. autoComplete='new-password'
  434. value={inputs.PreConsumedQuota}
  435. type='number'
  436. min='0'
  437. placeholder='请求结束后多退少补'
  438. />
  439. <Form.Input
  440. label='邀请新用户奖励额度'
  441. name='QuotaForInviter'
  442. onChange={handleInputChange}
  443. autoComplete='new-password'
  444. value={inputs.QuotaForInviter}
  445. type='number'
  446. min='0'
  447. placeholder='例如:2000'
  448. />
  449. <Form.Input
  450. label='新用户使用邀请码奖励额度'
  451. name='QuotaForInvitee'
  452. onChange={handleInputChange}
  453. autoComplete='new-password'
  454. value={inputs.QuotaForInvitee}
  455. type='number'
  456. min='0'
  457. placeholder='例如:1000'
  458. />
  459. </Form.Group>
  460. <Form.Button
  461. onClick={() => {
  462. submitConfig('quota').then();
  463. }}
  464. >
  465. 保存额度设置
  466. </Form.Button>
  467. <Divider />
  468. <Header as='h3' inverted={isDark}>
  469. 倍率设置
  470. </Header>
  471. <Form.Group widths='equal'>
  472. <Form.TextArea
  473. label='模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)'
  474. name='ModelPrice'
  475. onChange={handleInputChange}
  476. style={{
  477. minHeight: 250,
  478. fontFamily: 'JetBrains Mono, Consolas',
  479. }}
  480. autoComplete='new-password'
  481. value={inputs.ModelPrice}
  482. placeholder='为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀'
  483. />
  484. </Form.Group>
  485. <Form.Group widths='equal'>
  486. <Form.TextArea
  487. label='模型倍率'
  488. name='ModelRatio'
  489. onChange={handleInputChange}
  490. style={{
  491. minHeight: 250,
  492. fontFamily: 'JetBrains Mono, Consolas',
  493. }}
  494. autoComplete='new-password'
  495. value={inputs.ModelRatio}
  496. placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
  497. />
  498. </Form.Group>
  499. <Form.Group widths='equal'>
  500. <Form.TextArea
  501. label='分组倍率'
  502. name='GroupRatio'
  503. onChange={handleInputChange}
  504. style={{
  505. minHeight: 250,
  506. fontFamily: 'JetBrains Mono, Consolas',
  507. }}
  508. autoComplete='new-password'
  509. value={inputs.GroupRatio}
  510. placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
  511. />
  512. </Form.Group>
  513. <Form.Button
  514. onClick={() => {
  515. submitConfig('ratio').then();
  516. }}
  517. >
  518. 保存倍率设置
  519. </Form.Button>
  520. </Form>
  521. </Grid.Column>
  522. </Grid>
  523. </>
  524. );
  525. };
  526. export default OperationSetting;