diff --git a/controller/user.go b/controller/user.go index 0b9fccf2a..982329cec 100644 --- a/controller/user.go +++ b/controller/user.go @@ -1097,6 +1097,7 @@ type UpdateUserSettingRequest struct { WebhookUrl string `json:"webhook_url,omitempty"` WebhookSecret string `json:"webhook_secret,omitempty"` NotificationEmail string `json:"notification_email,omitempty"` + BarkUrl string `json:"bark_url,omitempty"` AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"` RecordIpLog bool `json:"record_ip_log"` } @@ -1112,7 +1113,7 @@ func UpdateUserSetting(c *gin.Context) { } // 验证预警类型 - if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook { + if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook && req.QuotaWarningType != dto.NotifyTypeBark { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "无效的预警类型", @@ -1160,6 +1161,33 @@ func UpdateUserSetting(c *gin.Context) { } } + // 如果是Bark类型,验证Bark URL + if req.QuotaWarningType == dto.NotifyTypeBark { + if req.BarkUrl == "" { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "Bark推送URL不能为空", + }) + return + } + // 验证URL格式 + if _, err := url.ParseRequestURI(req.BarkUrl); err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "无效的Bark推送URL", + }) + return + } + // 检查是否是HTTP或HTTPS + if !strings.HasPrefix(req.BarkUrl, "https://") && !strings.HasPrefix(req.BarkUrl, "http://") { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "Bark推送URL必须以http://或https://开头", + }) + return + } + } + userId := c.GetInt("id") user, err := model.GetUserById(userId, true) if err != nil { @@ -1188,6 +1216,11 @@ func UpdateUserSetting(c *gin.Context) { settings.NotificationEmail = req.NotificationEmail } + // 如果是Bark类型,添加Bark URL到设置中 + if req.QuotaWarningType == dto.NotifyTypeBark { + settings.BarkUrl = req.BarkUrl + } + // 更新用户设置 user.SetSetting(settings) if err := user.Update(false); err != nil { diff --git a/dto/user_settings.go b/dto/user_settings.go index 56beb7118..89dd926ef 100644 --- a/dto/user_settings.go +++ b/dto/user_settings.go @@ -6,6 +6,7 @@ type UserSetting struct { WebhookUrl string `json:"webhook_url,omitempty"` // WebhookUrl webhook地址 WebhookSecret string `json:"webhook_secret,omitempty"` // WebhookSecret webhook密钥 NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址 + BarkUrl string `json:"bark_url,omitempty"` // BarkUrl Bark推送URL AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型 RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置 @@ -14,4 +15,5 @@ type UserSetting struct { var ( NotifyTypeEmail = "email" // Email 邮件 NotifyTypeWebhook = "webhook" // Webhook + NotifyTypeBark = "bark" // Bark 推送 ) diff --git a/service/quota.go b/service/quota.go index 8f65bd20e..e078a1ad1 100644 --- a/service/quota.go +++ b/service/quota.go @@ -535,8 +535,27 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon if quotaTooLow { prompt := "您的额度即将用尽" topUpLink := fmt.Sprintf("%s/topup", setting.ServerAddress) - content := "{{value}},当前剩余额度为 {{value}},为了不影响您的使用,请及时充值。
充值链接:{{value}}" - err := NotifyUser(relayInfo.UserId, relayInfo.UserEmail, relayInfo.UserSetting, dto.NewNotify(dto.NotifyTypeQuotaExceed, prompt, content, []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota), topUpLink, topUpLink})) + + // 根据通知方式生成不同的内容格式 + var content string + var values []interface{} + + notifyType := userSetting.NotifyType + if notifyType == "" { + notifyType = dto.NotifyTypeEmail + } + + if notifyType == dto.NotifyTypeBark { + // Bark推送使用简短文本,不支持HTML + content = "{{value}},剩余额度:{{value}},请及时充值" + values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)} + } else { + // 默认内容格式,适用于Email和Webhook + content = "{{value}},当前剩余额度为 {{value}},为了不影响您的使用,请及时充值。
充值链接:{{value}}" + values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota), topUpLink, topUpLink} + } + + err := NotifyUser(relayInfo.UserId, relayInfo.UserEmail, relayInfo.UserSetting, dto.NewNotify(dto.NotifyTypeQuotaExceed, prompt, content, values)) if err != nil { common.SysError(fmt.Sprintf("failed to send quota notify to user %d: %s", relayInfo.UserId, err.Error())) } diff --git a/service/user_notify.go b/service/user_notify.go index 7c864a1b1..c4a3ea91f 100644 --- a/service/user_notify.go +++ b/service/user_notify.go @@ -2,9 +2,12 @@ package service import ( "fmt" + "net/http" + "net/url" "one-api/common" "one-api/dto" "one-api/model" + "one-api/setting" "strings" ) @@ -51,6 +54,13 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data // 获取 webhook secret webhookSecret := userSetting.WebhookSecret return SendWebhookNotify(webhookURLStr, webhookSecret, data) + case dto.NotifyTypeBark: + barkURL := userSetting.BarkUrl + if barkURL == "" { + common.SysLog(fmt.Sprintf("user %d has no bark url, skip sending bark", userId)) + return nil + } + return sendBarkNotify(barkURL, data) } return nil } @@ -64,3 +74,67 @@ func sendEmailNotify(userEmail string, data dto.Notify) error { } return common.SendEmail(data.Title, userEmail, content) } + +func sendBarkNotify(barkURL string, data dto.Notify) error { + // 处理占位符 + content := data.Content + for _, value := range data.Values { + content = strings.Replace(content, dto.ContentValueParam, fmt.Sprintf("%v", value), 1) + } + + // 替换模板变量 + finalURL := strings.ReplaceAll(barkURL, "{{title}}", url.QueryEscape(data.Title)) + finalURL = strings.ReplaceAll(finalURL, "{{content}}", url.QueryEscape(content)) + + // 发送GET请求到Bark + var req *http.Request + var resp *http.Response + var err error + + if setting.EnableWorker() { + // 使用worker发送请求 + workerReq := &WorkerRequest{ + URL: finalURL, + Key: setting.WorkerValidKey, + Method: http.MethodGet, + Headers: map[string]string{ + "User-Agent": "OneAPI-Bark-Notify/1.0", + }, + } + + resp, err = DoWorkerRequest(workerReq) + if err != nil { + return fmt.Errorf("failed to send bark request through worker: %v", err) + } + defer resp.Body.Close() + + // 检查响应状态 + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("bark request failed with status code: %d", resp.StatusCode) + } + } else { + // 直接发送请求 + req, err = http.NewRequest(http.MethodGet, finalURL, nil) + if err != nil { + return fmt.Errorf("failed to create bark request: %v", err) + } + + // 设置User-Agent + req.Header.Set("User-Agent", "OneAPI-Bark-Notify/1.0") + + // 发送请求 + client := GetHttpClient() + resp, err = client.Do(req) + if err != nil { + return fmt.Errorf("failed to send bark request: %v", err) + } + defer resp.Body.Close() + + // 检查响应状态 + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("bark request failed with status code: %d", resp.StatusCode) + } + } + + return nil +} diff --git a/web/src/components/settings/PersonalSetting.jsx b/web/src/components/settings/PersonalSetting.jsx index 07f2b8c48..422cf0e88 100644 --- a/web/src/components/settings/PersonalSetting.jsx +++ b/web/src/components/settings/PersonalSetting.jsx @@ -67,6 +67,7 @@ const PersonalSetting = () => { webhookUrl: '', webhookSecret: '', notificationEmail: '', + barkUrl: '', acceptUnsetModelRatioModel: false, recordIpLog: false, }); @@ -108,6 +109,7 @@ const PersonalSetting = () => { webhookUrl: settings.webhook_url || '', webhookSecret: settings.webhook_secret || '', notificationEmail: settings.notification_email || '', + barkUrl: settings.bark_url || '', acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false, recordIpLog: settings.record_ip_log || false, @@ -285,6 +287,7 @@ const PersonalSetting = () => { webhook_url: notificationSettings.webhookUrl, webhook_secret: notificationSettings.webhookSecret, notification_email: notificationSettings.notificationEmail, + bark_url: notificationSettings.barkUrl, accept_unset_model_ratio_model: notificationSettings.acceptUnsetModelRatioModel, record_ip_log: notificationSettings.recordIpLog, diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index d76706c55..c7a31bd52 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -347,6 +347,7 @@ const NotificationSettings = ({ > {t('邮件通知')} {t('Webhook通知')} + {t('Bark通知')} )} + + {/* Bark推送设置 */} + {notificationSettings.warningType === 'bark' && ( + <> + handleFormChange('barkUrl', val)} + prefix={} + extraText={t( + '支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)', + )} + showClear + rules={[ + { + required: + notificationSettings.warningType === 'bark', + message: t('请输入Bark推送URL'), + }, + { + pattern: /^https?:\/\/.+/, + message: t('Bark推送URL必须以http://或https://开头'), + }, + ]} + /> + +
+
+ {t('模板示例')} +
+
+ https://api.day.app/yourkey/{'{{title}}'}/{'{{content}}'}?sound=alarm&group=quota +
+
+
{'title'}: {t('通知标题')}
+
{'content'}: {t('通知内容')}
+
+ {t('更多参数请参考')}{' '} + + Bark 官方文档 + +
+
+
+ + )}