feat: bark notification #1699
This commit is contained in:
parent
5bb732394f
commit
4f44bbed31
@ -1097,6 +1097,7 @@ type UpdateUserSettingRequest struct {
|
|||||||
WebhookUrl string `json:"webhook_url,omitempty"`
|
WebhookUrl string `json:"webhook_url,omitempty"`
|
||||||
WebhookSecret string `json:"webhook_secret,omitempty"`
|
WebhookSecret string `json:"webhook_secret,omitempty"`
|
||||||
NotificationEmail string `json:"notification_email,omitempty"`
|
NotificationEmail string `json:"notification_email,omitempty"`
|
||||||
|
BarkUrl string `json:"bark_url,omitempty"`
|
||||||
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
|
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
|
||||||
RecordIpLog bool `json:"record_ip_log"`
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "无效的预警类型",
|
"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")
|
userId := c.GetInt("id")
|
||||||
user, err := model.GetUserById(userId, true)
|
user, err := model.GetUserById(userId, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1188,6 +1216,11 @@ func UpdateUserSetting(c *gin.Context) {
|
|||||||
settings.NotificationEmail = req.NotificationEmail
|
settings.NotificationEmail = req.NotificationEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是Bark类型,添加Bark URL到设置中
|
||||||
|
if req.QuotaWarningType == dto.NotifyTypeBark {
|
||||||
|
settings.BarkUrl = req.BarkUrl
|
||||||
|
}
|
||||||
|
|
||||||
// 更新用户设置
|
// 更新用户设置
|
||||||
user.SetSetting(settings)
|
user.SetSetting(settings)
|
||||||
if err := user.Update(false); err != nil {
|
if err := user.Update(false); err != nil {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ type UserSetting struct {
|
|||||||
WebhookUrl string `json:"webhook_url,omitempty"` // WebhookUrl webhook地址
|
WebhookUrl string `json:"webhook_url,omitempty"` // WebhookUrl webhook地址
|
||||||
WebhookSecret string `json:"webhook_secret,omitempty"` // WebhookSecret webhook密钥
|
WebhookSecret string `json:"webhook_secret,omitempty"` // WebhookSecret webhook密钥
|
||||||
NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址
|
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 是否接受未设置价格的模型
|
AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型
|
||||||
RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP
|
RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP
|
||||||
SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置
|
SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置
|
||||||
@ -14,4 +15,5 @@ type UserSetting struct {
|
|||||||
var (
|
var (
|
||||||
NotifyTypeEmail = "email" // Email 邮件
|
NotifyTypeEmail = "email" // Email 邮件
|
||||||
NotifyTypeWebhook = "webhook" // Webhook
|
NotifyTypeWebhook = "webhook" // Webhook
|
||||||
|
NotifyTypeBark = "bark" // Bark 推送
|
||||||
)
|
)
|
||||||
|
|||||||
@ -535,8 +535,27 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon
|
|||||||
if quotaTooLow {
|
if quotaTooLow {
|
||||||
prompt := "您的额度即将用尽"
|
prompt := "您的额度即将用尽"
|
||||||
topUpLink := fmt.Sprintf("%s/topup", setting.ServerAddress)
|
topUpLink := fmt.Sprintf("%s/topup", setting.ServerAddress)
|
||||||
content := "{{value}},当前剩余额度为 {{value}},为了不影响您的使用,请及时充值。<br/>充值链接:<a href='{{value}}'>{{value}}</a>"
|
|
||||||
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}},为了不影响您的使用,请及时充值。<br/>充值链接:<a href='{{value}}'>{{value}}</a>"
|
||||||
|
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 {
|
if err != nil {
|
||||||
common.SysError(fmt.Sprintf("failed to send quota notify to user %d: %s", relayInfo.UserId, err.Error()))
|
common.SysError(fmt.Sprintf("failed to send quota notify to user %d: %s", relayInfo.UserId, err.Error()))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,12 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
|
"one-api/setting"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,6 +54,13 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data
|
|||||||
// 获取 webhook secret
|
// 获取 webhook secret
|
||||||
webhookSecret := userSetting.WebhookSecret
|
webhookSecret := userSetting.WebhookSecret
|
||||||
return SendWebhookNotify(webhookURLStr, webhookSecret, data)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@ -64,3 +74,67 @@ func sendEmailNotify(userEmail string, data dto.Notify) error {
|
|||||||
}
|
}
|
||||||
return common.SendEmail(data.Title, userEmail, content)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -67,6 +67,7 @@ const PersonalSetting = () => {
|
|||||||
webhookUrl: '',
|
webhookUrl: '',
|
||||||
webhookSecret: '',
|
webhookSecret: '',
|
||||||
notificationEmail: '',
|
notificationEmail: '',
|
||||||
|
barkUrl: '',
|
||||||
acceptUnsetModelRatioModel: false,
|
acceptUnsetModelRatioModel: false,
|
||||||
recordIpLog: false,
|
recordIpLog: false,
|
||||||
});
|
});
|
||||||
@ -108,6 +109,7 @@ const PersonalSetting = () => {
|
|||||||
webhookUrl: settings.webhook_url || '',
|
webhookUrl: settings.webhook_url || '',
|
||||||
webhookSecret: settings.webhook_secret || '',
|
webhookSecret: settings.webhook_secret || '',
|
||||||
notificationEmail: settings.notification_email || '',
|
notificationEmail: settings.notification_email || '',
|
||||||
|
barkUrl: settings.bark_url || '',
|
||||||
acceptUnsetModelRatioModel:
|
acceptUnsetModelRatioModel:
|
||||||
settings.accept_unset_model_ratio_model || false,
|
settings.accept_unset_model_ratio_model || false,
|
||||||
recordIpLog: settings.record_ip_log || false,
|
recordIpLog: settings.record_ip_log || false,
|
||||||
@ -285,6 +287,7 @@ const PersonalSetting = () => {
|
|||||||
webhook_url: notificationSettings.webhookUrl,
|
webhook_url: notificationSettings.webhookUrl,
|
||||||
webhook_secret: notificationSettings.webhookSecret,
|
webhook_secret: notificationSettings.webhookSecret,
|
||||||
notification_email: notificationSettings.notificationEmail,
|
notification_email: notificationSettings.notificationEmail,
|
||||||
|
bark_url: notificationSettings.barkUrl,
|
||||||
accept_unset_model_ratio_model:
|
accept_unset_model_ratio_model:
|
||||||
notificationSettings.acceptUnsetModelRatioModel,
|
notificationSettings.acceptUnsetModelRatioModel,
|
||||||
record_ip_log: notificationSettings.recordIpLog,
|
record_ip_log: notificationSettings.recordIpLog,
|
||||||
|
|||||||
@ -347,6 +347,7 @@ const NotificationSettings = ({
|
|||||||
>
|
>
|
||||||
<Radio value='email'>{t('邮件通知')}</Radio>
|
<Radio value='email'>{t('邮件通知')}</Radio>
|
||||||
<Radio value='webhook'>{t('Webhook通知')}</Radio>
|
<Radio value='webhook'>{t('Webhook通知')}</Radio>
|
||||||
|
<Radio value='bark'>{t('Bark通知')}</Radio>
|
||||||
</Form.RadioGroup>
|
</Form.RadioGroup>
|
||||||
|
|
||||||
<Form.AutoComplete
|
<Form.AutoComplete
|
||||||
@ -483,6 +484,58 @@ const NotificationSettings = ({
|
|||||||
</Form.Slot>
|
</Form.Slot>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Bark推送设置 */}
|
||||||
|
{notificationSettings.warningType === 'bark' && (
|
||||||
|
<>
|
||||||
|
<Form.Input
|
||||||
|
field='barkUrl'
|
||||||
|
label={t('Bark推送URL')}
|
||||||
|
placeholder={t('请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}')}
|
||||||
|
onChange={(val) => handleFormChange('barkUrl', val)}
|
||||||
|
prefix={<IconLink />}
|
||||||
|
extraText={t(
|
||||||
|
'支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)',
|
||||||
|
)}
|
||||||
|
showClear
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required:
|
||||||
|
notificationSettings.warningType === 'bark',
|
||||||
|
message: t('请输入Bark推送URL'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^https?:\/\/.+/,
|
||||||
|
message: t('Bark推送URL必须以http://或https://开头'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='mt-3 p-4 bg-gray-50/50 rounded-xl'>
|
||||||
|
<div className='text-sm text-gray-700 mb-3'>
|
||||||
|
<strong>{t('模板示例')}</strong>
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-gray-600 font-mono bg-white p-3 rounded-lg shadow-sm mb-4'>
|
||||||
|
https://api.day.app/yourkey/{'{{title}}'}/{'{{content}}'}?sound=alarm&group=quota
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-gray-500 space-y-2'>
|
||||||
|
<div>• <strong>{'title'}:</strong> {t('通知标题')}</div>
|
||||||
|
<div>• <strong>{'content'}:</strong> {t('通知内容')}</div>
|
||||||
|
<div className='mt-3 pt-3 border-t border-gray-200'>
|
||||||
|
<span className='text-gray-400'>{t('更多参数请参考')}</span>{' '}
|
||||||
|
<a
|
||||||
|
href='https://github.com/Finb/Bark'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
className='text-blue-500 hover:text-blue-600 font-medium'
|
||||||
|
>
|
||||||
|
Bark 官方文档
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user