OperationSetting.js 20 KB

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