OperationSetting.js 20 KB

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