OperationSetting.js 21 KB

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