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
98050ee6
authored
Oct 01, 2025
by
RedwindA
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add Gotify notification option for quota alerts
parent
7b96f7c8
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
287 additions
and
6 deletions
+287
-6
controller/user.go
+50
-1
dto/user_settings.go
+4
-0
service/quota.go
+4
-1
service/user_notify.go
+112
-4
web/src/components/settings/PersonalSetting.jsx
+15
-0
web/src/components/settings/personal/cards/NotificationSettings.jsx
+102
-0
No files found.
controller/user.go
View file @
98050ee6
...
@@ -1102,6 +1102,9 @@ type UpdateUserSettingRequest struct {
...
@@ -1102,6 +1102,9 @@ type UpdateUserSettingRequest struct {
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"`
BarkUrl
string
`json:"bark_url,omitempty"`
GotifyUrl
string
`json:"gotify_url,omitempty"`
GotifyToken
string
`json:"gotify_token,omitempty"`
GotifyPriority
int
`json:"gotify_priority,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"`
}
}
...
@@ -1117,7 +1120,7 @@ func UpdateUserSetting(c *gin.Context) {
...
@@ -1117,7 +1120,7 @@ func UpdateUserSetting(c *gin.Context) {
}
}
// 验证预警类型
// 验证预警类型
if
req
.
QuotaWarningType
!=
dto
.
NotifyTypeEmail
&&
req
.
QuotaWarningType
!=
dto
.
NotifyTypeWebhook
&&
req
.
QuotaWarningType
!=
dto
.
NotifyTypeBark
{
if
req
.
QuotaWarningType
!=
dto
.
NotifyTypeEmail
&&
req
.
QuotaWarningType
!=
dto
.
NotifyTypeWebhook
&&
req
.
QuotaWarningType
!=
dto
.
NotifyTypeBark
&&
req
.
QuotaWarningType
!=
dto
.
NotifyTypeGotify
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"success"
:
false
,
"message"
:
"无效的预警类型"
,
"message"
:
"无效的预警类型"
,
...
@@ -1192,6 +1195,40 @@ func UpdateUserSetting(c *gin.Context) {
...
@@ -1192,6 +1195,40 @@ func UpdateUserSetting(c *gin.Context) {
}
}
}
}
// 如果是Gotify类型,验证Gotify URL和Token
if
req
.
QuotaWarningType
==
dto
.
NotifyTypeGotify
{
if
req
.
GotifyUrl
==
""
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"Gotify服务器地址不能为空"
,
})
return
}
if
req
.
GotifyToken
==
""
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"Gotify令牌不能为空"
,
})
return
}
// 验证URL格式
if
_
,
err
:=
url
.
ParseRequestURI
(
req
.
GotifyUrl
);
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"无效的Gotify服务器地址"
,
})
return
}
// 检查是否是HTTP或HTTPS
if
!
strings
.
HasPrefix
(
req
.
GotifyUrl
,
"https://"
)
&&
!
strings
.
HasPrefix
(
req
.
GotifyUrl
,
"http://"
)
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"Gotify服务器地址必须以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
{
...
@@ -1225,6 +1262,18 @@ func UpdateUserSetting(c *gin.Context) {
...
@@ -1225,6 +1262,18 @@ func UpdateUserSetting(c *gin.Context) {
settings
.
BarkUrl
=
req
.
BarkUrl
settings
.
BarkUrl
=
req
.
BarkUrl
}
}
// 如果是Gotify类型,添加Gotify配置到设置中
if
req
.
QuotaWarningType
==
dto
.
NotifyTypeGotify
{
settings
.
GotifyUrl
=
req
.
GotifyUrl
settings
.
GotifyToken
=
req
.
GotifyToken
// Gotify优先级范围0-10,超出范围则使用默认值5
if
req
.
GotifyPriority
<
0
||
req
.
GotifyPriority
>
10
{
settings
.
GotifyPriority
=
5
}
else
{
settings
.
GotifyPriority
=
req
.
GotifyPriority
}
}
// 更新用户设置
// 更新用户设置
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 @
98050ee6
...
@@ -7,6 +7,9 @@ type UserSetting struct {
...
@@ -7,6 +7,9 @@ type UserSetting struct {
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
BarkUrl
string
`json:"bark_url,omitempty"`
// BarkUrl Bark推送URL
GotifyUrl
string
`json:"gotify_url,omitempty"`
// GotifyUrl Gotify服务器地址
GotifyToken
string
`json:"gotify_token,omitempty"`
// GotifyToken Gotify应用令牌
GotifyPriority
int
`json:"gotify_priority"`
// GotifyPriority Gotify消息优先级
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 左侧边栏模块配置
...
@@ -16,4 +19,5 @@ var (
...
@@ -16,4 +19,5 @@ var (
NotifyTypeEmail
=
"email"
// Email 邮件
NotifyTypeEmail
=
"email"
// Email 邮件
NotifyTypeWebhook
=
"webhook"
// Webhook
NotifyTypeWebhook
=
"webhook"
// Webhook
NotifyTypeBark
=
"bark"
// Bark 推送
NotifyTypeBark
=
"bark"
// Bark 推送
NotifyTypeGotify
=
"gotify"
// Gotify 推送
)
)
service/quota.go
View file @
98050ee6
...
@@ -549,8 +549,11 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon
...
@@ -549,8 +549,11 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon
// Bark推送使用简短文本,不支持HTML
// Bark推送使用简短文本,不支持HTML
content
=
"{{value}},剩余额度:{{value}},请及时充值"
content
=
"{{value}},剩余额度:{{value}},请及时充值"
values
=
[]
interface
{}{
prompt
,
logger
.
FormatQuota
(
relayInfo
.
UserQuota
)}
values
=
[]
interface
{}{
prompt
,
logger
.
FormatQuota
(
relayInfo
.
UserQuota
)}
}
else
if
notifyType
==
dto
.
NotifyTypeGotify
{
content
=
"{{value}},当前剩余额度为 {{value}},请及时充值。"
values
=
[]
interface
{}{
prompt
,
logger
.
FormatQuota
(
relayInfo
.
UserQuota
)}
}
else
{
}
else
{
// 默认内容格式,适用于Email和Webhook
// 默认内容格式,适用于Email和Webhook
(支持HTML)
content
=
"{{value}},当前剩余额度为 {{value}},为了不影响您的使用,请及时充值。<br/>充值链接:<a href='{{value}}'>{{value}}</a>"
content
=
"{{value}},当前剩余额度为 {{value}},为了不影响您的使用,请及时充值。<br/>充值链接:<a href='{{value}}'>{{value}}</a>"
values
=
[]
interface
{}{
prompt
,
logger
.
FormatQuota
(
relayInfo
.
UserQuota
),
topUpLink
,
topUpLink
}
values
=
[]
interface
{}{
prompt
,
logger
.
FormatQuota
(
relayInfo
.
UserQuota
),
topUpLink
,
topUpLink
}
}
}
...
...
service/user_notify.go
View file @
98050ee6
package
service
package
service
import
(
import
(
"bytes"
"encoding/json"
"fmt"
"fmt"
"net/http"
"net/http"
"net/url"
"net/url"
...
@@ -37,13 +39,16 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data
...
@@ -37,13 +39,16 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data
switch
notifyType
{
switch
notifyType
{
case
dto
.
NotifyTypeEmail
:
case
dto
.
NotifyTypeEmail
:
// check setting email
// 优先使用设置中的通知邮箱,如果为空则使用用户的默认邮箱
userEmail
=
userSetting
.
NotificationEmail
emailToUse
:=
userSetting
.
NotificationEmail
if
userEmail
==
""
{
if
emailToUse
==
""
{
emailToUse
=
userEmail
}
if
emailToUse
==
""
{
common
.
SysLog
(
fmt
.
Sprintf
(
"user %d has no email, skip sending email"
,
userId
))
common
.
SysLog
(
fmt
.
Sprintf
(
"user %d has no email, skip sending email"
,
userId
))
return
nil
return
nil
}
}
return
sendEmailNotify
(
userEmail
,
data
)
return
sendEmailNotify
(
emailToUse
,
data
)
case
dto
.
NotifyTypeWebhook
:
case
dto
.
NotifyTypeWebhook
:
webhookURLStr
:=
userSetting
.
WebhookUrl
webhookURLStr
:=
userSetting
.
WebhookUrl
if
webhookURLStr
==
""
{
if
webhookURLStr
==
""
{
...
@@ -61,6 +66,14 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data
...
@@ -61,6 +66,14 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data
return
nil
return
nil
}
}
return
sendBarkNotify
(
barkURL
,
data
)
return
sendBarkNotify
(
barkURL
,
data
)
case
dto
.
NotifyTypeGotify
:
gotifyUrl
:=
userSetting
.
GotifyUrl
gotifyToken
:=
userSetting
.
GotifyToken
if
gotifyUrl
==
""
||
gotifyToken
==
""
{
common
.
SysLog
(
fmt
.
Sprintf
(
"user %d has no gotify url or token, skip sending gotify"
,
userId
))
return
nil
}
return
sendGotifyNotify
(
gotifyUrl
,
gotifyToken
,
userSetting
.
GotifyPriority
,
data
)
}
}
return
nil
return
nil
}
}
...
@@ -144,3 +157,98 @@ func sendBarkNotify(barkURL string, data dto.Notify) error {
...
@@ -144,3 +157,98 @@ func sendBarkNotify(barkURL string, data dto.Notify) error {
return
nil
return
nil
}
}
func
sendGotifyNotify
(
gotifyUrl
string
,
gotifyToken
string
,
priority
int
,
data
dto
.
Notify
)
error
{
// 处理占位符
content
:=
data
.
Content
for
_
,
value
:=
range
data
.
Values
{
content
=
strings
.
Replace
(
content
,
dto
.
ContentValueParam
,
fmt
.
Sprintf
(
"%v"
,
value
),
1
)
}
// 构建完整的 Gotify API URL
// 确保 URL 以 /message 结尾
finalURL
:=
strings
.
TrimSuffix
(
gotifyUrl
,
"/"
)
+
"/message?token="
+
url
.
QueryEscape
(
gotifyToken
)
// Gotify优先级范围0-10,如果超出范围则使用默认值5
if
priority
<
0
||
priority
>
10
{
priority
=
5
}
// 构建 JSON payload
type
GotifyMessage
struct
{
Title
string
`json:"title"`
Message
string
`json:"message"`
Priority
int
`json:"priority"`
}
payload
:=
GotifyMessage
{
Title
:
data
.
Title
,
Message
:
content
,
Priority
:
priority
,
}
// 序列化为 JSON
payloadBytes
,
err
:=
json
.
Marshal
(
payload
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to marshal gotify payload: %v"
,
err
)
}
var
req
*
http
.
Request
var
resp
*
http
.
Response
if
system_setting
.
EnableWorker
()
{
// 使用worker发送请求
workerReq
:=
&
WorkerRequest
{
URL
:
finalURL
,
Key
:
system_setting
.
WorkerValidKey
,
Method
:
http
.
MethodPost
,
Headers
:
map
[
string
]
string
{
"Content-Type"
:
"application/json; charset=utf-8"
,
"User-Agent"
:
"OneAPI-Gotify-Notify/1.0"
,
},
Body
:
payloadBytes
,
}
resp
,
err
=
DoWorkerRequest
(
workerReq
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to send gotify request through worker: %v"
,
err
)
}
defer
resp
.
Body
.
Close
()
// 检查响应状态
if
resp
.
StatusCode
<
200
||
resp
.
StatusCode
>=
300
{
return
fmt
.
Errorf
(
"gotify request failed with status code: %d"
,
resp
.
StatusCode
)
}
}
else
{
// SSRF防护:验证Gotify URL(非Worker模式)
fetchSetting
:=
system_setting
.
GetFetchSetting
()
if
err
:=
common
.
ValidateURLWithFetchSetting
(
finalURL
,
fetchSetting
.
EnableSSRFProtection
,
fetchSetting
.
AllowPrivateIp
,
fetchSetting
.
DomainFilterMode
,
fetchSetting
.
IpFilterMode
,
fetchSetting
.
DomainList
,
fetchSetting
.
IpList
,
fetchSetting
.
AllowedPorts
,
fetchSetting
.
ApplyIPFilterForDomain
);
err
!=
nil
{
return
fmt
.
Errorf
(
"request reject: %v"
,
err
)
}
// 直接发送请求
req
,
err
=
http
.
NewRequest
(
http
.
MethodPost
,
finalURL
,
bytes
.
NewBuffer
(
payloadBytes
))
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to create gotify request: %v"
,
err
)
}
// 设置请求头
req
.
Header
.
Set
(
"Content-Type"
,
"application/json; charset=utf-8"
)
req
.
Header
.
Set
(
"User-Agent"
,
"NewAPI-Gotify-Notify/1.0"
)
// 发送请求
client
:=
GetHttpClient
()
resp
,
err
=
client
.
Do
(
req
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to send gotify request: %v"
,
err
)
}
defer
resp
.
Body
.
Close
()
// 检查响应状态
if
resp
.
StatusCode
<
200
||
resp
.
StatusCode
>=
300
{
return
fmt
.
Errorf
(
"gotify request failed with status code: %d"
,
resp
.
StatusCode
)
}
}
return
nil
}
web/src/components/settings/PersonalSetting.jsx
View file @
98050ee6
...
@@ -81,6 +81,9 @@ const PersonalSetting = () => {
...
@@ -81,6 +81,9 @@ const PersonalSetting = () => {
webhookSecret
:
''
,
webhookSecret
:
''
,
notificationEmail
:
''
,
notificationEmail
:
''
,
barkUrl
:
''
,
barkUrl
:
''
,
gotifyUrl
:
''
,
gotifyToken
:
''
,
gotifyPriority
:
5
,
acceptUnsetModelRatioModel
:
false
,
acceptUnsetModelRatioModel
:
false
,
recordIpLog
:
false
,
recordIpLog
:
false
,
});
});
...
@@ -149,6 +152,12 @@ const PersonalSetting = () => {
...
@@ -149,6 +152,12 @@ const PersonalSetting = () => {
webhookSecret
:
settings
.
webhook_secret
||
''
,
webhookSecret
:
settings
.
webhook_secret
||
''
,
notificationEmail
:
settings
.
notification_email
||
''
,
notificationEmail
:
settings
.
notification_email
||
''
,
barkUrl
:
settings
.
bark_url
||
''
,
barkUrl
:
settings
.
bark_url
||
''
,
gotifyUrl
:
settings
.
gotify_url
||
''
,
gotifyToken
:
settings
.
gotify_token
||
''
,
gotifyPriority
:
settings
.
gotify_priority
!==
undefined
?
settings
.
gotify_priority
:
5
,
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
,
...
@@ -406,6 +415,12 @@ const PersonalSetting = () => {
...
@@ -406,6 +415,12 @@ const PersonalSetting = () => {
webhook_secret
:
notificationSettings
.
webhookSecret
,
webhook_secret
:
notificationSettings
.
webhookSecret
,
notification_email
:
notificationSettings
.
notificationEmail
,
notification_email
:
notificationSettings
.
notificationEmail
,
bark_url
:
notificationSettings
.
barkUrl
,
bark_url
:
notificationSettings
.
barkUrl
,
gotify_url
:
notificationSettings
.
gotifyUrl
,
gotify_token
:
notificationSettings
.
gotifyToken
,
gotify_priority
:
(()
=>
{
const
parsed
=
parseInt
(
notificationSettings
.
gotifyPriority
);
return
isNaN
(
parsed
)
?
5
:
parsed
;
})(),
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 @
98050ee6
...
@@ -400,6 +400,7 @@ const NotificationSettings = ({
...
@@ -400,6 +400,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
>
<
Radio
value=
'bark'
>
{
t
(
'Bark通知'
)
}
</
Radio
>
<
Radio
value=
'gotify'
>
{
t
(
'Gotify通知'
)
}
</
Radio
>
</
Form
.
RadioGroup
>
</
Form
.
RadioGroup
>
<
Form
.
AutoComplete
<
Form
.
AutoComplete
...
@@ -596,6 +597,107 @@ const NotificationSettings = ({
...
@@ -596,6 +597,107 @@ const NotificationSettings = ({
</
div
>
</
div
>
</>
</>
)
}
)
}
{
/* Gotify推送设置 */
}
{
notificationSettings
.
warningType
===
'gotify'
&&
(
<>
<
Form
.
Input
field=
'gotifyUrl'
label=
{
t
(
'Gotify服务器地址'
)
}
placeholder=
{
t
(
'请输入Gotify服务器地址,例如: https://gotify.example.com'
,
)
}
onChange=
{
(
val
)
=>
handleFormChange
(
'gotifyUrl'
,
val
)
}
prefix=
{
<
IconLink
/>
}
extraText=
{
t
(
'支持HTTP和HTTPS,填写Gotify服务器的完整URL地址'
,
)
}
showClear
rules=
{
[
{
required
:
notificationSettings
.
warningType
===
'gotify'
,
message
:
t
(
'请输入Gotify服务器地址'
),
},
{
pattern
:
/^https
?
:
\/\/
.+/
,
message
:
t
(
'Gotify服务器地址必须以http://或https://开头'
),
},
]
}
/>
<
Form
.
Input
field=
'gotifyToken'
label=
{
t
(
'Gotify应用令牌'
)
}
placeholder=
{
t
(
'请输入Gotify应用令牌'
)
}
onChange=
{
(
val
)
=>
handleFormChange
(
'gotifyToken'
,
val
)
}
prefix=
{
<
IconKey
/>
}
extraText=
{
t
(
'在Gotify服务器创建应用后获得的令牌,用于发送通知'
,
)
}
showClear
rules=
{
[
{
required
:
notificationSettings
.
warningType
===
'gotify'
,
message
:
t
(
'请输入Gotify应用令牌'
),
},
]
}
/>
<
Form
.
AutoComplete
field=
'gotifyPriority'
label=
{
t
(
'消息优先级'
)
}
placeholder=
{
t
(
'请选择消息优先级'
)
}
data=
{
[
{
value
:
0
,
label
:
t
(
'0 - 最低'
)
},
{
value
:
2
,
label
:
t
(
'2 - 低'
)
},
{
value
:
5
,
label
:
t
(
'5 - 正常(默认)'
)
},
{
value
:
8
,
label
:
t
(
'8 - 高'
)
},
{
value
:
10
,
label
:
t
(
'10 - 最高'
)
},
]
}
onChange=
{
(
val
)
=>
handleFormChange
(
'gotifyPriority'
,
val
)
}
prefix=
{
<
IconBell
/>
}
extraText=
{
t
(
'消息优先级,范围0-10,默认为5'
)
}
style=
{
{
width
:
'100%'
,
maxWidth
:
'300px'
}
}
/>
<
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-500 space-y-2'
>
<
div
>
1.
{
t
(
'在Gotify服务器的应用管理中创建新应用'
)
}
</
div
>
<
div
>
2.
{
' '
}
{
t
(
'复制应用的令牌(Token)并填写到上方的应用令牌字段'
,
)
}
</
div
>
<
div
>
3.
{
t
(
'填写Gotify服务器的完整URL地址'
)
}
</
div
>
<
div
className=
'mt-3 pt-3 border-t border-gray-200'
>
<
span
className=
'text-gray-400'
>
{
t
(
'更多信息请参考'
)
}
</
span
>
{
' '
}
<
a
href=
'https://gotify.net/'
target=
'_blank'
rel=
'noopener noreferrer'
className=
'text-blue-500 hover:text-blue-600 font-medium'
>
Gotify 官方文档
</
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