OperationSetting.js 20 KB

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