OperationSetting.js 19 KB

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