OperationSetting.js 17 KB

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