NotificationSettings.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useRef, useEffect } from 'react';
  16. import {
  17. Button,
  18. Typography,
  19. Card,
  20. Avatar,
  21. Form,
  22. Radio,
  23. Toast,
  24. Tabs,
  25. TabPane
  26. } from '@douyinfe/semi-ui';
  27. import {
  28. IconMail,
  29. IconKey,
  30. IconBell,
  31. IconLink
  32. } from '@douyinfe/semi-icons';
  33. import { ShieldCheck, Bell, DollarSign } from 'lucide-react';
  34. import { renderQuotaWithPrompt } from '../../../../helpers';
  35. import CodeViewer from '../../../playground/CodeViewer';
  36. const NotificationSettings = ({
  37. t,
  38. notificationSettings,
  39. handleNotificationSettingChange,
  40. saveNotificationSettings
  41. }) => {
  42. const formApiRef = useRef(null);
  43. // 初始化表单值
  44. useEffect(() => {
  45. if (formApiRef.current && notificationSettings) {
  46. formApiRef.current.setValues(notificationSettings);
  47. }
  48. }, [notificationSettings]);
  49. // 处理表单字段变化
  50. const handleFormChange = (field, value) => {
  51. handleNotificationSettingChange(field, value);
  52. };
  53. // 表单提交
  54. const handleSubmit = () => {
  55. if (formApiRef.current) {
  56. formApiRef.current.validate()
  57. .then(() => {
  58. saveNotificationSettings();
  59. })
  60. .catch((errors) => {
  61. console.log('表单验证失败:', errors);
  62. Toast.error(t('请检查表单填写是否正确'));
  63. });
  64. } else {
  65. saveNotificationSettings();
  66. }
  67. };
  68. return (
  69. <Card
  70. className="!rounded-2xl shadow-sm border-0"
  71. footer={
  72. <div className="flex justify-end">
  73. <Button
  74. type='primary'
  75. onClick={handleSubmit}
  76. >
  77. {t('保存设置')}
  78. </Button>
  79. </div>
  80. }
  81. >
  82. {/* 卡片头部 */}
  83. <div className="flex items-center mb-4">
  84. <Avatar size="small" color="blue" className="mr-3 shadow-md">
  85. <Bell size={16} />
  86. </Avatar>
  87. <div>
  88. <Typography.Text className="text-lg font-medium">{t('其他设置')}</Typography.Text>
  89. <div className="text-xs text-gray-600">{t('通知、价格和隐私相关设置')}</div>
  90. </div>
  91. </div>
  92. <Form
  93. getFormApi={(api) => (formApiRef.current = api)}
  94. initValues={notificationSettings}
  95. onSubmit={handleSubmit}
  96. >
  97. {() => (
  98. <Tabs type="card" defaultActiveKey="notification">
  99. {/* 通知配置 Tab */}
  100. <TabPane
  101. tab={
  102. <div className="flex items-center">
  103. <Bell size={16} className="mr-2" />
  104. {t('通知配置')}
  105. </div>
  106. }
  107. itemKey="notification"
  108. >
  109. <div className="py-4">
  110. <Form.RadioGroup
  111. field='warningType'
  112. label={t('通知方式')}
  113. initValue={notificationSettings.warningType}
  114. onChange={(value) => handleFormChange('warningType', value)}
  115. rules={[{ required: true, message: t('请选择通知方式') }]}
  116. >
  117. <Radio value="email">{t('邮件通知')}</Radio>
  118. <Radio value="webhook">{t('Webhook通知')}</Radio>
  119. </Form.RadioGroup>
  120. <Form.AutoComplete
  121. field='warningThreshold'
  122. label={
  123. <span>
  124. {t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}
  125. </span>
  126. }
  127. placeholder={t('请输入预警额度')}
  128. data={[
  129. { value: 100000, label: '0.2$' },
  130. { value: 500000, label: '1$' },
  131. { value: 1000000, label: '5$' },
  132. { value: 5000000, label: '10$' },
  133. ]}
  134. onChange={(val) => handleFormChange('warningThreshold', val)}
  135. prefix={<IconBell />}
  136. extraText={t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
  137. style={{ width: '100%', maxWidth: '300px' }}
  138. rules={[
  139. { required: true, message: t('请输入预警阈值') },
  140. {
  141. validator: (rule, value) => {
  142. const numValue = Number(value);
  143. if (isNaN(numValue) || numValue <= 0) {
  144. return Promise.reject(t('预警阈值必须为正数'));
  145. }
  146. return Promise.resolve();
  147. }
  148. }
  149. ]}
  150. />
  151. {/* 邮件通知设置 */}
  152. {notificationSettings.warningType === 'email' && (
  153. <Form.Input
  154. field='notificationEmail'
  155. label={t('通知邮箱')}
  156. placeholder={t('留空则使用账号绑定的邮箱')}
  157. onChange={(val) => handleFormChange('notificationEmail', val)}
  158. prefix={<IconMail />}
  159. extraText={t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
  160. showClear
  161. />
  162. )}
  163. {/* Webhook通知设置 */}
  164. {notificationSettings.warningType === 'webhook' && (
  165. <>
  166. <Form.Input
  167. field='webhookUrl'
  168. label={t('Webhook地址')}
  169. placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
  170. onChange={(val) => handleFormChange('webhookUrl', val)}
  171. prefix={<IconLink />}
  172. extraText={t('只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求')}
  173. showClear
  174. rules={[
  175. {
  176. required: notificationSettings.warningType === 'webhook',
  177. message: t('请输入Webhook地址')
  178. },
  179. {
  180. pattern: /^https:\/\/.+/,
  181. message: t('Webhook地址必须以https://开头')
  182. }
  183. ]}
  184. />
  185. <Form.Input
  186. field='webhookSecret'
  187. label={t('接口凭证')}
  188. placeholder={t('请输入密钥')}
  189. onChange={(val) => handleFormChange('webhookSecret', val)}
  190. prefix={<IconKey />}
  191. extraText={t('密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性')}
  192. showClear
  193. />
  194. <Form.Slot label={t('Webhook请求结构说明')}>
  195. <div>
  196. <div style={{ height: '200px', marginBottom: '12px' }}>
  197. <CodeViewer
  198. content={{
  199. "type": "quota_exceed",
  200. "title": "额度预警通知",
  201. "content": "您的额度即将用尽,当前剩余额度为 {{value}}",
  202. "values": ["$0.99"],
  203. "timestamp": 1739950503
  204. }}
  205. title="webhook"
  206. language="json"
  207. />
  208. </div>
  209. <div className="text-xs text-gray-500 leading-relaxed">
  210. <div><strong>type:</strong> {t('通知类型 (quota_exceed: 额度预警)')} </div>
  211. <div><strong>title:</strong> {t('通知标题')}</div>
  212. <div><strong>content:</strong> {t('通知内容,支持 {{value}} 变量占位符')}</div>
  213. <div><strong>values:</strong> {t('按顺序替换content中的变量占位符')}</div>
  214. <div><strong>timestamp:</strong> {t('Unix时间戳')}</div>
  215. </div>
  216. </div>
  217. </Form.Slot>
  218. </>
  219. )}
  220. </div>
  221. </TabPane>
  222. {/* 价格设置 Tab */}
  223. <TabPane
  224. tab={
  225. <div className="flex items-center">
  226. <DollarSign size={16} className="mr-2" />
  227. {t('价格设置')}
  228. </div>
  229. }
  230. itemKey="pricing"
  231. >
  232. <div className="py-4">
  233. <Form.Switch
  234. field='acceptUnsetModelRatioModel'
  235. label={t('接受未设置价格模型')}
  236. checkedText={t('开')}
  237. uncheckedText={t('关')}
  238. onChange={(value) => handleFormChange('acceptUnsetModelRatioModel', value)}
  239. extraText={t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
  240. />
  241. </div>
  242. </TabPane>
  243. {/* 隐私设置 Tab */}
  244. <TabPane
  245. tab={
  246. <div className="flex items-center">
  247. <ShieldCheck size={16} className="mr-2" />
  248. {t('隐私设置')}
  249. </div>
  250. }
  251. itemKey="privacy"
  252. >
  253. <div className="py-4">
  254. <Form.Switch
  255. field='recordIpLog'
  256. label={t('记录请求与错误日志IP')}
  257. checkedText={t('开')}
  258. uncheckedText={t('关')}
  259. onChange={(value) => handleFormChange('recordIpLog', value)}
  260. extraText={t('开启后,仅"消费"和"错误"日志将记录您的客户端IP地址')}
  261. />
  262. </div>
  263. </TabPane>
  264. </Tabs>
  265. )}
  266. </Form>
  267. </Card>
  268. );
  269. };
  270. export default NotificationSettings;