OperationSetting.js 16 KB

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