OperationSetting.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  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();let [inputs, setInputs] = useState({
  6. QuotaForNewUser: 0,
  7. QuotaForInviter: 0,
  8. QuotaForInvitee: 0,
  9. QuotaRemindThreshold: 0,
  10. PreConsumedQuota: 0,
  11. ModelRatio: '',
  12. GroupRatio: '',
  13. TopUpLink: '',
  14. ChatLink: '',
  15. QuotaPerUnit: 0,
  16. AutomaticDisableChannelEnabled: '',
  17. ChannelDisableThreshold: 0,
  18. LogConsumeEnabled: '',
  19. DisplayInCurrencyEnabled: '',
  20. DisplayTokenStatEnabled: '',
  21. RetryTimes: 0
  22. });
  23. const [originInputs, setOriginInputs] = useState({});
  24. let [loading, setLoading] = useState(false);let [historyTimestamp, setHistoryTimestamp] = useState(timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600)); // a month ago
  25. const getOptions = async () => {
  26. const res = await API.get('/api/option/');
  27. const { success, message, data } = res.data;
  28. if (success) {
  29. let newInputs = {};
  30. data.forEach((item) => {
  31. if (item.key === 'ModelRatio' || item.key === 'GroupRatio') {
  32. item.value = JSON.stringify(JSON.parse(item.value), null, 2);
  33. }
  34. newInputs[item.key] = item.value;
  35. });
  36. setInputs(newInputs);
  37. setOriginInputs(newInputs);
  38. } else {
  39. showError(message);
  40. }
  41. };
  42. useEffect(() => {
  43. getOptions().then();
  44. }, []);
  45. const updateOption = async (key, value) => {
  46. setLoading(true);
  47. if (key.endsWith('Enabled')) {
  48. value = inputs[key] === 'true' ? 'false' : 'true';
  49. }
  50. const res = await API.put('/api/option/', {
  51. key,
  52. value
  53. });
  54. const {success, message} = res.data;
  55. if (success) {
  56. setInputs((inputs) => ({...inputs, [key]: value}));
  57. } else {
  58. showError(message);
  59. }
  60. setLoading(false);
  61. };
  62. const handleInputChange = async (e, {name, value}) => {
  63. if (name.endsWith('Enabled')) {
  64. await updateOption(name, value);
  65. } else {
  66. setInputs((inputs) => ({...inputs, [name]: value}));
  67. }
  68. };
  69. const submitConfig = async (group) => {
  70. switch (group) {
  71. case 'monitor':
  72. if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) {
  73. await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold);
  74. }
  75. if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
  76. await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
  77. }
  78. break;
  79. case 'ratio':
  80. if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
  81. if (!verifyJSON(inputs.ModelRatio)) {
  82. showError('模型倍率不是合法的 JSON 字符串');
  83. return;
  84. }
  85. await updateOption('ModelRatio', inputs.ModelRatio);
  86. }
  87. if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
  88. if (!verifyJSON(inputs.GroupRatio)) {
  89. showError('分组倍率不是合法的 JSON 字符串');
  90. return;
  91. }
  92. await updateOption('GroupRatio', inputs.GroupRatio);
  93. }
  94. break;
  95. case 'quota':
  96. if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
  97. await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
  98. }
  99. if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
  100. await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
  101. }
  102. if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
  103. await updateOption('QuotaForInviter', inputs.QuotaForInviter);
  104. }
  105. if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
  106. await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
  107. }
  108. break;
  109. case 'general':
  110. if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
  111. await updateOption('TopUpLink', inputs.TopUpLink);
  112. }
  113. if (originInputs['ChatLink'] !== inputs.ChatLink) {
  114. await updateOption('ChatLink', inputs.ChatLink);
  115. }
  116. if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
  117. await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
  118. }
  119. if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
  120. await updateOption('RetryTimes', inputs.RetryTimes);
  121. }
  122. break;
  123. }
  124. };
  125. const deleteHistoryLogs = async () => {
  126. console.log(inputs);
  127. const res = await API.delete(`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`);
  128. const { success, message, data } = res.data;
  129. if (success) {
  130. showSuccess(`${data} 条日志已清理!`);
  131. return;
  132. }
  133. showError('日志清理失败:' + message);
  134. };return (
  135. <Grid columns={1}>
  136. <Grid.Column>
  137. <Form loading={loading}>
  138. <Header as='h3'>
  139. 通用设置
  140. </Header>
  141. <Form.Group widths={4}>
  142. <Form.Input
  143. label='充值链接'
  144. name='TopUpLink'
  145. onChange={handleInputChange}
  146. autoComplete='new-password'
  147. value={inputs.TopUpLink}
  148. type='link'
  149. placeholder='例如发卡网站的购买链接'
  150. />
  151. <Form.Input
  152. label='聊天页面链接'
  153. name='ChatLink'
  154. onChange={handleInputChange}
  155. autoComplete='new-password'
  156. value={inputs.ChatLink}
  157. type='link'
  158. placeholder='例如 ChatGPT Next Web 的部署地址'
  159. />
  160. <Form.Input
  161. label='单位美元额度'
  162. name='QuotaPerUnit'
  163. onChange={handleInputChange}
  164. autoComplete='new-password'
  165. value={inputs.QuotaPerUnit}
  166. type='number'
  167. step='0.01'
  168. placeholder='一单位货币能兑换的额度'
  169. />
  170. <Form.Input
  171. label='失败重试次数'
  172. name='RetryTimes'
  173. type={'number'}
  174. step='1'
  175. min='0'
  176. onChange={handleInputChange}
  177. autoComplete='new-password'
  178. value={inputs.RetryTimes}
  179. placeholder='失败重试次数'
  180. />
  181. </Form.Group>
  182. <Form.Group inline>
  183. <Form.Checkbox
  184. checked={inputs.DisplayInCurrencyEnabled === 'true'}
  185. label='以货币形式显示额度'
  186. name='DisplayInCurrencyEnabled'
  187. onChange={handleInputChange}
  188. />
  189. <Form.Checkbox
  190. checked={inputs.DisplayTokenStatEnabled === 'true'}
  191. label='Billing 相关 API 显示令牌额度而非用户额度'
  192. name='DisplayTokenStatEnabled'
  193. onChange={handleInputChange}
  194. />
  195. </Form.Group>
  196. <Form.Button onClick={() => {
  197. submitConfig('general').then();
  198. }}>保存通用设置</Form.Button><Divider />
  199. <Header as='h3'>
  200. 日志设置
  201. </Header>
  202. <Form.Group inline>
  203. <Form.Checkbox
  204. checked={inputs.LogConsumeEnabled === 'true'}
  205. label='启用额度消费日志记录'
  206. name='LogConsumeEnabled'
  207. onChange={handleInputChange}
  208. />
  209. </Form.Group>
  210. <Form.Group widths={4}>
  211. <Form.Input label='目标时间' value={historyTimestamp} type='datetime-local'
  212. name='history_timestamp'
  213. onChange={(e, { name, value }) => {
  214. setHistoryTimestamp(value);
  215. }} />
  216. </Form.Group>
  217. <Form.Button onClick={() => {
  218. deleteHistoryLogs().then();
  219. }}>清理历史日志</Form.Button>
  220. <Divider/>
  221. <Header as='h3'>
  222. 监控设置
  223. </Header>
  224. <Form.Group widths={3}>
  225. <Form.Input
  226. label='最长响应时间'
  227. name='ChannelDisableThreshold'
  228. onChange={handleInputChange}
  229. autoComplete='new-password'
  230. value={inputs.ChannelDisableThreshold}
  231. type='number'
  232. min='0'
  233. placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
  234. />
  235. <Form.Input
  236. label='额度提醒阈值'
  237. name='QuotaRemindThreshold'
  238. onChange={handleInputChange}
  239. autoComplete='new-password'
  240. value={inputs.QuotaRemindThreshold}
  241. type='number'
  242. min='0'
  243. placeholder='低于此额度时将发送邮件提醒用户'
  244. />
  245. </Form.Group>
  246. <Form.Group inline>
  247. <Form.Checkbox
  248. checked={inputs.AutomaticDisableChannelEnabled === 'true'}
  249. label='失败时自动禁用通道'
  250. name='AutomaticDisableChannelEnabled'
  251. onChange={handleInputChange}
  252. />
  253. </Form.Group>
  254. <Form.Button onClick={() => {
  255. submitConfig('monitor').then();
  256. }}>保存监控设置</Form.Button>
  257. <Divider/>
  258. <Header as='h3'>
  259. 额度设置
  260. </Header>
  261. <Form.Group widths={4}>
  262. <Form.Input
  263. label='新用户初始额度'
  264. name='QuotaForNewUser'
  265. onChange={handleInputChange}
  266. autoComplete='new-password'
  267. value={inputs.QuotaForNewUser}
  268. type='number'
  269. min='0'
  270. placeholder='例如:100'
  271. />
  272. <Form.Input
  273. label='请求预扣费额度'
  274. name='PreConsumedQuota'
  275. onChange={handleInputChange}
  276. autoComplete='new-password'
  277. value={inputs.PreConsumedQuota}
  278. type='number'
  279. min='0'
  280. placeholder='请求结束后多退少补'
  281. />
  282. <Form.Input
  283. label='邀请新用户奖励额度'
  284. name='QuotaForInviter'
  285. onChange={handleInputChange}
  286. autoComplete='new-password'
  287. value={inputs.QuotaForInviter}
  288. type='number'
  289. min='0'
  290. placeholder='例如:2000'
  291. />
  292. <Form.Input
  293. label='新用户使用邀请码奖励额度'
  294. name='QuotaForInvitee'
  295. onChange={handleInputChange}
  296. autoComplete='new-password'
  297. value={inputs.QuotaForInvitee}
  298. type='number'
  299. min='0'
  300. placeholder='例如:1000'
  301. />
  302. </Form.Group>
  303. <Form.Button onClick={() => {
  304. submitConfig('quota').then();
  305. }}>保存额度设置</Form.Button>
  306. <Divider/>
  307. <Header as='h3'>
  308. 倍率设置
  309. </Header>
  310. <Form.Group widths='equal'>
  311. <Form.TextArea
  312. label='模型倍率'
  313. name='ModelRatio'
  314. onChange={handleInputChange}
  315. style={{minHeight: 250, fontFamily: 'JetBrains Mono, Consolas'}}
  316. autoComplete='new-password'
  317. value={inputs.ModelRatio}
  318. placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
  319. />
  320. </Form.Group>
  321. <Form.Group widths='equal'>
  322. <Form.TextArea
  323. label='分组倍率'
  324. name='GroupRatio'
  325. onChange={handleInputChange}
  326. style={{minHeight: 250, fontFamily: 'JetBrains Mono, Consolas'}}
  327. autoComplete='new-password'
  328. value={inputs.GroupRatio}
  329. placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
  330. />
  331. </Form.Group>
  332. <Form.Button onClick={() => {
  333. submitConfig('ratio').then();
  334. }}>保存倍率设置</Form.Button>
  335. </Form>
  336. </Grid.Column>
  337. </Grid>
  338. )
  339. ;
  340. };
  341. export default OperationSetting;