mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-06-07 22:09:57 +00:00
feat: add Gotify notification option for quota alerts
This commit is contained in:
@@ -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) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证预警类型
|
// 验证预警类型
|
||||||
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) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是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) {
|
|||||||
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 {
|
||||||
|
|||||||
@@ -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 (
|
|||||||
NotifyTypeEmail = "email" // Email 邮件
|
NotifyTypeEmail = "email" // Email 邮件
|
||||||
NotifyTypeWebhook = "webhook" // Webhook
|
NotifyTypeWebhook = "webhook" // Webhook
|
||||||
NotifyTypeBark = "bark" // Bark 推送
|
NotifyTypeBark = "bark" // Bark 推送
|
||||||
|
NotifyTypeGotify = "gotify" // Gotify 推送
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
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
|
|||||||
|
|
||||||
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
|
|||||||
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 {
|
|||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 = () => {
|
|||||||
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 = () => {
|
|||||||
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,
|
||||||
|
|||||||
@@ -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 = ({
|
|||||||
</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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user