SystemSetting.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. import React, { useEffect, useState } from 'react';
  2. import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react';
  3. import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
  4. const SystemSetting = () => {
  5. let [inputs, setInputs] = useState({
  6. PasswordLoginEnabled: '',
  7. PasswordRegisterEnabled: '',
  8. EmailVerificationEnabled: '',
  9. GitHubOAuthEnabled: '',
  10. GitHubClientId: '',
  11. GitHubClientSecret: '',
  12. Notice: '',
  13. SMTPServer: '',
  14. SMTPPort: '',
  15. SMTPAccount: '',
  16. SMTPFrom: '',
  17. SMTPToken: '',
  18. ServerAddress: '',
  19. Footer: '',
  20. WeChatAuthEnabled: '',
  21. WeChatServerAddress: '',
  22. WeChatServerToken: '',
  23. WeChatAccountQRCodeImageURL: '',
  24. TurnstileCheckEnabled: '',
  25. TurnstileSiteKey: '',
  26. TurnstileSecretKey: '',
  27. RegisterEnabled: '',
  28. QuotaForNewUser: 0,
  29. QuotaRemindThreshold: 0,
  30. PreConsumedQuota: 0,
  31. ModelRatio: '',
  32. TopUpLink: '',
  33. AutomaticDisableChannelEnabled: '',
  34. ChannelDisableThreshold: 0,
  35. });
  36. const [originInputs, setOriginInputs] = useState({});
  37. let [loading, setLoading] = useState(false);
  38. const getOptions = async () => {
  39. const res = await API.get('/api/option/');
  40. const { success, message, data } = res.data;
  41. if (success) {
  42. let newInputs = {};
  43. data.forEach((item) => {
  44. newInputs[item.key] = item.value;
  45. });
  46. setInputs(newInputs);
  47. setOriginInputs(newInputs);
  48. } else {
  49. showError(message);
  50. }
  51. };
  52. useEffect(() => {
  53. getOptions().then();
  54. }, []);
  55. const updateOption = async (key, value) => {
  56. setLoading(true);
  57. switch (key) {
  58. case 'PasswordLoginEnabled':
  59. case 'PasswordRegisterEnabled':
  60. case 'EmailVerificationEnabled':
  61. case 'GitHubOAuthEnabled':
  62. case 'WeChatAuthEnabled':
  63. case 'TurnstileCheckEnabled':
  64. case 'RegisterEnabled':
  65. case 'AutomaticDisableChannelEnabled':
  66. value = inputs[key] === 'true' ? 'false' : 'true';
  67. break;
  68. default:
  69. break;
  70. }
  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 (
  85. name === 'Notice' ||
  86. name.startsWith('SMTP') ||
  87. name === 'ServerAddress' ||
  88. name === 'GitHubClientId' ||
  89. name === 'GitHubClientSecret' ||
  90. name === 'WeChatServerAddress' ||
  91. name === 'WeChatServerToken' ||
  92. name === 'WeChatAccountQRCodeImageURL' ||
  93. name === 'TurnstileSiteKey' ||
  94. name === 'TurnstileSecretKey' ||
  95. name === 'QuotaForNewUser' ||
  96. name === 'QuotaRemindThreshold' ||
  97. name === 'PreConsumedQuota' ||
  98. name === 'ModelRatio' ||
  99. name === 'TopUpLink'
  100. ) {
  101. setInputs((inputs) => ({ ...inputs, [name]: value }));
  102. } else {
  103. await updateOption(name, value);
  104. }
  105. };
  106. const submitServerAddress = async () => {
  107. let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
  108. await updateOption('ServerAddress', ServerAddress);
  109. };
  110. const submitOperationConfig = async () => {
  111. if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
  112. await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
  113. }
  114. if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
  115. await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
  116. }
  117. if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
  118. await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
  119. }
  120. if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
  121. if (!verifyJSON(inputs.ModelRatio)) {
  122. showError('模型倍率不是合法的 JSON 字符串');
  123. return;
  124. }
  125. await updateOption('ModelRatio', inputs.ModelRatio);
  126. }
  127. if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
  128. await updateOption('TopUpLink', inputs.TopUpLink);
  129. }
  130. };
  131. const submitSMTP = async () => {
  132. if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
  133. await updateOption('SMTPServer', inputs.SMTPServer);
  134. }
  135. if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
  136. await updateOption('SMTPAccount', inputs.SMTPAccount);
  137. }
  138. if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
  139. await updateOption('SMTPFrom', inputs.SMTPFrom);
  140. }
  141. if (
  142. originInputs['SMTPPort'] !== inputs.SMTPPort &&
  143. inputs.SMTPPort !== ''
  144. ) {
  145. await updateOption('SMTPPort', inputs.SMTPPort);
  146. }
  147. if (
  148. originInputs['SMTPToken'] !== inputs.SMTPToken &&
  149. inputs.SMTPToken !== ''
  150. ) {
  151. await updateOption('SMTPToken', inputs.SMTPToken);
  152. }
  153. };
  154. const submitWeChat = async () => {
  155. if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
  156. await updateOption(
  157. 'WeChatServerAddress',
  158. removeTrailingSlash(inputs.WeChatServerAddress)
  159. );
  160. }
  161. if (
  162. originInputs['WeChatAccountQRCodeImageURL'] !==
  163. inputs.WeChatAccountQRCodeImageURL
  164. ) {
  165. await updateOption(
  166. 'WeChatAccountQRCodeImageURL',
  167. inputs.WeChatAccountQRCodeImageURL
  168. );
  169. }
  170. if (
  171. originInputs['WeChatServerToken'] !== inputs.WeChatServerToken &&
  172. inputs.WeChatServerToken !== ''
  173. ) {
  174. await updateOption('WeChatServerToken', inputs.WeChatServerToken);
  175. }
  176. };
  177. const submitGitHubOAuth = async () => {
  178. if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
  179. await updateOption('GitHubClientId', inputs.GitHubClientId);
  180. }
  181. if (
  182. originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret &&
  183. inputs.GitHubClientSecret !== ''
  184. ) {
  185. await updateOption('GitHubClientSecret', inputs.GitHubClientSecret);
  186. }
  187. };
  188. const submitTurnstile = async () => {
  189. if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
  190. await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
  191. }
  192. if (
  193. originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey &&
  194. inputs.TurnstileSecretKey !== ''
  195. ) {
  196. await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey);
  197. }
  198. };
  199. return (
  200. <Grid columns={1}>
  201. <Grid.Column>
  202. <Form loading={loading}>
  203. <Header as='h3'>通用设置</Header>
  204. <Form.Group widths='equal'>
  205. <Form.Input
  206. label='服务器地址'
  207. placeholder='例如:https://yourdomain.com'
  208. value={inputs.ServerAddress}
  209. name='ServerAddress'
  210. onChange={handleInputChange}
  211. />
  212. </Form.Group>
  213. <Form.Button onClick={submitServerAddress}>
  214. 更新服务器地址
  215. </Form.Button>
  216. <Divider />
  217. <Header as='h3'>配置登录注册</Header>
  218. <Form.Group inline>
  219. <Form.Checkbox
  220. checked={inputs.PasswordLoginEnabled === 'true'}
  221. label='允许通过密码进行登录'
  222. name='PasswordLoginEnabled'
  223. onChange={handleInputChange}
  224. />
  225. <Form.Checkbox
  226. checked={inputs.PasswordRegisterEnabled === 'true'}
  227. label='允许通过密码进行注册'
  228. name='PasswordRegisterEnabled'
  229. onChange={handleInputChange}
  230. />
  231. <Form.Checkbox
  232. checked={inputs.EmailVerificationEnabled === 'true'}
  233. label='通过密码注册时需要进行邮箱验证'
  234. name='EmailVerificationEnabled'
  235. onChange={handleInputChange}
  236. />
  237. <Form.Checkbox
  238. checked={inputs.GitHubOAuthEnabled === 'true'}
  239. label='允许通过 GitHub 账户登录 & 注册'
  240. name='GitHubOAuthEnabled'
  241. onChange={handleInputChange}
  242. />
  243. <Form.Checkbox
  244. checked={inputs.WeChatAuthEnabled === 'true'}
  245. label='允许通过微信登录 & 注册'
  246. name='WeChatAuthEnabled'
  247. onChange={handleInputChange}
  248. />
  249. </Form.Group>
  250. <Form.Group inline>
  251. <Form.Checkbox
  252. checked={inputs.RegisterEnabled === 'true'}
  253. label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
  254. name='RegisterEnabled'
  255. onChange={handleInputChange}
  256. />
  257. <Form.Checkbox
  258. checked={inputs.TurnstileCheckEnabled === 'true'}
  259. label='启用 Turnstile 用户校验'
  260. name='TurnstileCheckEnabled'
  261. onChange={handleInputChange}
  262. />
  263. </Form.Group>
  264. <Divider />
  265. <Header as='h3'>
  266. 运营设置
  267. </Header>
  268. <Form.Group widths={4}>
  269. <Form.Input
  270. label='新用户初始配额'
  271. name='QuotaForNewUser'
  272. onChange={handleInputChange}
  273. autoComplete='new-password'
  274. value={inputs.QuotaForNewUser}
  275. type='number'
  276. min='0'
  277. placeholder='例如:100'
  278. />
  279. <Form.Input
  280. label='充值链接'
  281. name='TopUpLink'
  282. onChange={handleInputChange}
  283. autoComplete='new-password'
  284. value={inputs.TopUpLink}
  285. type='link'
  286. placeholder='例如发卡网站的购买链接'
  287. />
  288. <Form.Input
  289. label='额度提醒阈值'
  290. name='QuotaRemindThreshold'
  291. onChange={handleInputChange}
  292. autoComplete='new-password'
  293. value={inputs.QuotaRemindThreshold}
  294. type='number'
  295. min='0'
  296. placeholder='低于此额度时将发送邮件提醒用户'
  297. />
  298. <Form.Input
  299. label='请求预扣费额度'
  300. name='PreConsumedQuota'
  301. onChange={handleInputChange}
  302. autoComplete='new-password'
  303. value={inputs.PreConsumedQuota}
  304. type='number'
  305. min='0'
  306. placeholder='请求结束后多退少补'
  307. />
  308. </Form.Group>
  309. <Form.Group widths='equal'>
  310. <Form.TextArea
  311. label='模型倍率'
  312. name='ModelRatio'
  313. onChange={handleInputChange}
  314. style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
  315. autoComplete='new-password'
  316. value={inputs.ModelRatio}
  317. placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
  318. />
  319. </Form.Group>
  320. <Form.Button onClick={submitOperationConfig}>保存运营设置</Form.Button>
  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.Group>
  337. <Form.Group inline>
  338. <Form.Checkbox
  339. checked={inputs.AutomaticDisableChannelEnabled === 'true'}
  340. label='失败时自动禁用通道'
  341. name='AutomaticDisableChannelEnabled'
  342. onChange={handleInputChange}
  343. />
  344. </Form.Group>
  345. <Divider />
  346. <Header as='h3'>
  347. 配置 SMTP
  348. <Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
  349. </Header>
  350. <Form.Group widths={3}>
  351. <Form.Input
  352. label='SMTP 服务器地址'
  353. name='SMTPServer'
  354. onChange={handleInputChange}
  355. autoComplete='new-password'
  356. value={inputs.SMTPServer}
  357. placeholder='例如:smtp.qq.com'
  358. />
  359. <Form.Input
  360. label='SMTP 端口'
  361. name='SMTPPort'
  362. onChange={handleInputChange}
  363. autoComplete='new-password'
  364. value={inputs.SMTPPort}
  365. placeholder='默认: 587'
  366. />
  367. <Form.Input
  368. label='SMTP 账户'
  369. name='SMTPAccount'
  370. onChange={handleInputChange}
  371. autoComplete='new-password'
  372. value={inputs.SMTPAccount}
  373. placeholder='通常是邮箱地址'
  374. />
  375. </Form.Group>
  376. <Form.Group widths={3}>
  377. <Form.Input
  378. label='SMTP 发送者邮箱'
  379. name='SMTPFrom'
  380. onChange={handleInputChange}
  381. autoComplete='new-password'
  382. value={inputs.SMTPFrom}
  383. placeholder='通常和邮箱地址保持一致'
  384. />
  385. <Form.Input
  386. label='SMTP 访问凭证'
  387. name='SMTPToken'
  388. onChange={handleInputChange}
  389. type='password'
  390. autoComplete='new-password'
  391. value={inputs.SMTPToken}
  392. placeholder='敏感信息不会发送到前端显示'
  393. />
  394. </Form.Group>
  395. <Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
  396. <Divider />
  397. <Header as='h3'>
  398. 配置 GitHub OAuth App
  399. <Header.Subheader>
  400. 用以支持通过 GitHub 进行登录注册,
  401. <a href='https://github.com/settings/developers' target='_blank'>
  402. 点击此处
  403. </a>
  404. 管理你的 GitHub OAuth App
  405. </Header.Subheader>
  406. </Header>
  407. <Message>
  408. Homepage URL 填 <code>{inputs.ServerAddress}</code>
  409. ,Authorization callback URL 填{' '}
  410. <code>{`${inputs.ServerAddress}/oauth/github`}</code>
  411. </Message>
  412. <Form.Group widths={3}>
  413. <Form.Input
  414. label='GitHub Client ID'
  415. name='GitHubClientId'
  416. onChange={handleInputChange}
  417. autoComplete='new-password'
  418. value={inputs.GitHubClientId}
  419. placeholder='输入你注册的 GitHub OAuth APP 的 ID'
  420. />
  421. <Form.Input
  422. label='GitHub Client Secret'
  423. name='GitHubClientSecret'
  424. onChange={handleInputChange}
  425. type='password'
  426. autoComplete='new-password'
  427. value={inputs.GitHubClientSecret}
  428. placeholder='敏感信息不会发送到前端显示'
  429. />
  430. </Form.Group>
  431. <Form.Button onClick={submitGitHubOAuth}>
  432. 保存 GitHub OAuth 设置
  433. </Form.Button>
  434. <Divider />
  435. <Header as='h3'>
  436. 配置 WeChat Server
  437. <Header.Subheader>
  438. 用以支持通过微信进行登录注册,
  439. <a
  440. href='https://github.com/songquanpeng/wechat-server'
  441. target='_blank'
  442. >
  443. 点击此处
  444. </a>
  445. 了解 WeChat Server
  446. </Header.Subheader>
  447. </Header>
  448. <Form.Group widths={3}>
  449. <Form.Input
  450. label='WeChat Server 服务器地址'
  451. name='WeChatServerAddress'
  452. placeholder='例如:https://yourdomain.com'
  453. onChange={handleInputChange}
  454. autoComplete='new-password'
  455. value={inputs.WeChatServerAddress}
  456. />
  457. <Form.Input
  458. label='WeChat Server 访问凭证'
  459. name='WeChatServerToken'
  460. type='password'
  461. onChange={handleInputChange}
  462. autoComplete='new-password'
  463. value={inputs.WeChatServerToken}
  464. placeholder='敏感信息不会发送到前端显示'
  465. />
  466. <Form.Input
  467. label='微信公众号二维码图片链接'
  468. name='WeChatAccountQRCodeImageURL'
  469. onChange={handleInputChange}
  470. autoComplete='new-password'
  471. value={inputs.WeChatAccountQRCodeImageURL}
  472. placeholder='输入一个图片链接'
  473. />
  474. </Form.Group>
  475. <Form.Button onClick={submitWeChat}>
  476. 保存 WeChat Server 设置
  477. </Form.Button>
  478. <Divider />
  479. <Header as='h3'>
  480. 配置 Turnstile
  481. <Header.Subheader>
  482. 用以支持用户校验,
  483. <a href='https://dash.cloudflare.com/' target='_blank'>
  484. 点击此处
  485. </a>
  486. 管理你的 Turnstile Sites,推荐选择 Invisible Widget Type
  487. </Header.Subheader>
  488. </Header>
  489. <Form.Group widths={3}>
  490. <Form.Input
  491. label='Turnstile Site Key'
  492. name='TurnstileSiteKey'
  493. onChange={handleInputChange}
  494. autoComplete='new-password'
  495. value={inputs.TurnstileSiteKey}
  496. placeholder='输入你注册的 Turnstile Site Key'
  497. />
  498. <Form.Input
  499. label='Turnstile Secret Key'
  500. name='TurnstileSecretKey'
  501. onChange={handleInputChange}
  502. type='password'
  503. autoComplete='new-password'
  504. value={inputs.TurnstileSecretKey}
  505. placeholder='敏感信息不会发送到前端显示'
  506. />
  507. </Form.Group>
  508. <Form.Button onClick={submitTurnstile}>
  509. 保存 Turnstile 设置
  510. </Form.Button>
  511. </Form>
  512. </Grid.Column>
  513. </Grid>
  514. );
  515. };
  516. export default SystemSetting;