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 = ({
>