Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
99afd05d
authored
Sep 01, 2025
by
Seefs
Committed by
GitHub
Sep 01, 2025
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1712 from seefs001/feature/bark
feat: bark notification #1699
parents
60b7db29
4f44bbed
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
187 additions
and
3 deletions
+187
-3
controller/user.go
+34
-1
dto/user_settings.go
+2
-0
service/quota.go
+21
-2
service/user_notify.go
+74
-0
web/src/components/settings/PersonalSetting.jsx
+3
-0
web/src/components/settings/personal/cards/NotificationSettings.jsx
+53
-0
No files found.
controller/user.go
View file @
99afd05d
...
@@ -1097,6 +1097,7 @@ type UpdateUserSettingRequest struct {
...
@@ -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) {
...
@@ -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) {
...
@@ -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) {
...
@@ -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
{
...
...
dto/user_settings.go
View file @
99afd05d
...
@@ -6,6 +6,7 @@ type UserSetting struct {
...
@@ -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 {
...
@@ -14,4 +15,5 @@ type UserSetting struct {
var
(
var
(
NotifyTypeEmail
=
"email"
// Email 邮件
NotifyTypeEmail
=
"email"
// Email 邮件
NotifyTypeWebhook
=
"webhook"
// Webhook
NotifyTypeWebhook
=
"webhook"
// Webhook
NotifyTypeBark
=
"bark"
// Bark 推送
)
)
service/quota.go
View file @
99afd05d
...
@@ -535,8 +535,27 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon
...
@@ -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
()))
}
}
...
...
service/user_notify.go
View file @
99afd05d
...
@@ -2,9 +2,12 @@ package service
...
@@ -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
...
@@ -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 {
...
@@ -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
}
web/src/components/settings/PersonalSetting.jsx
View file @
99afd05d
...
@@ -67,6 +67,7 @@ const PersonalSetting = () => {
...
@@ -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 = () => {
...
@@ -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 = () => {
...
@@ -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
,
...
...
web/src/components/settings/personal/cards/NotificationSettings.jsx
View file @
99afd05d
...
@@ -347,6 +347,7 @@ const NotificationSettings = ({
...
@@ -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 = ({
...
@@ -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
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment