mirror of
https://github.com/nezhahq/nezha.git
synced 2025-01-22 20:58:14 -05:00
feat: add i18n support
This commit is contained in:
commit
05c5bcc6cb
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,6 +9,7 @@
|
|||||||
*.test
|
*.test
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*~
|
||||||
*.out
|
*.out
|
||||||
*.pprof
|
*.pprof
|
||||||
.idea
|
.idea
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -99,7 +97,7 @@ func updateAlertRule(c *gin.Context) (any, error) {
|
|||||||
|
|
||||||
var r model.AlertRule
|
var r model.AlertRule
|
||||||
if err := singleton.DB.First(&r, id).Error; err != nil {
|
if err := singleton.DB.First(&r, id).Error; err != nil {
|
||||||
return nil, fmt.Errorf("alert id %d does not exist", id)
|
return nil, singleton.Localizer.ErrorT("alert id %d does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateRule(&r); err != nil {
|
if err := validateRule(&r); err != nil {
|
||||||
@ -154,22 +152,22 @@ func validateRule(r *model.AlertRule) error {
|
|||||||
for _, rule := range r.Rules {
|
for _, rule := range r.Rules {
|
||||||
if !rule.IsTransferDurationRule() {
|
if !rule.IsTransferDurationRule() {
|
||||||
if rule.Duration < 3 {
|
if rule.Duration < 3 {
|
||||||
return errors.New("错误: Duration 至少为 3")
|
return singleton.Localizer.ErrorT("duration need to be at least 3")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if rule.CycleInterval < 1 {
|
if rule.CycleInterval < 1 {
|
||||||
return errors.New("错误: cycle_interval 至少为 1")
|
return singleton.Localizer.ErrorT("cycle_interval need to be at least 1")
|
||||||
}
|
}
|
||||||
if rule.CycleStart == nil {
|
if rule.CycleStart == nil {
|
||||||
return errors.New("错误: cycle_start 未设置")
|
return singleton.Localizer.ErrorT("cycle_start is not set")
|
||||||
}
|
}
|
||||||
if rule.CycleStart.After(time.Now()) {
|
if rule.CycleStart.After(time.Now()) {
|
||||||
return errors.New("错误: cycle_start 是个未来值")
|
return singleton.Localizer.ErrorT("cycle_start is a future value")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return errors.New("至少定义一条规则")
|
return singleton.Localizer.ErrorT("need to configure at least a single rule")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,257 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/hashicorp/go-uuid"
|
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
|
||||||
"github.com/naiba/nezha/pkg/websocketx"
|
|
||||||
"github.com/naiba/nezha/proto"
|
|
||||||
"github.com/naiba/nezha/service/rpc"
|
|
||||||
"github.com/naiba/nezha/service/singleton"
|
|
||||||
)
|
|
||||||
|
|
||||||
type commonPage struct {
|
|
||||||
r *gin.Engine
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *commonPage) serve() {
|
|
||||||
cr := cp.r.Group("")
|
|
||||||
// TODO: 界面直接跳转使用该接口
|
|
||||||
cr.GET("/network/:id", cp.network)
|
|
||||||
cr.GET("/network", cp.network)
|
|
||||||
cr.GET("/file", cp.createFM)
|
|
||||||
cr.GET("/file/:id", cp.fm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *commonPage) network(c *gin.Context) {
|
|
||||||
var (
|
|
||||||
monitorHistory *model.ServiceHistory
|
|
||||||
servers []model.Server
|
|
||||||
serverIdsWithMonitor []uint64
|
|
||||||
monitorInfos = []byte("{}")
|
|
||||||
id uint64
|
|
||||||
)
|
|
||||||
if len(singleton.SortedServerList) > 0 {
|
|
||||||
id = singleton.SortedServerList[0].ID
|
|
||||||
}
|
|
||||||
if err := singleton.DB.Model(&model.ServiceHistory{}).Select("monitor_id, server_id").
|
|
||||||
Where("monitor_id != 0 and server_id != 0").Limit(1).First(&monitorHistory).Error; err != nil {
|
|
||||||
// mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
||||||
// Code: http.StatusForbidden,
|
|
||||||
// Title: "请求失败",
|
|
||||||
// Msg: "请求参数有误:" + "server monitor history not found",
|
|
||||||
// Link: "/",
|
|
||||||
// Btn: "返回重试",
|
|
||||||
// }, true)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
if monitorHistory == nil || monitorHistory.ServerID == 0 {
|
|
||||||
if len(singleton.SortedServerList) > 0 {
|
|
||||||
id = singleton.SortedServerList[0].ID
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
id = monitorHistory.ServerID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
idStr := c.Param("id")
|
|
||||||
if idStr != "" {
|
|
||||||
var err error
|
|
||||||
id, err = strconv.ParseUint(idStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
// mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
||||||
// Code: http.StatusForbidden,
|
|
||||||
// Title: "请求失败",
|
|
||||||
// Msg: "请求参数有误:" + err.Error(),
|
|
||||||
// Link: "/",
|
|
||||||
// Btn: "返回重试",
|
|
||||||
// }, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, ok := singleton.ServerList[id]
|
|
||||||
if !ok {
|
|
||||||
// mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
||||||
// Code: http.StatusForbidden,
|
|
||||||
// Title: "请求失败",
|
|
||||||
// Msg: "请求参数有误:" + "server id not found",
|
|
||||||
// Link: "/",
|
|
||||||
// Btn: "返回重试",
|
|
||||||
// }, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
|
||||||
var isViewPasswordVerfied bool
|
|
||||||
|
|
||||||
if err := singleton.DB.Model(&model.ServiceHistory{}).
|
|
||||||
Select("distinct(server_id)").
|
|
||||||
Where("server_id != 0").
|
|
||||||
Find(&serverIdsWithMonitor).
|
|
||||||
Error; err != nil {
|
|
||||||
// mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
||||||
// Code: http.StatusForbidden,
|
|
||||||
// Title: "请求失败",
|
|
||||||
// Msg: "请求参数有误:" + "no server with monitor histories",
|
|
||||||
// Link: "/",
|
|
||||||
// Btn: "返回重试",
|
|
||||||
// }, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if isMember || isViewPasswordVerfied {
|
|
||||||
for _, server := range singleton.SortedServerList {
|
|
||||||
for _, id := range serverIdsWithMonitor {
|
|
||||||
if server.ID == id {
|
|
||||||
servers = append(servers, *server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, server := range singleton.SortedServerListForGuest {
|
|
||||||
for _, id := range serverIdsWithMonitor {
|
|
||||||
if server.ID == id {
|
|
||||||
servers = append(servers, *server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serversBytes, _ := utils.Json.Marshal(model.StreamServerData{
|
|
||||||
Now: time.Now().Unix() * 1000,
|
|
||||||
// Servers: servers,
|
|
||||||
})
|
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "", gin.H{
|
|
||||||
"Servers": string(serversBytes),
|
|
||||||
"MonitorInfos": string(monitorInfos),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *commonPage) fm(c *gin.Context) {
|
|
||||||
streamId := c.Param("id")
|
|
||||||
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
|
|
||||||
// mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
||||||
// Code: http.StatusForbidden,
|
|
||||||
// Title: "无权访问",
|
|
||||||
// Msg: "FM会话不存在",
|
|
||||||
// Link: "/",
|
|
||||||
// Btn: "返回首页",
|
|
||||||
// }, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
|
||||||
|
|
||||||
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
|
||||||
if err != nil {
|
|
||||||
// mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
||||||
// Code: http.StatusInternalServerError,
|
|
||||||
// // Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
|
||||||
// // MessageID: "NetworkError",
|
|
||||||
// // }),
|
|
||||||
// Msg: "Websocket协议切换失败",
|
|
||||||
// Link: "/",
|
|
||||||
// Btn: "返回首页",
|
|
||||||
// }, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer wsConn.Close()
|
|
||||||
conn := websocketx.NewConn(wsConn)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// PING 保活
|
|
||||||
for {
|
|
||||||
if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second * 10)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *commonPage) createFM(c *gin.Context) {
|
|
||||||
IdString := c.Query("id")
|
|
||||||
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
|
|
||||||
// mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
||||||
// Code: http.StatusForbidden,
|
|
||||||
// Title: "无权访问",
|
|
||||||
// Msg: "用户未登录",
|
|
||||||
// Link: "/login",
|
|
||||||
// Btn: "去登录",
|
|
||||||
// }, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
streamId, err := uuid.GenerateUUID()
|
|
||||||
if err != nil {
|
|
||||||
// mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
||||||
// Code: http.StatusInternalServerError,
|
|
||||||
// // Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
|
||||||
// // MessageID: "SystemError",
|
|
||||||
// // }),
|
|
||||||
// Msg: "生成会话ID失败",
|
|
||||||
// Link: "/server",
|
|
||||||
// Btn: "返回重试",
|
|
||||||
// }, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
|
||||||
|
|
||||||
serverId, err := strconv.Atoi(IdString)
|
|
||||||
if err != nil {
|
|
||||||
// mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
||||||
// Code: http.StatusForbidden,
|
|
||||||
// Title: "请求失败",
|
|
||||||
// Msg: "请求参数有误:" + err.Error(),
|
|
||||||
// Link: "/server",
|
|
||||||
// Btn: "返回重试",
|
|
||||||
// }, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
singleton.ServerLock.RLock()
|
|
||||||
server := singleton.ServerList[uint64(serverId)]
|
|
||||||
singleton.ServerLock.RUnlock()
|
|
||||||
if server == nil {
|
|
||||||
// mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
||||||
// Code: http.StatusForbidden,
|
|
||||||
// Title: "请求失败",
|
|
||||||
// Msg: "服务器不存在或处于离线状态",
|
|
||||||
// Link: "/server",
|
|
||||||
// Btn: "返回重试",
|
|
||||||
// }, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmData, _ := utils.Json.Marshal(&model.TaskFM{
|
|
||||||
StreamID: streamId,
|
|
||||||
})
|
|
||||||
if err := server.TaskStream.Send(&proto.Task{
|
|
||||||
Type: model.TaskTypeFM,
|
|
||||||
Data: string(fmData),
|
|
||||||
}); err != nil {
|
|
||||||
// mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
||||||
// Code: http.StatusForbidden,
|
|
||||||
// Title: "请求失败",
|
|
||||||
// Msg: "Agent信令下发失败",
|
|
||||||
// Link: "/server",
|
|
||||||
// Btn: "返回重试",
|
|
||||||
// }, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "dashboard-", gin.H{
|
|
||||||
"SessionID": streamId,
|
|
||||||
})
|
|
||||||
}
|
|
@ -185,7 +185,7 @@ func commonHandler[T any](handler handlerFunc[T]) func(*gin.Context) {
|
|||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case *gormError:
|
case *gormError:
|
||||||
log.Printf("NEZHA>> gorm error: %v", err)
|
log.Printf("NEZHA>> gorm error: %v", err)
|
||||||
c.JSON(http.StatusOK, newErrorResponse(errors.New("database error")))
|
c.JSON(http.StatusOK, newErrorResponse(singleton.Localizer.ErrorT("database error")))
|
||||||
return
|
return
|
||||||
case *wsError:
|
case *wsError:
|
||||||
// Connection is upgraded to WebSocket, so c.Writer is no longer usable
|
// Connection is upgraded to WebSocket, so c.Writer is no longer usable
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -61,7 +60,7 @@ func createCron(c *gin.Context) (uint64, error) {
|
|||||||
cr.Cover = cf.Cover
|
cr.Cover = cf.Cover
|
||||||
|
|
||||||
if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger {
|
if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger {
|
||||||
return 0, errors.New("计划任务类型不得使用触发服务器执行方式")
|
return 0, singleton.Localizer.ErrorT("scheduled tasks cannot be triggered by alarms")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于计划任务类型,需要更新CronJob
|
// 对于计划任务类型,需要更新CronJob
|
||||||
@ -120,7 +119,7 @@ func updateCron(c *gin.Context) (any, error) {
|
|||||||
cr.Cover = cf.Cover
|
cr.Cover = cf.Cover
|
||||||
|
|
||||||
if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger {
|
if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger {
|
||||||
return nil, errors.New("计划任务类型不得使用触发服务器执行方式")
|
return nil, singleton.Localizer.ErrorT("scheduled tasks cannot be triggered by alarms")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于计划任务类型,需要更新CronJob
|
// 对于计划任务类型,需要更新CronJob
|
||||||
@ -159,7 +158,7 @@ func manualTriggerCron(c *gin.Context) (any, error) {
|
|||||||
|
|
||||||
var cr model.Cron
|
var cr model.Cron
|
||||||
if err := singleton.DB.First(&cr, id).Error; err != nil {
|
if err := singleton.DB.First(&cr, id).Error; err != nil {
|
||||||
return nil, fmt.Errorf("task id %d does not exist", id)
|
return nil, singleton.Localizer.ErrorT("task id %d does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
singleton.ManualTrigger(&cr)
|
singleton.ManualTrigger(&cr)
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -55,7 +53,7 @@ func createDDNS(c *gin.Context) (uint64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if df.MaxRetries < 1 || df.MaxRetries > 10 {
|
if df.MaxRetries < 1 || df.MaxRetries > 10 {
|
||||||
return 0, errors.New("重试次数必须为大于 1 且不超过 10 的整数")
|
return 0, singleton.Localizer.ErrorT("the retry count must be an integer between 1 and 10")
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Name = df.Name
|
p.Name = df.Name
|
||||||
@ -78,7 +76,7 @@ func createDDNS(c *gin.Context) (uint64, error) {
|
|||||||
// IDN to ASCII
|
// IDN to ASCII
|
||||||
domainValid, domainErr := idna.Lookup.ToASCII(domain)
|
domainValid, domainErr := idna.Lookup.ToASCII(domain)
|
||||||
if domainErr != nil {
|
if domainErr != nil {
|
||||||
return 0, fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr)
|
return 0, singleton.Localizer.ErrorT("error parsing %s: %v", domain, domainErr)
|
||||||
}
|
}
|
||||||
p.Domains[n] = domainValid
|
p.Domains[n] = domainValid
|
||||||
}
|
}
|
||||||
@ -119,12 +117,12 @@ func updateDDNS(c *gin.Context) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if df.MaxRetries < 1 || df.MaxRetries > 10 {
|
if df.MaxRetries < 1 || df.MaxRetries > 10 {
|
||||||
return nil, errors.New("重试次数必须为大于 1 且不超过 10 的整数")
|
return nil, singleton.Localizer.ErrorT("the retry count must be an integer between 1 and 10")
|
||||||
}
|
}
|
||||||
|
|
||||||
var p model.DDNSProfile
|
var p model.DDNSProfile
|
||||||
if err = singleton.DB.First(&p, id).Error; err != nil {
|
if err = singleton.DB.First(&p, id).Error; err != nil {
|
||||||
return nil, fmt.Errorf("profile id %d does not exist", id)
|
return nil, singleton.Localizer.ErrorT("profile id %d does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Name = df.Name
|
p.Name = df.Name
|
||||||
@ -147,7 +145,7 @@ func updateDDNS(c *gin.Context) (any, error) {
|
|||||||
// IDN to ASCII
|
// IDN to ASCII
|
||||||
domainValid, domainErr := idna.Lookup.ToASCII(domain)
|
domainValid, domainErr := idna.Lookup.ToASCII(domain)
|
||||||
if domainErr != nil {
|
if domainErr != nil {
|
||||||
return nil, fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr)
|
return nil, singleton.Localizer.ErrorT("error parsing %s: %v", domain, domainErr)
|
||||||
}
|
}
|
||||||
p.Domains[n] = domainValid
|
p.Domains[n] = domainValid
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -26,7 +25,7 @@ import (
|
|||||||
// @Success 200 {object} model.CreateFMResponse
|
// @Success 200 {object} model.CreateFMResponse
|
||||||
// @Router /file [get]
|
// @Router /file [get]
|
||||||
func createFM(c *gin.Context) (*model.CreateFMResponse, error) {
|
func createFM(c *gin.Context) (*model.CreateFMResponse, error) {
|
||||||
idStr := c.Param("id")
|
idStr := c.Query("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -43,7 +42,7 @@ func createFM(c *gin.Context) (*model.CreateFMResponse, error) {
|
|||||||
server := singleton.ServerList[id]
|
server := singleton.ServerList[id]
|
||||||
singleton.ServerLock.RUnlock()
|
singleton.ServerLock.RUnlock()
|
||||||
if server == nil || server.TaskStream == nil {
|
if server == nil || server.TaskStream == nil {
|
||||||
return nil, errors.New("server not found or not connected")
|
return nil, singleton.Localizer.ErrorT("server not found or not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmData, _ := utils.Json.Marshal(&model.TaskFM{
|
fmData, _ := utils.Json.Marshal(&model.TaskFM{
|
||||||
|
@ -1,636 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/jinzhu/copier"
|
|
||||||
"golang.org/x/net/idna"
|
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
|
||||||
"github.com/naiba/nezha/proto"
|
|
||||||
"github.com/naiba/nezha/service/singleton"
|
|
||||||
)
|
|
||||||
|
|
||||||
type memberAPI struct {
|
|
||||||
r gin.IRouter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) serve() {
|
|
||||||
mr := ma.r.Group("")
|
|
||||||
// mr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
|
||||||
// MemberOnly: true,
|
|
||||||
// IsPage: false,
|
|
||||||
// Msg: "访问此接口需要登录",
|
|
||||||
// Btn: "点此登录",
|
|
||||||
// Redirect: "/login",
|
|
||||||
// }))
|
|
||||||
mr.POST("/cron", ma.addOrEditCron)
|
|
||||||
mr.GET("/cron/:id/manual", ma.manualTrigger)
|
|
||||||
mr.POST("/force-update", ma.forceUpdate)
|
|
||||||
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
|
|
||||||
mr.POST("/notification", ma.addOrEditNotification)
|
|
||||||
mr.POST("/ddns", ma.addOrEditDDNS)
|
|
||||||
mr.POST("/nat", ma.addOrEditNAT)
|
|
||||||
mr.POST("/alert-rule", ma.addOrEditAlertRule)
|
|
||||||
mr.POST("/setting", ma.updateSetting)
|
|
||||||
mr.DELETE("/:model/:id", ma.delete)
|
|
||||||
mr.POST("/logout", ma.logout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) delete(c *gin.Context) {
|
|
||||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
||||||
if id < 1 {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: "错误的 Server ID",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
switch c.Param("model") {
|
|
||||||
|
|
||||||
case "cron":
|
|
||||||
err = singleton.DB.Unscoped().Delete(&model.Cron{}, "id = ?", id).Error
|
|
||||||
if err == nil {
|
|
||||||
singleton.CronLock.RLock()
|
|
||||||
defer singleton.CronLock.RUnlock()
|
|
||||||
cr := singleton.Crons[id]
|
|
||||||
if cr != nil && cr.CronJobID != 0 {
|
|
||||||
singleton.Cron.Remove(cr.CronJobID)
|
|
||||||
}
|
|
||||||
delete(singleton.Crons, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: fmt.Sprintf("数据库错误:%s", err),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type cronForm struct {
|
|
||||||
ID uint64
|
|
||||||
TaskType uint8 // 0:计划任务 1:触发任务
|
|
||||||
Name string
|
|
||||||
Scheduler string
|
|
||||||
Command string
|
|
||||||
ServersRaw string
|
|
||||||
Cover uint8
|
|
||||||
PushSuccessful string
|
|
||||||
NotificationTag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) addOrEditCron(c *gin.Context) {
|
|
||||||
var cf cronForm
|
|
||||||
var cr model.Cron
|
|
||||||
err := c.ShouldBindJSON(&cf)
|
|
||||||
if err == nil {
|
|
||||||
cr.TaskType = cf.TaskType
|
|
||||||
cr.Name = cf.Name
|
|
||||||
cr.Scheduler = cf.Scheduler
|
|
||||||
cr.Command = cf.Command
|
|
||||||
cr.ServersRaw = cf.ServersRaw
|
|
||||||
cr.PushSuccessful = cf.PushSuccessful == "on"
|
|
||||||
//cr.NotificationTag = cf.NotificationTag
|
|
||||||
cr.ID = cf.ID
|
|
||||||
cr.Cover = cf.Cover
|
|
||||||
err = utils.Json.Unmarshal([]byte(cf.ServersRaw), &cr.Servers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计划任务类型不得使用触发服务器执行方式
|
|
||||||
if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger {
|
|
||||||
err = errors.New("计划任务类型不得使用触发服务器执行方式")
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: fmt.Sprintf("请求错误:%s", err),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := singleton.DB.Begin()
|
|
||||||
if err == nil {
|
|
||||||
// 保证NotificationTag不为空
|
|
||||||
//if cr.NotificationTag == "" {
|
|
||||||
// cr.NotificationTag = "default"
|
|
||||||
//}
|
|
||||||
if cf.ID == 0 {
|
|
||||||
err = tx.Create(&cr).Error
|
|
||||||
} else {
|
|
||||||
err = tx.Save(&cr).Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
err = tx.Commit().Error
|
|
||||||
} else {
|
|
||||||
tx.Rollback()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: fmt.Sprintf("请求错误:%s", err),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
singleton.CronLock.Lock()
|
|
||||||
defer singleton.CronLock.Unlock()
|
|
||||||
crOld := singleton.Crons[cr.ID]
|
|
||||||
if crOld != nil && crOld.CronJobID != 0 {
|
|
||||||
singleton.Cron.Remove(crOld.CronJobID)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(singleton.Crons, cr.ID)
|
|
||||||
singleton.Crons[cr.ID] = &cr
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) manualTrigger(c *gin.Context) {
|
|
||||||
var cr model.Cron
|
|
||||||
if err := singleton.DB.First(&cr, "id = ?", c.Param("id")).Error; err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//singleton.ManualTrigger(cr)
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type BatchUpdateServerGroupRequest struct {
|
|
||||||
Servers []uint64
|
|
||||||
Group string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) batchUpdateServerGroup(c *gin.Context) {
|
|
||||||
var req BatchUpdateServerGroupRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := singleton.DB.Model(&model.Server{}).Where("id in (?)", req.Servers).Update("tag", req.Group).Error; err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
singleton.ServerLock.Lock()
|
|
||||||
|
|
||||||
for i := 0; i < len(req.Servers); i++ {
|
|
||||||
serverId := req.Servers[i]
|
|
||||||
var s model.Server
|
|
||||||
copier.Copy(&s, singleton.ServerList[serverId])
|
|
||||||
// s.Tag = req.Group
|
|
||||||
// // 如果修改了Ta
|
|
||||||
// oldTag := singleton.ServerList[serverId].Tag
|
|
||||||
// newTag := s.Tag
|
|
||||||
// if newTag != oldTag {
|
|
||||||
// index := -1
|
|
||||||
// for i := 0; i < len(singleton.ServerTagToIDList[oldTag]); i++ {
|
|
||||||
// if singleton.ServerTagToIDList[oldTag][i] == s.ID {
|
|
||||||
// index = i
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if index > -1 {
|
|
||||||
// // 删除旧 Tag-ID 绑定关系
|
|
||||||
// singleton.ServerTagToIDList[oldTag] = append(singleton.ServerTagToIDList[oldTag][:index], singleton.ServerTagToIDList[oldTag][index+1:]...)
|
|
||||||
// if len(singleton.ServerTagToIDList[oldTag]) == 0 {
|
|
||||||
// delete(singleton.ServerTagToIDList, oldTag)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // 设置新的 Tag-ID 绑定关系
|
|
||||||
// singleton.ServerTagToIDList[newTag] = append(singleton.ServerTagToIDList[newTag], s.ID)
|
|
||||||
// }
|
|
||||||
singleton.ServerList[s.ID] = &s
|
|
||||||
}
|
|
||||||
|
|
||||||
singleton.ServerLock.Unlock()
|
|
||||||
|
|
||||||
singleton.ReSortServer()
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) forceUpdate(c *gin.Context) {
|
|
||||||
var forceUpdateServers []uint64
|
|
||||||
if err := c.ShouldBindJSON(&forceUpdateServers); err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var executeResult bytes.Buffer
|
|
||||||
|
|
||||||
for i := 0; i < len(forceUpdateServers); i++ {
|
|
||||||
singleton.ServerLock.RLock()
|
|
||||||
server := singleton.ServerList[forceUpdateServers[i]]
|
|
||||||
singleton.ServerLock.RUnlock()
|
|
||||||
if server != nil && server.TaskStream != nil {
|
|
||||||
if err := server.TaskStream.Send(&proto.Task{
|
|
||||||
Type: model.TaskTypeUpgrade,
|
|
||||||
}); err != nil {
|
|
||||||
executeResult.WriteString(fmt.Sprintf("%d 下发指令失败 %+v<br/>", forceUpdateServers[i], err))
|
|
||||||
} else {
|
|
||||||
executeResult.WriteString(fmt.Sprintf("%d 下发指令成功<br/>", forceUpdateServers[i]))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
executeResult.WriteString(fmt.Sprintf("%d 离线<br/>", forceUpdateServers[i]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
Message: executeResult.String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type notificationForm struct {
|
|
||||||
ID uint64
|
|
||||||
Name string
|
|
||||||
URL string
|
|
||||||
RequestMethod int
|
|
||||||
RequestType int
|
|
||||||
RequestHeader string
|
|
||||||
RequestBody string
|
|
||||||
VerifySSL string
|
|
||||||
SkipCheck string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
|
|
||||||
var nf notificationForm
|
|
||||||
var n model.Notification
|
|
||||||
err := c.ShouldBindJSON(&nf)
|
|
||||||
if err == nil {
|
|
||||||
n.Name = nf.Name
|
|
||||||
n.RequestMethod = nf.RequestMethod
|
|
||||||
n.RequestType = nf.RequestType
|
|
||||||
n.RequestHeader = nf.RequestHeader
|
|
||||||
n.RequestBody = nf.RequestBody
|
|
||||||
n.URL = nf.URL
|
|
||||||
verifySSL := nf.VerifySSL == "on"
|
|
||||||
n.VerifySSL = &verifySSL
|
|
||||||
n.ID = nf.ID
|
|
||||||
ns := model.NotificationServerBundle{
|
|
||||||
Notification: &n,
|
|
||||||
Server: nil,
|
|
||||||
Loc: singleton.Loc,
|
|
||||||
}
|
|
||||||
// 勾选了跳过检查
|
|
||||||
if nf.SkipCheck != "on" {
|
|
||||||
err = ns.Send("这是测试消息")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
if n.ID == 0 {
|
|
||||||
err = singleton.DB.Create(&n).Error
|
|
||||||
} else {
|
|
||||||
err = singleton.DB.Save(&n).Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: fmt.Sprintf("请求错误:%s", err),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
singleton.OnRefreshOrAddNotification(&n)
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type ddnsForm struct {
|
|
||||||
ID uint64
|
|
||||||
MaxRetries uint64
|
|
||||||
EnableIPv4 string
|
|
||||||
EnableIPv6 string
|
|
||||||
Name string
|
|
||||||
Provider string
|
|
||||||
DomainsRaw string
|
|
||||||
AccessID string
|
|
||||||
AccessSecret string
|
|
||||||
WebhookURL string
|
|
||||||
WebhookMethod uint8
|
|
||||||
WebhookRequestType uint8
|
|
||||||
WebhookRequestBody string
|
|
||||||
WebhookHeaders string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) addOrEditDDNS(c *gin.Context) {
|
|
||||||
var df ddnsForm
|
|
||||||
var p model.DDNSProfile
|
|
||||||
err := c.ShouldBindJSON(&df)
|
|
||||||
if err == nil {
|
|
||||||
if df.MaxRetries < 1 || df.MaxRetries > 10 {
|
|
||||||
err = errors.New("重试次数必须为大于 1 且不超过 10 的整数")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
p.Name = df.Name
|
|
||||||
p.ID = df.ID
|
|
||||||
enableIPv4 := df.EnableIPv4 == "on"
|
|
||||||
enableIPv6 := df.EnableIPv6 == "on"
|
|
||||||
p.EnableIPv4 = &enableIPv4
|
|
||||||
p.EnableIPv6 = &enableIPv6
|
|
||||||
p.MaxRetries = df.MaxRetries
|
|
||||||
p.Provider = df.Provider
|
|
||||||
p.DomainsRaw = df.DomainsRaw
|
|
||||||
p.Domains = strings.Split(p.DomainsRaw, ",")
|
|
||||||
p.AccessID = df.AccessID
|
|
||||||
p.AccessSecret = df.AccessSecret
|
|
||||||
p.WebhookURL = df.WebhookURL
|
|
||||||
p.WebhookMethod = df.WebhookMethod
|
|
||||||
p.WebhookRequestType = df.WebhookRequestType
|
|
||||||
p.WebhookRequestBody = df.WebhookRequestBody
|
|
||||||
p.WebhookHeaders = df.WebhookHeaders
|
|
||||||
|
|
||||||
for n, domain := range p.Domains {
|
|
||||||
// IDN to ASCII
|
|
||||||
domainValid, domainErr := idna.Lookup.ToASCII(domain)
|
|
||||||
if domainErr != nil {
|
|
||||||
err = fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
p.Domains[n] = domainValid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
if p.ID == 0 {
|
|
||||||
err = singleton.DB.Create(&p).Error
|
|
||||||
} else {
|
|
||||||
err = singleton.DB.Save(&p).Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: fmt.Sprintf("请求错误:%s", err),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//singleton.OnDDNSUpdate()
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type natForm struct {
|
|
||||||
ID uint64
|
|
||||||
Name string
|
|
||||||
ServerID uint64
|
|
||||||
Host string
|
|
||||||
Domain string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) addOrEditNAT(c *gin.Context) {
|
|
||||||
var nf natForm
|
|
||||||
var n model.NAT
|
|
||||||
err := c.ShouldBindJSON(&nf)
|
|
||||||
if err == nil {
|
|
||||||
n.Name = nf.Name
|
|
||||||
n.ID = nf.ID
|
|
||||||
n.Domain = nf.Domain
|
|
||||||
n.Host = nf.Host
|
|
||||||
n.ServerID = nf.ServerID
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
if n.ID == 0 {
|
|
||||||
err = singleton.DB.Create(&n).Error
|
|
||||||
} else {
|
|
||||||
err = singleton.DB.Save(&n).Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: fmt.Sprintf("请求错误:%s", err),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//singleton.OnNATUpdate()
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type alertRuleForm struct {
|
|
||||||
ID uint64
|
|
||||||
Name string
|
|
||||||
RulesRaw string
|
|
||||||
FailTriggerTasksRaw string // 失败时触发的任务id
|
|
||||||
RecoverTriggerTasksRaw string // 恢复时触发的任务id
|
|
||||||
NotificationTag string
|
|
||||||
TriggerMode int
|
|
||||||
Enable string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) {
|
|
||||||
var arf alertRuleForm
|
|
||||||
var r model.AlertRule
|
|
||||||
err := c.ShouldBindJSON(&arf)
|
|
||||||
if err == nil {
|
|
||||||
err = utils.Json.Unmarshal([]byte(arf.RulesRaw), &r.Rules)
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
if len(r.Rules) == 0 {
|
|
||||||
err = errors.New("至少定义一条规则")
|
|
||||||
} else {
|
|
||||||
for i := 0; i < len(r.Rules); i++ {
|
|
||||||
if !r.Rules[i].IsTransferDurationRule() {
|
|
||||||
if r.Rules[i].Duration < 3 {
|
|
||||||
err = errors.New("错误:Duration 至少为 3")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if r.Rules[i].CycleInterval < 1 {
|
|
||||||
err = errors.New("错误: cycle_interval 至少为 1")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if r.Rules[i].CycleStart == nil {
|
|
||||||
err = errors.New("错误: cycle_start 未设置")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if r.Rules[i].CycleStart.After(time.Now()) {
|
|
||||||
err = errors.New("错误: cycle_start 是个未来值")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
r.Name = arf.Name
|
|
||||||
r.RulesRaw = arf.RulesRaw
|
|
||||||
r.FailTriggerTasksRaw = arf.FailTriggerTasksRaw
|
|
||||||
r.RecoverTriggerTasksRaw = arf.RecoverTriggerTasksRaw
|
|
||||||
//r.NotificationTag = arf.NotificationTag
|
|
||||||
enable := arf.Enable == "on"
|
|
||||||
r.TriggerMode = arf.TriggerMode
|
|
||||||
r.Enable = &enable
|
|
||||||
r.ID = arf.ID
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
err = utils.Json.Unmarshal([]byte(arf.FailTriggerTasksRaw), &r.FailTriggerTasks)
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
err = utils.Json.Unmarshal([]byte(arf.RecoverTriggerTasksRaw), &r.RecoverTriggerTasks)
|
|
||||||
}
|
|
||||||
//保证NotificationTag不为空
|
|
||||||
if err == nil {
|
|
||||||
//if r.NotificationTag == "" {
|
|
||||||
// r.NotificationTag = "default"
|
|
||||||
//}
|
|
||||||
if r.ID == 0 {
|
|
||||||
err = singleton.DB.Create(&r).Error
|
|
||||||
} else {
|
|
||||||
err = singleton.DB.Save(&r).Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: fmt.Sprintf("请求错误:%s", err),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//singleton.OnRefreshOrAddAlert(r)
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type logoutForm struct {
|
|
||||||
ID uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) logout(c *gin.Context) {
|
|
||||||
admin := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User)
|
|
||||||
var lf logoutForm
|
|
||||||
if err := c.ShouldBindJSON(&lf); err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: fmt.Sprintf("请求错误:%s", err),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if lf.ID != admin.ID {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: fmt.Sprintf("请求错误:%s", "用户ID不匹配"),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
singleton.DB.Model(admin).UpdateColumns(model.User{
|
|
||||||
// Token: "",
|
|
||||||
// TokenExpired: time.Now(),
|
|
||||||
})
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
})
|
|
||||||
|
|
||||||
// if oidcLogoutUrl := singleton.Conf.Oauth2.OidcLogoutURL; oidcLogoutUrl != "" {
|
|
||||||
// // 重定向到 OIDC 退出登录地址。不知道为什么,这里的重定向不生效
|
|
||||||
// c.Redirect(http.StatusOK, oidcLogoutUrl)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
type settingForm struct {
|
|
||||||
SiteName string
|
|
||||||
Language string
|
|
||||||
CustomNameservers string
|
|
||||||
IgnoredIPNotification string
|
|
||||||
IPChangeNotificationTag string // IP变更提醒的通知组
|
|
||||||
InstallHost string
|
|
||||||
Cover uint8
|
|
||||||
|
|
||||||
EnableIPChangeNotification string
|
|
||||||
EnablePlainIPInNotification string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *memberAPI) updateSetting(c *gin.Context) {
|
|
||||||
var sf settingForm
|
|
||||||
if err := c.ShouldBind(&sf); err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: fmt.Sprintf("请求错误:%s", err),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if _, yes := model.Themes[sf.Theme]; !yes {
|
|
||||||
// c.JSON(http.StatusOK, model.Response{
|
|
||||||
// Code: http.StatusBadRequest,
|
|
||||||
// Message: fmt.Sprintf("前台主题不存在:%s", sf.Theme),
|
|
||||||
// })
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if _, yes := model.DashboardThemes[sf.DashboardTheme]; !yes {
|
|
||||||
// c.JSON(http.StatusOK, model.Response{
|
|
||||||
// Code: http.StatusBadRequest,
|
|
||||||
// Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme),
|
|
||||||
// })
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
/*
|
|
||||||
singleton.Conf.Language = sf.Language
|
|
||||||
singleton.Conf.EnableIPChangeNotification = sf.EnableIPChangeNotification == "on"
|
|
||||||
singleton.Conf.EnablePlainIPInNotification = sf.EnablePlainIPInNotification == "on"
|
|
||||||
singleton.Conf.Cover = sf.Cover
|
|
||||||
singleton.Conf.InstallHost = sf.InstallHost
|
|
||||||
singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification
|
|
||||||
singleton.Conf.IPChangeNotificationTag = sf.IPChangeNotificationTag
|
|
||||||
singleton.Conf.SiteName = sf.SiteName
|
|
||||||
singleton.Conf.DNSServers = sf.CustomNameservers
|
|
||||||
// 保证NotificationTag不为空
|
|
||||||
if singleton.Conf.IPChangeNotificationTag == "" {
|
|
||||||
singleton.Conf.IPChangeNotificationTag = "default"
|
|
||||||
}
|
|
||||||
if err := singleton.Conf.Save(); err != nil {
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: fmt.Sprintf("请求错误:%s", err),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 更新DNS服务器
|
|
||||||
singleton.OnNameserverUpdate()
|
|
||||||
c.JSON(http.StatusOK, model.Response{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/naiba/nezha/model"
|
|
||||||
"github.com/naiba/nezha/service/singleton"
|
|
||||||
)
|
|
||||||
|
|
||||||
type memberPage struct {
|
|
||||||
r *gin.Engine
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mp *memberPage) serve() {
|
|
||||||
mr := mp.r.Group("")
|
|
||||||
// mr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
|
||||||
// MemberOnly: true,
|
|
||||||
// IsPage: true,
|
|
||||||
// // Msg: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
|
|
||||||
// // Btn: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
|
|
||||||
// Redirect: "/login",
|
|
||||||
// }))
|
|
||||||
mr.GET("/cron", mp.cron)
|
|
||||||
mr.GET("/notification", mp.notification)
|
|
||||||
mr.GET("/ddns", mp.ddns)
|
|
||||||
mr.GET("/nat", mp.nat)
|
|
||||||
mr.GET("/setting", mp.setting)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mp *memberPage) cron(c *gin.Context) {
|
|
||||||
var crons []model.Cron
|
|
||||||
singleton.DB.Find(&crons)
|
|
||||||
c.HTML(http.StatusOK, "dashboard-", gin.H{
|
|
||||||
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ScheduledTasks"}),
|
|
||||||
"Crons": crons,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mp *memberPage) notification(c *gin.Context) {
|
|
||||||
var nf []model.Notification
|
|
||||||
singleton.DB.Find(&nf)
|
|
||||||
var ar []model.AlertRule
|
|
||||||
singleton.DB.Find(&ar)
|
|
||||||
c.HTML(http.StatusOK, "dashboard-", gin.H{
|
|
||||||
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Notification"}),
|
|
||||||
"Notifications": nf,
|
|
||||||
"AlertRules": ar,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mp *memberPage) ddns(c *gin.Context) {
|
|
||||||
var data []model.DDNSProfile
|
|
||||||
singleton.DB.Find(&data)
|
|
||||||
c.HTML(http.StatusOK, "dashboard-", gin.H{
|
|
||||||
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "DDNS"}),
|
|
||||||
"DDNS": data,
|
|
||||||
//"ProviderMap": model.ProviderMap,
|
|
||||||
//"ProviderList": model.ProviderList,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mp *memberPage) nat(c *gin.Context) {
|
|
||||||
var data []model.NAT
|
|
||||||
singleton.DB.Find(&data)
|
|
||||||
c.HTML(http.StatusOK, "dashboard-", gin.H{
|
|
||||||
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "NAT"}),
|
|
||||||
"NAT": data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mp *memberPage) setting(c *gin.Context) {
|
|
||||||
c.HTML(http.StatusOK, "dashboard-", gin.H{
|
|
||||||
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
|
|
||||||
//"Languages": model.Languages,
|
|
||||||
//"DashboardThemes": model.DashboardThemes,
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -93,7 +92,7 @@ func updateNAT(c *gin.Context) (any, error) {
|
|||||||
|
|
||||||
var n model.NAT
|
var n model.NAT
|
||||||
if err = singleton.DB.First(&n, id).Error; err != nil {
|
if err = singleton.DB.First(&n, id).Error; err != nil {
|
||||||
return nil, fmt.Errorf("profile id %d does not exist", id)
|
return nil, singleton.Localizer.ErrorT("profile id %d does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
n.Name = nf.Name
|
n.Name = nf.Name
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -55,8 +54,8 @@ func createNotification(c *gin.Context) (uint64, error) {
|
|||||||
n.RequestHeader = nf.RequestHeader
|
n.RequestHeader = nf.RequestHeader
|
||||||
n.RequestBody = nf.RequestBody
|
n.RequestBody = nf.RequestBody
|
||||||
n.URL = nf.URL
|
n.URL = nf.URL
|
||||||
verifySSL := nf.VerifySSL
|
verifyTLS := nf.VerifyTLS
|
||||||
n.VerifySSL = &verifySSL
|
n.VerifyTLS = &verifyTLS
|
||||||
|
|
||||||
ns := model.NotificationServerBundle{
|
ns := model.NotificationServerBundle{
|
||||||
Notification: &n,
|
Notification: &n,
|
||||||
@ -65,7 +64,7 @@ func createNotification(c *gin.Context) (uint64, error) {
|
|||||||
}
|
}
|
||||||
// 未勾选跳过检查
|
// 未勾选跳过检查
|
||||||
if !nf.SkipCheck {
|
if !nf.SkipCheck {
|
||||||
if err := ns.Send("这是测试消息"); err != nil {
|
if err := ns.Send(singleton.Localizer.T("a test message")); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,7 +103,7 @@ func updateNotification(c *gin.Context) (any, error) {
|
|||||||
|
|
||||||
var n model.Notification
|
var n model.Notification
|
||||||
if err := singleton.DB.First(&n, id).Error; err != nil {
|
if err := singleton.DB.First(&n, id).Error; err != nil {
|
||||||
return nil, fmt.Errorf("notification id %d does not exist", id)
|
return nil, singleton.Localizer.ErrorT("notification id %d does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
n.Name = nf.Name
|
n.Name = nf.Name
|
||||||
@ -113,8 +112,8 @@ func updateNotification(c *gin.Context) (any, error) {
|
|||||||
n.RequestHeader = nf.RequestHeader
|
n.RequestHeader = nf.RequestHeader
|
||||||
n.RequestBody = nf.RequestBody
|
n.RequestBody = nf.RequestBody
|
||||||
n.URL = nf.URL
|
n.URL = nf.URL
|
||||||
verifySSL := nf.VerifySSL
|
verifyTLS := nf.VerifyTLS
|
||||||
n.VerifySSL = &verifySSL
|
n.VerifyTLS = &verifyTLS
|
||||||
|
|
||||||
ns := model.NotificationServerBundle{
|
ns := model.NotificationServerBundle{
|
||||||
Notification: &n,
|
Notification: &n,
|
||||||
@ -123,7 +122,7 @@ func updateNotification(c *gin.Context) (any, error) {
|
|||||||
}
|
}
|
||||||
// 未勾选跳过检查
|
// 未勾选跳过检查
|
||||||
if !nf.SkipCheck {
|
if !nf.SkipCheck {
|
||||||
if err := ns.Send("这是测试消息"); err != nil {
|
if err := ns.Send(singleton.Localizer.T("a test message")); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -78,7 +77,7 @@ func createNotificationGroup(c *gin.Context) (uint64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if count != int64(len(ngf.Notifications)) {
|
if count != int64(len(ngf.Notifications)) {
|
||||||
return 0, fmt.Errorf("have invalid notification id")
|
return 0, singleton.Localizer.ErrorT("have invalid notification id")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
@ -129,7 +128,7 @@ func updateNotificationGroup(c *gin.Context) (any, error) {
|
|||||||
}
|
}
|
||||||
var ngDB model.NotificationGroup
|
var ngDB model.NotificationGroup
|
||||||
if err := singleton.DB.First(&ngDB, id).Error; err != nil {
|
if err := singleton.DB.First(&ngDB, id).Error; err != nil {
|
||||||
return nil, fmt.Errorf("group id %d does not exist", id)
|
return nil, singleton.Localizer.ErrorT("group id %d does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
ngDB.Name = ngf.Name
|
ngDB.Name = ngf.Name
|
||||||
@ -140,7 +139,7 @@ func updateNotificationGroup(c *gin.Context) (any, error) {
|
|||||||
return nil, newGormError("%v", err)
|
return nil, newGormError("%v", err)
|
||||||
}
|
}
|
||||||
if count != int64(len(ngf.Notifications)) {
|
if count != int64(len(ngf.Notifications)) {
|
||||||
return nil, fmt.Errorf("have invalid notification id")
|
return nil, singleton.Localizer.ErrorT("have invalid notification id")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = singleton.DB.Transaction(func(tx *gorm.DB) error {
|
err = singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -57,7 +56,7 @@ func updateServer(c *gin.Context) (any, error) {
|
|||||||
|
|
||||||
var s model.Server
|
var s model.Server
|
||||||
if err := singleton.DB.First(&s, id).Error; err != nil {
|
if err := singleton.DB.First(&s, id).Error; err != nil {
|
||||||
return nil, fmt.Errorf("server id %d does not exist", id)
|
return nil, singleton.Localizer.ErrorT("server id %d does not exist", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Name = sf.Name
|
s.Name = sf.Name
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -76,7 +75,7 @@ func createServerGroup(c *gin.Context) (uint64, error) {
|
|||||||
return 0, newGormError("%v", err)
|
return 0, newGormError("%v", err)
|
||||||
}
|
}
|
||||||
if count != int64(len(sgf.Servers)) {
|
if count != int64(len(sgf.Servers)) {
|
||||||
return 0, fmt.Errorf("have invalid server id")
|
return 0, singleton.Localizer.ErrorT("have invalid server id")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
err := singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
@ -128,7 +127,7 @@ func updateServerGroup(c *gin.Context) (any, error) {
|
|||||||
|
|
||||||
var sgDB model.ServerGroup
|
var sgDB model.ServerGroup
|
||||||
if err := singleton.DB.First(&sgDB, id).Error; err != nil {
|
if err := singleton.DB.First(&sgDB, id).Error; err != nil {
|
||||||
return nil, fmt.Errorf("group id %d does not exist", id)
|
return nil, singleton.Localizer.ErrorT("group id %d does not exist", id)
|
||||||
}
|
}
|
||||||
sgDB.Name = sg.Name
|
sgDB.Name = sg.Name
|
||||||
|
|
||||||
@ -137,7 +136,7 @@ func updateServerGroup(c *gin.Context) (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if count != int64(len(sg.Servers)) {
|
if count != int64(len(sg.Servers)) {
|
||||||
return nil, fmt.Errorf("have invalid server id")
|
return nil, singleton.Localizer.ErrorT("have invalid server id")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = singleton.DB.Transaction(func(tx *gorm.DB) error {
|
err = singleton.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -78,14 +76,14 @@ func listServiceHistory(c *gin.Context) ([]*model.ServiceInfos, error) {
|
|||||||
singleton.ServerLock.RLock()
|
singleton.ServerLock.RLock()
|
||||||
server, ok := singleton.ServerList[id]
|
server, ok := singleton.ServerList[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("server not found")
|
return nil, singleton.Localizer.ErrorT("server not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||||
authorized := isMember // TODO || isViewPasswordVerfied
|
authorized := isMember // TODO || isViewPasswordVerfied
|
||||||
|
|
||||||
if server.HideForGuest && !authorized {
|
if server.HideForGuest && !authorized {
|
||||||
return nil, errors.New("unauthorized")
|
return nil, singleton.Localizer.ErrorT("unauthorized")
|
||||||
}
|
}
|
||||||
singleton.ServerLock.RUnlock()
|
singleton.ServerLock.RUnlock()
|
||||||
|
|
||||||
@ -154,7 +152,7 @@ func listServerWithServices(c *gin.Context) ([]uint64, error) {
|
|||||||
server, ok := singleton.ServerList[id]
|
server, ok := singleton.ServerList[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
singleton.ServerLock.RUnlock()
|
singleton.ServerLock.RUnlock()
|
||||||
return nil, errors.New("server not found")
|
return nil, singleton.Localizer.ErrorT("server not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !server.HideForGuest || authorized {
|
if !server.HideForGuest || authorized {
|
||||||
@ -201,7 +199,7 @@ func createService(c *gin.Context) (uint64, error) {
|
|||||||
m.FailTriggerTasks = mf.FailTriggerTasks
|
m.FailTriggerTasks = mf.FailTriggerTasks
|
||||||
|
|
||||||
if err := singleton.DB.Create(&m).Error; err != nil {
|
if err := singleton.DB.Create(&m).Error; err != nil {
|
||||||
return 0, err
|
return 0, newGormError("%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var skipServers []uint64
|
var skipServers []uint64
|
||||||
@ -246,7 +244,7 @@ func updateService(c *gin.Context) (any, error) {
|
|||||||
}
|
}
|
||||||
var m model.Service
|
var m model.Service
|
||||||
if err := singleton.DB.First(&m, id).Error; err != nil {
|
if err := singleton.DB.First(&m, id).Error; err != nil {
|
||||||
return nil, fmt.Errorf("service id %d does not exist", id)
|
return nil, singleton.Localizer.ErrorT("service id %d does not exist", id)
|
||||||
}
|
}
|
||||||
m.Name = mf.Name
|
m.Name = mf.Name
|
||||||
m.Target = strings.TrimSpace(mf.Target)
|
m.Target = strings.TrimSpace(mf.Target)
|
||||||
@ -265,7 +263,7 @@ func updateService(c *gin.Context) (any, error) {
|
|||||||
m.FailTriggerTasks = mf.FailTriggerTasks
|
m.FailTriggerTasks = mf.FailTriggerTasks
|
||||||
|
|
||||||
if err := singleton.DB.Save(&m).Error; err != nil {
|
if err := singleton.DB.Save(&m).Error; err != nil {
|
||||||
return nil, err
|
return nil, newGormError("%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var skipServers []uint64
|
var skipServers []uint64
|
||||||
|
@ -63,9 +63,10 @@ func updateConfig(c *gin.Context) (any, error) {
|
|||||||
singleton.Conf.CustomCodeDashboard = sf.CustomCodeDashboard
|
singleton.Conf.CustomCodeDashboard = sf.CustomCodeDashboard
|
||||||
|
|
||||||
if err := singleton.Conf.Save(); err != nil {
|
if err := singleton.Conf.Save(); err != nil {
|
||||||
return nil, err
|
return nil, newGormError("%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
singleton.OnNameserverUpdate()
|
singleton.OnNameserverUpdate()
|
||||||
|
singleton.OnUpdateLang(singleton.Conf.Language)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -41,7 +40,7 @@ func createTerminal(c *gin.Context) (*model.CreateTerminalResponse, error) {
|
|||||||
server := singleton.ServerList[createTerminalReq.ServerID]
|
server := singleton.ServerList[createTerminalReq.ServerID]
|
||||||
singleton.ServerLock.RUnlock()
|
singleton.ServerLock.RUnlock()
|
||||||
if server == nil || server.TaskStream == nil {
|
if server == nil || server.TaskStream == nil {
|
||||||
return nil, errors.New("server not found or not connected")
|
return nil, singleton.Localizer.ErrorT("server not found or not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
|
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
"github.com/naiba/nezha/service/singleton"
|
"github.com/naiba/nezha/service/singleton"
|
||||||
@ -44,10 +42,10 @@ func createUser(c *gin.Context) (uint64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(uf.Password) < 6 {
|
if len(uf.Password) < 6 {
|
||||||
return 0, errors.New("password length must be greater than 6")
|
return 0, singleton.Localizer.ErrorT("password length must be greater than 6")
|
||||||
}
|
}
|
||||||
if uf.Username == "" {
|
if uf.Username == "" {
|
||||||
return 0, errors.New("username can't be empty")
|
return 0, singleton.Localizer.ErrorT("username can't be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
var u model.User
|
var u model.User
|
||||||
|
1
go.mod
1
go.mod
@ -6,6 +6,7 @@ toolchain go1.23.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/appleboy/gin-jwt/v2 v2.10.0
|
github.com/appleboy/gin-jwt/v2 v2.10.0
|
||||||
|
github.com/chai2010/gettext-go v1.0.3
|
||||||
github.com/gin-contrib/pprof v1.4.0
|
github.com/gin-contrib/pprof v1.4.0
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.1
|
||||||
|
2
go.sum
2
go.sum
@ -13,6 +13,8 @@ github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKz
|
|||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
||||||
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80=
|
||||||
|
github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
@ -18,7 +18,7 @@ const (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Debug bool `mapstructure:"debug" json:"debug,omitempty"` // debug模式开关
|
Debug bool `mapstructure:"debug" json:"debug,omitempty"` // debug模式开关
|
||||||
|
|
||||||
Language string `mapstructure:"language" json:"language,omitempty"` // 系统语言,默认 zh-CN
|
Language string `mapstructure:"language" json:"language,omitempty"` // 系统语言,默认 zh_CN
|
||||||
SiteName string `mapstructure:"site_name" json:"site_name,omitempty"`
|
SiteName string `mapstructure:"site_name" json:"site_name,omitempty"`
|
||||||
JWTSecretKey string `mapstructure:"jwt_secret_key" json:"jwt_secret_key,omitempty"`
|
JWTSecretKey string `mapstructure:"jwt_secret_key" json:"jwt_secret_key,omitempty"`
|
||||||
AgentSecretKey string `mapstructure:"agent_secret_key" json:"agent_secret_key,omitempty"`
|
AgentSecretKey string `mapstructure:"agent_secret_key" json:"agent_secret_key,omitempty"`
|
||||||
@ -62,6 +62,9 @@ func (c *Config) Read(path string) error {
|
|||||||
if c.ListenPort == 0 {
|
if c.ListenPort == 0 {
|
||||||
c.ListenPort = 8008
|
c.ListenPort = 8008
|
||||||
}
|
}
|
||||||
|
if c.Language == "" {
|
||||||
|
c.Language = "zh_CN"
|
||||||
|
}
|
||||||
if c.Location == "" {
|
if c.Location == "" {
|
||||||
c.Location = "Asia/Shanghai"
|
c.Location = "Asia/Shanghai"
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ type Notification struct {
|
|||||||
RequestType int `json:"request_type,omitempty"`
|
RequestType int `json:"request_type,omitempty"`
|
||||||
RequestHeader string `json:"request_header,omitempty" gorm:"type:longtext"`
|
RequestHeader string `json:"request_header,omitempty" gorm:"type:longtext"`
|
||||||
RequestBody string `json:"request_body,omitempty" gorm:"type:longtext"`
|
RequestBody string `json:"request_body,omitempty" gorm:"type:longtext"`
|
||||||
VerifySSL *bool `json:"verify_ssl,omitempty"`
|
VerifyTLS *bool `json:"verify_tls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *NotificationServerBundle) reqURL(message string) string {
|
func (ns *NotificationServerBundle) reqURL(message string) string {
|
||||||
@ -111,7 +111,7 @@ func (n *Notification) setRequestHeader(req *http.Request) error {
|
|||||||
func (ns *NotificationServerBundle) Send(message string) error {
|
func (ns *NotificationServerBundle) Send(message string) error {
|
||||||
var client *http.Client
|
var client *http.Client
|
||||||
n := ns.Notification
|
n := ns.Notification
|
||||||
if n.VerifySSL != nil && *n.VerifySSL {
|
if n.VerifyTLS != nil && *n.VerifyTLS {
|
||||||
client = utils.HttpClient
|
client = utils.HttpClient
|
||||||
} else {
|
} else {
|
||||||
client = utils.HttpClientSkipTlsVerify
|
client = utils.HttpClientSkipTlsVerify
|
||||||
|
@ -7,6 +7,6 @@ type NotificationForm struct {
|
|||||||
RequestType int `json:"request_type,omitempty"`
|
RequestType int `json:"request_type,omitempty"`
|
||||||
RequestHeader string `json:"request_header,omitempty"`
|
RequestHeader string `json:"request_header,omitempty"`
|
||||||
RequestBody string `json:"request_body,omitempty"`
|
RequestBody string `json:"request_body,omitempty"`
|
||||||
VerifySSL bool `json:"verify_ssl,omitempty"`
|
VerifyTLS bool `json:"verify_tls,omitempty"`
|
||||||
SkipCheck bool `json:"skip_check,omitempty"`
|
SkipCheck bool `json:"skip_check,omitempty"`
|
||||||
}
|
}
|
||||||
|
105
pkg/i18n/i18n.go
Normal file
105
pkg/i18n/i18n.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/chai2010/gettext-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed translations
|
||||||
|
var Translations embed.FS
|
||||||
|
|
||||||
|
var Languages = map[string]string{
|
||||||
|
"zh_CN": "简体中文",
|
||||||
|
"zh_TW": "繁體中文",
|
||||||
|
"en_US": "English",
|
||||||
|
"es_ES": "Español",
|
||||||
|
}
|
||||||
|
|
||||||
|
type Localizer struct {
|
||||||
|
intlMap map[string]gettext.Gettexter
|
||||||
|
lang string
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocalizer(lang, domain, path string, data any) *Localizer {
|
||||||
|
intl := gettext.New(domain, path, data)
|
||||||
|
intl.SetLanguage(lang)
|
||||||
|
|
||||||
|
intlMap := make(map[string]gettext.Gettexter)
|
||||||
|
intlMap[lang] = intl
|
||||||
|
|
||||||
|
return &Localizer{intlMap: intlMap, lang: lang}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Localizer) SetLanguage(lang string) {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
l.lang = lang
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Localizer) Exists(lang string) bool {
|
||||||
|
l.mu.RLock()
|
||||||
|
defer l.mu.RUnlock()
|
||||||
|
|
||||||
|
if _, ok := l.intlMap[lang]; ok {
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Localizer) AppendIntl(lang, domain, path string, data any) {
|
||||||
|
intl := gettext.New(domain, path, data)
|
||||||
|
intl.SetLanguage(lang)
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
l.intlMap[lang] = intl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modified from k8s.io/kubectl/pkg/util/i18n
|
||||||
|
|
||||||
|
func (l *Localizer) T(orig string) string {
|
||||||
|
l.mu.RLock()
|
||||||
|
intl, ok := l.intlMap[l.lang]
|
||||||
|
l.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return orig
|
||||||
|
}
|
||||||
|
|
||||||
|
return intl.PGettext("", orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// N translates a string, possibly substituting arguments into it along
|
||||||
|
// the way. If len(args) is > 0, args1 is assumed to be the plural value
|
||||||
|
// and plural translation is used.
|
||||||
|
func (l *Localizer) N(orig string, args ...int) string {
|
||||||
|
l.mu.RLock()
|
||||||
|
intl, ok := l.intlMap[l.lang]
|
||||||
|
l.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return orig
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
return intl.PGettext("", orig)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(intl.PNGettext("", orig, orig+".plural", args[0]),
|
||||||
|
args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorT produces an error with a translated error string.
|
||||||
|
// Substitution is performed via the `T` function above, following
|
||||||
|
// the same rules.
|
||||||
|
func (l *Localizer) ErrorT(defaultValue string, args ...any) error {
|
||||||
|
return fmt.Errorf(l.T(defaultValue), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Localizer) Tf(defaultValue string, args ...any) string {
|
||||||
|
return fmt.Sprintf(l.T(defaultValue), args...)
|
||||||
|
}
|
220
pkg/i18n/template.pot
Normal file
220
pkg/i18n/template.pot
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-11-01 13:32+0800\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=CHARSET\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:100
|
||||||
|
#, c-format
|
||||||
|
msgid "alert id %d does not exist"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:155
|
||||||
|
msgid "duration need to be at least 3"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:159
|
||||||
|
msgid "cycle_interval need to be at least 1"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:162
|
||||||
|
msgid "cycle_start is not set"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:165
|
||||||
|
msgid "cycle_start is a future value"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:170
|
||||||
|
msgid "need to configure at least a single rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/controller.go:188
|
||||||
|
msgid "database error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122
|
||||||
|
msgid "scheduled tasks cannot be triggered by alarms"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/cron.go:161
|
||||||
|
#, c-format
|
||||||
|
msgid "task id %d does not exist"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120
|
||||||
|
msgid "the retry count must be an integer between 1 and 10"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148
|
||||||
|
msgid "error parsing %s: %v"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95
|
||||||
|
#, c-format
|
||||||
|
msgid "profile id %d does not exist"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43
|
||||||
|
msgid "server not found or not connected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification.go:67
|
||||||
|
#: cmd/dashboard/controller/notification.go:125
|
||||||
|
msgid "a test message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification.go:106
|
||||||
|
#, c-format
|
||||||
|
msgid "notification id %d does not exist"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:80
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:142
|
||||||
|
msgid "have invalid notification id"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:131
|
||||||
|
#: cmd/dashboard/controller/server_group.go:130
|
||||||
|
#, c-format
|
||||||
|
msgid "group id %d does not exist"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/server.go:59
|
||||||
|
#, c-format
|
||||||
|
msgid "server id %d does not exist"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/server_group.go:78
|
||||||
|
#: cmd/dashboard/controller/server_group.go:139
|
||||||
|
msgid "have invalid server id"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:79
|
||||||
|
#: cmd/dashboard/controller/service.go:155
|
||||||
|
msgid "server not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:86
|
||||||
|
msgid "unauthorized"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:247
|
||||||
|
#, c-format
|
||||||
|
msgid "service id %d does not exist"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/user.go:45
|
||||||
|
msgid "password length must be greater than 6"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/user.go:48
|
||||||
|
msgid "username can't be empty"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:122
|
||||||
|
msgid "timeout: no connection established"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:125
|
||||||
|
msgid "timeout: user connection not established"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:128
|
||||||
|
msgid "timeout: agent connection not established"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:57
|
||||||
|
msgid "Scheduled Task Executed Successfully"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:61
|
||||||
|
msgid "Scheduled Task Executed Failed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:156
|
||||||
|
msgid "IP Changed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/alertsentinel.go:159
|
||||||
|
msgid "Incident"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/alertsentinel.go:169
|
||||||
|
msgid "Resolved"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:52
|
||||||
|
msgid "Tasks failed to register: ["
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:59
|
||||||
|
msgid ""
|
||||||
|
"] These tasks will not execute properly. Fix them in the admin dashboard."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:150 service/singleton/crontask.go:175
|
||||||
|
#, c-format
|
||||||
|
msgid "[Task failed] %s: server %s is offline and cannot execute the task"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:439
|
||||||
|
#, c-format
|
||||||
|
msgid "[Latency] %s %2f > %2f, Reporter: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:446
|
||||||
|
#, c-format
|
||||||
|
msgid "[Latency] %s %2f < %2f, Reporter: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:472
|
||||||
|
#, c-format
|
||||||
|
msgid "[%s] %s Reporter: %s, Error: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:515
|
||||||
|
#, c-format
|
||||||
|
msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:555
|
||||||
|
#, c-format
|
||||||
|
msgid "The TLS certificate will expire within seven days. Expiration time: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:568
|
||||||
|
#, c-format
|
||||||
|
msgid ""
|
||||||
|
"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, "
|
||||||
|
"expires at %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:604
|
||||||
|
msgid "No Data"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:606
|
||||||
|
msgid "Good"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:608
|
||||||
|
msgid "Low Availability"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:610
|
||||||
|
msgid "Down"
|
||||||
|
msgstr ""
|
BIN
pkg/i18n/translations/en_US/LC_MESSAGES/nezha.mo
Normal file
BIN
pkg/i18n/translations/en_US/LC_MESSAGES/nezha.mo
Normal file
Binary file not shown.
224
pkg/i18n/translations/en_US/LC_MESSAGES/nezha.po
Normal file
224
pkg/i18n/translations/en_US/LC_MESSAGES/nezha.po
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: \n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-11-01 13:14+0800\n"
|
||||||
|
"PO-Revision-Date: 2024-11-01 13:20+0800\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Language: en_US\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Poedit 3.5\n"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:100
|
||||||
|
#, c-format
|
||||||
|
msgid "alert id %d does not exist"
|
||||||
|
msgstr "alert id %d does not exist"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:155
|
||||||
|
msgid "duration need to be at least 3"
|
||||||
|
msgstr "duration need to be at least 3"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:159
|
||||||
|
msgid "cycle_interval need to be at least 1"
|
||||||
|
msgstr "cycle_interval need to be at least 1"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:162
|
||||||
|
msgid "cycle_start is not set"
|
||||||
|
msgstr "cycle_start is not set"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:165
|
||||||
|
msgid "cycle_start is a future value"
|
||||||
|
msgstr "cycle_start is a future value"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:170
|
||||||
|
msgid "need to configure at least a single rule"
|
||||||
|
msgstr "need to configure at least a single rule"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/controller.go:188
|
||||||
|
msgid "database error"
|
||||||
|
msgstr "database error"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122
|
||||||
|
msgid "scheduled tasks cannot be triggered by alarms"
|
||||||
|
msgstr "scheduled tasks cannot be triggered by alarms"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/cron.go:161
|
||||||
|
#, c-format
|
||||||
|
msgid "task id %d does not exist"
|
||||||
|
msgstr "task id %d does not exist"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120
|
||||||
|
msgid "the retry count must be an integer between 1 and 10"
|
||||||
|
msgstr "the retry count must be an integer between 1 and 10"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148
|
||||||
|
msgid "error parsing %s: %v"
|
||||||
|
msgstr "error parsing %s: %v"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95
|
||||||
|
#, c-format
|
||||||
|
msgid "profile id %d does not exist"
|
||||||
|
msgstr "profile id %d does not exist"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43
|
||||||
|
msgid "server not found or not connected"
|
||||||
|
msgstr "server not found or not connected"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification.go:67
|
||||||
|
#: cmd/dashboard/controller/notification.go:125
|
||||||
|
msgid "a test message"
|
||||||
|
msgstr "a test message"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification.go:106
|
||||||
|
#, c-format
|
||||||
|
msgid "notification id %d does not exist"
|
||||||
|
msgstr "notification id %d does not exist"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:80
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:142
|
||||||
|
msgid "have invalid notification id"
|
||||||
|
msgstr "have invalid notification id"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:131
|
||||||
|
#: cmd/dashboard/controller/server_group.go:130
|
||||||
|
#, c-format
|
||||||
|
msgid "group id %d does not exist"
|
||||||
|
msgstr "group id %d does not exist"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/server.go:59
|
||||||
|
#, c-format
|
||||||
|
msgid "server id %d does not exist"
|
||||||
|
msgstr "server id %d does not exist"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/server_group.go:78
|
||||||
|
#: cmd/dashboard/controller/server_group.go:139
|
||||||
|
msgid "have invalid server id"
|
||||||
|
msgstr "have invalid server id"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:79
|
||||||
|
#: cmd/dashboard/controller/service.go:155
|
||||||
|
msgid "server not found"
|
||||||
|
msgstr "server not found"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:86
|
||||||
|
msgid "unauthorized"
|
||||||
|
msgstr "unauthorized"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:247
|
||||||
|
#, c-format
|
||||||
|
msgid "service id %d does not exist"
|
||||||
|
msgstr "service id %d does not exist"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/user.go:45
|
||||||
|
msgid "password length must be greater than 6"
|
||||||
|
msgstr "password length must be greater than 6"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/user.go:48
|
||||||
|
msgid "username can't be empty"
|
||||||
|
msgstr "username can't be empty"
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:122
|
||||||
|
msgid "timeout: no connection established"
|
||||||
|
msgstr "timeout: no connection established"
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:125
|
||||||
|
msgid "timeout: user connection not established"
|
||||||
|
msgstr "timeout: user connection not established"
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:128
|
||||||
|
msgid "timeout: agent connection not established"
|
||||||
|
msgstr "timeout: agent connection not established"
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:57
|
||||||
|
msgid "Scheduled Task Executed Successfully"
|
||||||
|
msgstr "Scheduled Task Executed Successfully"
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:61
|
||||||
|
msgid "Scheduled Task Executed Failed"
|
||||||
|
msgstr "Scheduled Task Executed Failed"
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:156
|
||||||
|
msgid "IP Changed"
|
||||||
|
msgstr "IP Changed"
|
||||||
|
|
||||||
|
#: service/singleton/alertsentinel.go:159
|
||||||
|
msgid "Incident"
|
||||||
|
msgstr "Incident"
|
||||||
|
|
||||||
|
#: service/singleton/alertsentinel.go:169
|
||||||
|
msgid "Resolved"
|
||||||
|
msgstr "Resolved"
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:52
|
||||||
|
msgid "Tasks failed to register: ["
|
||||||
|
msgstr "Tasks failed to register: ["
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:59
|
||||||
|
msgid ""
|
||||||
|
"] These tasks will not execute properly. Fix them in the admin dashboard."
|
||||||
|
msgstr ""
|
||||||
|
"] These tasks will not execute properly. Fix them in the admin dashboard."
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:150 service/singleton/crontask.go:175
|
||||||
|
#, c-format
|
||||||
|
msgid "[Task failed] %s: server %s is offline and cannot execute the task"
|
||||||
|
msgstr "[Task failed] %s: server %s is offline and cannot execute the task"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:439
|
||||||
|
#, c-format
|
||||||
|
msgid "[Latency] %s %2f > %2f, Reporter: %s"
|
||||||
|
msgstr "[Latency] %s %2f > %2f, Reporter: %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:446
|
||||||
|
#, c-format
|
||||||
|
msgid "[Latency] %s %2f < %2f, Reporter: %s"
|
||||||
|
msgstr "[Latency] %s %2f < %2f, Reporter: %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:472
|
||||||
|
#, c-format
|
||||||
|
msgid "[%s] %s Reporter: %s, Error: %s"
|
||||||
|
msgstr "[%s] %s Reporter: %s, Error: %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:515
|
||||||
|
#, c-format
|
||||||
|
msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s"
|
||||||
|
msgstr "[TLS] Fetch cert info failed, Reporter: %s, Error: %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:555
|
||||||
|
#, c-format
|
||||||
|
msgid "The TLS certificate will expire within seven days. Expiration time: %s"
|
||||||
|
msgstr "The TLS certificate will expire within seven days. Expiration time: %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:568
|
||||||
|
#, c-format
|
||||||
|
msgid ""
|
||||||
|
"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, "
|
||||||
|
"expires at %s"
|
||||||
|
msgstr ""
|
||||||
|
"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, "
|
||||||
|
"expires at %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:604
|
||||||
|
msgid "No Data"
|
||||||
|
msgstr "No Data"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:606
|
||||||
|
msgid "Good"
|
||||||
|
msgstr "Good"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:608
|
||||||
|
msgid "Low Availability"
|
||||||
|
msgstr "Low Availability"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:610
|
||||||
|
msgid "Down"
|
||||||
|
msgstr "Down"
|
BIN
pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.mo
Normal file
BIN
pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.mo
Normal file
Binary file not shown.
222
pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.po
Normal file
222
pkg/i18n/translations/zh_CN/LC_MESSAGES/nezha.po
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: \n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-11-01 13:14+0800\n"
|
||||||
|
"PO-Revision-Date: 2024-11-01 13:20+0800\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Language: zh_CN\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Poedit 3.5\n"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:100
|
||||||
|
#, c-format
|
||||||
|
msgid "alert id %d does not exist"
|
||||||
|
msgstr "告警 ID %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:155
|
||||||
|
msgid "duration need to be at least 3"
|
||||||
|
msgstr "duration 至少为 3"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:159
|
||||||
|
msgid "cycle_interval need to be at least 1"
|
||||||
|
msgstr "cycle_interval 至少为 1"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:162
|
||||||
|
msgid "cycle_start is not set"
|
||||||
|
msgstr "cycle_start 未设置"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:165
|
||||||
|
msgid "cycle_start is a future value"
|
||||||
|
msgstr "cycle_start 是未来值"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:170
|
||||||
|
msgid "need to configure at least a single rule"
|
||||||
|
msgstr "需要至少定义一条规则"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/controller.go:188
|
||||||
|
msgid "database error"
|
||||||
|
msgstr "数据库错误"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122
|
||||||
|
msgid "scheduled tasks cannot be triggered by alarms"
|
||||||
|
msgstr "计划任务不能被告警触发"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/cron.go:161
|
||||||
|
#, c-format
|
||||||
|
msgid "task id %d does not exist"
|
||||||
|
msgstr "任务 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120
|
||||||
|
msgid "the retry count must be an integer between 1 and 10"
|
||||||
|
msgstr "重试次数必须为大于 1 且不超过 10 的整数"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148
|
||||||
|
msgid "error parsing %s: %v"
|
||||||
|
msgstr "解析 %s 时发生错误:%v"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95
|
||||||
|
#, c-format
|
||||||
|
msgid "profile id %d does not exist"
|
||||||
|
msgstr "配置 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43
|
||||||
|
msgid "server not found or not connected"
|
||||||
|
msgstr "服务器未找到或仍未连接"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification.go:67
|
||||||
|
#: cmd/dashboard/controller/notification.go:125
|
||||||
|
msgid "a test message"
|
||||||
|
msgstr "一条测试信息"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification.go:106
|
||||||
|
#, c-format
|
||||||
|
msgid "notification id %d does not exist"
|
||||||
|
msgstr "通知方式 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:80
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:142
|
||||||
|
msgid "have invalid notification id"
|
||||||
|
msgstr "通知方式 id 无效"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:131
|
||||||
|
#: cmd/dashboard/controller/server_group.go:130
|
||||||
|
#, c-format
|
||||||
|
msgid "group id %d does not exist"
|
||||||
|
msgstr "组 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/server.go:59
|
||||||
|
#, c-format
|
||||||
|
msgid "server id %d does not exist"
|
||||||
|
msgstr "服务器 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/server_group.go:78
|
||||||
|
#: cmd/dashboard/controller/server_group.go:139
|
||||||
|
msgid "have invalid server id"
|
||||||
|
msgstr "服务器 id 无效"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:79
|
||||||
|
#: cmd/dashboard/controller/service.go:155
|
||||||
|
msgid "server not found"
|
||||||
|
msgstr "未找到服务器"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:86
|
||||||
|
msgid "unauthorized"
|
||||||
|
msgstr "未授权"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:247
|
||||||
|
#, c-format
|
||||||
|
msgid "service id %d does not exist"
|
||||||
|
msgstr "服务 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/user.go:45
|
||||||
|
msgid "password length must be greater than 6"
|
||||||
|
msgstr "密码长度必须大于 6"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/user.go:48
|
||||||
|
msgid "username can't be empty"
|
||||||
|
msgstr "用户名不能为空"
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:122
|
||||||
|
msgid "timeout: no connection established"
|
||||||
|
msgstr "超时:无连接建立"
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:125
|
||||||
|
msgid "timeout: user connection not established"
|
||||||
|
msgstr "超时:用户连接未建立"
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:128
|
||||||
|
msgid "timeout: agent connection not established"
|
||||||
|
msgstr "超时:agent 连接未建立"
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:57
|
||||||
|
msgid "Scheduled Task Executed Successfully"
|
||||||
|
msgstr "计划任务执行成功"
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:61
|
||||||
|
msgid "Scheduled Task Executed Failed"
|
||||||
|
msgstr "计划任务执行失败"
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:156
|
||||||
|
msgid "IP Changed"
|
||||||
|
msgstr "IP 变更"
|
||||||
|
|
||||||
|
#: service/singleton/alertsentinel.go:159
|
||||||
|
msgid "Incident"
|
||||||
|
msgstr "事件"
|
||||||
|
|
||||||
|
#: service/singleton/alertsentinel.go:169
|
||||||
|
msgid "Resolved"
|
||||||
|
msgstr "恢复"
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:52
|
||||||
|
msgid "Tasks failed to register: ["
|
||||||
|
msgstr "注册失败的任务:["
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:59
|
||||||
|
msgid ""
|
||||||
|
"] These tasks will not execute properly. Fix them in the admin dashboard."
|
||||||
|
msgstr "这些任务将无法正常执行,请进入后台重新修改保存。"
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:150 service/singleton/crontask.go:175
|
||||||
|
#, c-format
|
||||||
|
msgid "[Task failed] %s: server %s is offline and cannot execute the task"
|
||||||
|
msgstr "[任务失败] %s,服务器 %s 离线,无法执行"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:439
|
||||||
|
#, c-format
|
||||||
|
msgid "[Latency] %s %2f > %2f, Reporter: %s"
|
||||||
|
msgstr "[延迟告警] %s %2f > %2f,报告服务: %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:446
|
||||||
|
#, c-format
|
||||||
|
msgid "[Latency] %s %2f < %2f, Reporter: %s"
|
||||||
|
msgstr "[延迟告警] %s %2f < %2f,报告服务: %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:472
|
||||||
|
#, c-format
|
||||||
|
msgid "[%s] %s Reporter: %s, Error: %s"
|
||||||
|
msgstr "[%s] %s 报告服务:%s,错误信息:%s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:515
|
||||||
|
#, c-format
|
||||||
|
msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s"
|
||||||
|
msgstr "[TLS] 获取证书信息失败,报告服务:%s,错误信息:%s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:555
|
||||||
|
#, c-format
|
||||||
|
msgid "The TLS certificate will expire within seven days. Expiration time: %s"
|
||||||
|
msgstr "TLS 证书将在 7 天内过期。过期时间为:%s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:568
|
||||||
|
#, c-format
|
||||||
|
msgid ""
|
||||||
|
"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, "
|
||||||
|
"expires at %s"
|
||||||
|
msgstr ""
|
||||||
|
"TLS 证书发生更改,旧值:颁发者 %s,过期日 %s;新值:颁发者 %s,过期日 %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:604
|
||||||
|
msgid "No Data"
|
||||||
|
msgstr "无数据"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:606
|
||||||
|
msgid "Good"
|
||||||
|
msgstr "正常"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:608
|
||||||
|
msgid "Low Availability"
|
||||||
|
msgstr "低可用"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:610
|
||||||
|
msgid "Down"
|
||||||
|
msgstr "故障"
|
BIN
pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.mo
Normal file
BIN
pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.mo
Normal file
Binary file not shown.
222
pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.po
Normal file
222
pkg/i18n/translations/zh_TW/LC_MESSAGES/nezha.po
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: \n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-11-01 13:14+0800\n"
|
||||||
|
"PO-Revision-Date: 2024-11-01 13:19+0800\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Language: zh_TW\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Poedit 3.5\n"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:100
|
||||||
|
#, c-format
|
||||||
|
msgid "alert id %d does not exist"
|
||||||
|
msgstr "告警 ID %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:155
|
||||||
|
msgid "duration need to be at least 3"
|
||||||
|
msgstr "duration 至少為 3"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:159
|
||||||
|
msgid "cycle_interval need to be at least 1"
|
||||||
|
msgstr "cycle_interval 至少為 1"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:162
|
||||||
|
msgid "cycle_start is not set"
|
||||||
|
msgstr "cycle_start 未設定"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:165
|
||||||
|
msgid "cycle_start is a future value"
|
||||||
|
msgstr "cycle_start 是未來值"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/alertrule.go:170
|
||||||
|
msgid "need to configure at least a single rule"
|
||||||
|
msgstr "需要至少定義一條規則"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/controller.go:188
|
||||||
|
msgid "database error"
|
||||||
|
msgstr "資料庫錯誤"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/cron.go:63 cmd/dashboard/controller/cron.go:122
|
||||||
|
msgid "scheduled tasks cannot be triggered by alarms"
|
||||||
|
msgstr "排程任務不能被告警觸發"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/cron.go:161
|
||||||
|
#, c-format
|
||||||
|
msgid "task id %d does not exist"
|
||||||
|
msgstr "任務 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:120
|
||||||
|
msgid "the retry count must be an integer between 1 and 10"
|
||||||
|
msgstr "重試次數必須為大於 1 且不超過 10 的整數"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:79 cmd/dashboard/controller/ddns.go:148
|
||||||
|
msgid "error parsing %s: %v"
|
||||||
|
msgstr "解析 %s 時發生錯誤:%v"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/ddns.go:125 cmd/dashboard/controller/nat.go:95
|
||||||
|
#, c-format
|
||||||
|
msgid "profile id %d does not exist"
|
||||||
|
msgstr "配置 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/fm.go:45 cmd/dashboard/controller/terminal.go:43
|
||||||
|
msgid "server not found or not connected"
|
||||||
|
msgstr "伺服器未找到或仍未連線"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification.go:67
|
||||||
|
#: cmd/dashboard/controller/notification.go:125
|
||||||
|
msgid "a test message"
|
||||||
|
msgstr "一條測試資訊"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification.go:106
|
||||||
|
#, c-format
|
||||||
|
msgid "notification id %d does not exist"
|
||||||
|
msgstr "通知方式 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:80
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:142
|
||||||
|
msgid "have invalid notification id"
|
||||||
|
msgstr "通知方式 id 無效"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/notification_group.go:131
|
||||||
|
#: cmd/dashboard/controller/server_group.go:130
|
||||||
|
#, c-format
|
||||||
|
msgid "group id %d does not exist"
|
||||||
|
msgstr "組 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/server.go:59
|
||||||
|
#, c-format
|
||||||
|
msgid "server id %d does not exist"
|
||||||
|
msgstr "伺服器 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/server_group.go:78
|
||||||
|
#: cmd/dashboard/controller/server_group.go:139
|
||||||
|
msgid "have invalid server id"
|
||||||
|
msgstr "伺服器 id 無效"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:79
|
||||||
|
#: cmd/dashboard/controller/service.go:155
|
||||||
|
msgid "server not found"
|
||||||
|
msgstr "未找到伺服器"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:86
|
||||||
|
msgid "unauthorized"
|
||||||
|
msgstr "未授權"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/service.go:247
|
||||||
|
#, c-format
|
||||||
|
msgid "service id %d does not exist"
|
||||||
|
msgstr "服務 id %d 不存在"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/user.go:45
|
||||||
|
msgid "password length must be greater than 6"
|
||||||
|
msgstr "密碼長度必須大於 6"
|
||||||
|
|
||||||
|
#: cmd/dashboard/controller/user.go:48
|
||||||
|
msgid "username can't be empty"
|
||||||
|
msgstr "使用者名稱不能為空"
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:122
|
||||||
|
msgid "timeout: no connection established"
|
||||||
|
msgstr "超時:無連線建立"
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:125
|
||||||
|
msgid "timeout: user connection not established"
|
||||||
|
msgstr "超時:使用者連線未建立"
|
||||||
|
|
||||||
|
#: service/rpc/io_stream.go:128
|
||||||
|
msgid "timeout: agent connection not established"
|
||||||
|
msgstr "超時:agent 連線未建立"
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:57
|
||||||
|
msgid "Scheduled Task Executed Successfully"
|
||||||
|
msgstr "排程任務執行成功"
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:61
|
||||||
|
msgid "Scheduled Task Executed Failed"
|
||||||
|
msgstr "排程任務執行失敗"
|
||||||
|
|
||||||
|
#: service/rpc/nezha.go:156
|
||||||
|
msgid "IP Changed"
|
||||||
|
msgstr "IP 變更"
|
||||||
|
|
||||||
|
#: service/singleton/alertsentinel.go:159
|
||||||
|
msgid "Incident"
|
||||||
|
msgstr "事件"
|
||||||
|
|
||||||
|
#: service/singleton/alertsentinel.go:169
|
||||||
|
msgid "Resolved"
|
||||||
|
msgstr "恢復"
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:52
|
||||||
|
msgid "Tasks failed to register: ["
|
||||||
|
msgstr "註冊失敗的任務:["
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:59
|
||||||
|
msgid ""
|
||||||
|
"] These tasks will not execute properly. Fix them in the admin dashboard."
|
||||||
|
msgstr "這些任務將無法正常執行,請進入後台重新修改儲存。"
|
||||||
|
|
||||||
|
#: service/singleton/crontask.go:150 service/singleton/crontask.go:175
|
||||||
|
#, c-format
|
||||||
|
msgid "[Task failed] %s: server %s is offline and cannot execute the task"
|
||||||
|
msgstr "[任務失敗] %s,伺服器 %s 離線,無法執行"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:439
|
||||||
|
#, c-format
|
||||||
|
msgid "[Latency] %s %2f > %2f, Reporter: %s"
|
||||||
|
msgstr "[延遲告警] %s %2f > %2f,報告服務: %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:446
|
||||||
|
#, c-format
|
||||||
|
msgid "[Latency] %s %2f < %2f, Reporter: %s"
|
||||||
|
msgstr "[延遲告警] %s %2f < %2f,報告服務: %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:472
|
||||||
|
#, c-format
|
||||||
|
msgid "[%s] %s Reporter: %s, Error: %s"
|
||||||
|
msgstr "[%s] %s 報告服務:%s,錯誤資訊:%s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:515
|
||||||
|
#, c-format
|
||||||
|
msgid "[TLS] Fetch cert info failed, Reporter: %s, Error: %s"
|
||||||
|
msgstr "[TLS] 獲取證書資訊失敗,報告服務:%s,錯誤資訊:%s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:555
|
||||||
|
#, c-format
|
||||||
|
msgid "The TLS certificate will expire within seven days. Expiration time: %s"
|
||||||
|
msgstr "TLS 證書將在 7 天內過期。過期時間為:%s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:568
|
||||||
|
#, c-format
|
||||||
|
msgid ""
|
||||||
|
"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, "
|
||||||
|
"expires at %s"
|
||||||
|
msgstr ""
|
||||||
|
"TLS 證書發生更改,舊值:頒發者 %s,過期日 %s;新值:頒發者 %s,過期日 %s"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:604
|
||||||
|
msgid "No Data"
|
||||||
|
msgstr "無資料"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:606
|
||||||
|
msgid "Good"
|
||||||
|
msgstr "正常"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:608
|
||||||
|
msgid "Low Availability"
|
||||||
|
msgstr "低可用"
|
||||||
|
|
||||||
|
#: service/singleton/servicesentinel.go:610
|
||||||
|
msgid "Down"
|
||||||
|
msgstr "故障"
|
@ -14,12 +14,12 @@ var (
|
|||||||
func init() {
|
func init() {
|
||||||
HttpClientSkipTlsVerify = httpClient(_httpClient{
|
HttpClientSkipTlsVerify = httpClient(_httpClient{
|
||||||
Transport: httpTransport(_httpTransport{
|
Transport: httpTransport(_httpTransport{
|
||||||
SkipVerifySSL: true,
|
SkipVerifyTLS: true,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
HttpClient = httpClient(_httpClient{
|
HttpClient = httpClient(_httpClient{
|
||||||
Transport: httpTransport(_httpTransport{
|
Transport: httpTransport(_httpTransport{
|
||||||
SkipVerifySSL: false,
|
SkipVerifyTLS: false,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -27,12 +27,12 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type _httpTransport struct {
|
type _httpTransport struct {
|
||||||
SkipVerifySSL bool
|
SkipVerifyTLS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpTransport(conf _httpTransport) *http.Transport {
|
func httpTransport(conf _httpTransport) *http.Transport {
|
||||||
return &http.Transport{
|
return &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: conf.SkipVerifySSL},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: conf.SkipVerifyTLS},
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,6 @@ var (
|
|||||||
DNSServers = []string{"1.1.1.1:53", "223.5.5.5:53"}
|
DNSServers = []string{"1.1.1.1:53", "223.5.5.5:53"}
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsWindows() bool {
|
|
||||||
return os.PathSeparator == '\\' && os.PathListSeparator == ';'
|
|
||||||
}
|
|
||||||
|
|
||||||
var ipv4Re = regexp.MustCompile(`(\d*\.).*(\.\d*)`)
|
var ipv4Re = regexp.MustCompile(`(\d*\.).*(\.\d*)`)
|
||||||
|
|
||||||
func ipv4Desensitize(ipv4Addr string) string {
|
func ipv4Desensitize(ipv4Addr string) string {
|
||||||
|
175
script/i18n.sh
Executable file
175
script/i18n.sh
Executable file
@ -0,0 +1,175 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
LANG=()
|
||||||
|
while IFS='' read -r line; do LANG+=("$line"); done < <(ls pkg/i18n/translations)
|
||||||
|
TEMPLATE="pkg/i18n/template.pot"
|
||||||
|
PODIR="pkg/i18n/translations/%s/LC_MESSAGES"
|
||||||
|
GIT_ROOT=$(git rev-parse --show-toplevel)
|
||||||
|
|
||||||
|
red='\033[0;31m'
|
||||||
|
green='\033[0;32m'
|
||||||
|
yellow='\033[0;33m'
|
||||||
|
plain='\033[0m'
|
||||||
|
|
||||||
|
err() {
|
||||||
|
printf "${red}%s${plain}\n" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
printf "${green}%s${plain}\n" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
printf "${yellow}%s${plain}\n" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
generate() {
|
||||||
|
case $1 in
|
||||||
|
"template")
|
||||||
|
generate_template
|
||||||
|
;;
|
||||||
|
"en")
|
||||||
|
generate_en
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
err "invalid argument"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_template() {
|
||||||
|
read -ra src < <(find . -name "*.go" | sort)
|
||||||
|
xgettext -C --add-comments=TRANSLATORS: -kErrorT -kT -kTf -kN:1,2 --from-code=UTF-8 -o $TEMPLATE "${src[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_en() {
|
||||||
|
local po_file
|
||||||
|
po_file=$(printf "$PODIR/nezha.po" "en_US")
|
||||||
|
local mo_file
|
||||||
|
mo_file=$(printf "$PODIR/nezha.mo" "en_US")
|
||||||
|
msginit --input=$TEMPLATE --locale=en_US.UTF-8 --output-file="$po_file" --no-translator
|
||||||
|
msgfmt "$po_file" -o "$mo_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
compile() {
|
||||||
|
if [[ $# != 0 ]]; then
|
||||||
|
compile_single "$1"
|
||||||
|
else
|
||||||
|
compile_all
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
compile_single() {
|
||||||
|
local param="$1"
|
||||||
|
local found=0
|
||||||
|
|
||||||
|
for lang in "${LANG[@]}"; do
|
||||||
|
if [[ "$lang" == "$param" ]]; then
|
||||||
|
found=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $found == 0 ]]; then
|
||||||
|
err "the language does not exist."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local po_file
|
||||||
|
po_file=$(printf "$PODIR/nezha.po" "$param")
|
||||||
|
local mo_file
|
||||||
|
mo_file=$(printf "$PODIR/nezha.mo" "$param")
|
||||||
|
|
||||||
|
msgfmt "$po_file" -o "$mo_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
compile_all() {
|
||||||
|
local po_file
|
||||||
|
local mo_file
|
||||||
|
for lang in "${LANG[@]}"; do
|
||||||
|
po_file=$(printf "$PODIR/nezha.po" "$lang")
|
||||||
|
mo_file=$(printf "$PODIR/nezha.mo" "$lang")
|
||||||
|
|
||||||
|
msgfmt "$po_file" -o "$mo_file"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if [[ $# != 0 ]]; then
|
||||||
|
update_single "$1"
|
||||||
|
else
|
||||||
|
update_all
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
update_single() {
|
||||||
|
local param="$1"
|
||||||
|
local found=0
|
||||||
|
|
||||||
|
for lang in "${LANG[@]}"; do
|
||||||
|
if [[ "$lang" == "$param" ]]; then
|
||||||
|
found=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $found == 0 ]]; then
|
||||||
|
err "the language does not exist."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local po_file
|
||||||
|
po_file=$(printf "$PODIR/nezha.po" "$param")
|
||||||
|
msgmerge -U "$po_file" $TEMPLATE
|
||||||
|
}
|
||||||
|
|
||||||
|
update_all() {
|
||||||
|
for lang in "${LANG[@]}"; do
|
||||||
|
local po_file
|
||||||
|
po_file=$(printf "$PODIR/nezha.po" "$lang")
|
||||||
|
msgmerge -U "$po_file" $TEMPLATE
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
echo "Usage: $0 [command] args"
|
||||||
|
echo ""
|
||||||
|
echo "Available commands:"
|
||||||
|
echo " update Update .po from .pot"
|
||||||
|
echo " compile Compile .mo from .po"
|
||||||
|
echo " generate Generate template or en_US locale"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 update # Update all locales"
|
||||||
|
echo " $0 update zh_CN # Update zh_CN locale"
|
||||||
|
echo " $0 compile # Compile all locales"
|
||||||
|
echo " $0 compile zh_CN # Compile zh_CN locale"
|
||||||
|
echo " $0 generate template # Generate template"
|
||||||
|
echo " $0 generate en # Generate en_US locale"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ $PWD != "$GIT_ROOT" ]]; then
|
||||||
|
err "Must execute in the project root"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
"update")
|
||||||
|
update "$2"
|
||||||
|
;;
|
||||||
|
"compile")
|
||||||
|
compile "$2"
|
||||||
|
;;
|
||||||
|
"generate")
|
||||||
|
generate "$2"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown command '$1'"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
@ -6,6 +6,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/service/singleton"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ioStreamContext struct {
|
type ioStreamContext struct {
|
||||||
@ -117,13 +119,13 @@ LOOP:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if stream.userIo == nil && stream.agentIo == nil {
|
if stream.userIo == nil && stream.agentIo == nil {
|
||||||
return errors.New("timeout: no connection established")
|
return singleton.Localizer.ErrorT("timeout: no connection established")
|
||||||
}
|
}
|
||||||
if stream.userIo == nil {
|
if stream.userIo == nil {
|
||||||
return errors.New("timeout: user connection not established")
|
return singleton.Localizer.ErrorT("timeout: user connection not established")
|
||||||
}
|
}
|
||||||
if stream.agentIo == nil {
|
if stream.agentIo == nil {
|
||||||
return errors.New("timeout: agent connection not established")
|
return singleton.Localizer.ErrorT("timeout: agent connection not established")
|
||||||
}
|
}
|
||||||
|
|
||||||
isDone := new(atomic.Bool)
|
isDone := new(atomic.Bool)
|
||||||
|
@ -54,11 +54,11 @@ func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Rece
|
|||||||
curServer := model.Server{}
|
curServer := model.Server{}
|
||||||
copier.Copy(&curServer, singleton.ServerList[clientID])
|
copier.Copy(&curServer, singleton.ServerList[clientID])
|
||||||
if cr.PushSuccessful && r.GetSuccessful() {
|
if cr.PushSuccessful && r.GetSuccessful() {
|
||||||
singleton.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", "Scheduled Task Executed Successfully",
|
singleton.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", singleton.Localizer.T("Scheduled Task Executed Successfully"),
|
||||||
cr.Name, singleton.ServerList[clientID].Name, r.GetData()), nil, &curServer)
|
cr.Name, singleton.ServerList[clientID].Name, r.GetData()), nil, &curServer)
|
||||||
}
|
}
|
||||||
if !r.GetSuccessful() {
|
if !r.GetSuccessful() {
|
||||||
singleton.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", "Scheduled Task Executed Failed",
|
singleton.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", singleton.Localizer.T("Scheduled Task Executed Failed"),
|
||||||
cr.Name, singleton.ServerList[clientID].Name, r.GetData()), nil, &curServer)
|
cr.Name, singleton.ServerList[clientID].Name, r.GetData()), nil, &curServer)
|
||||||
}
|
}
|
||||||
singleton.DB.Model(cr).Updates(model.Cron{
|
singleton.DB.Model(cr).Updates(model.Cron{
|
||||||
@ -153,7 +153,7 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
|
|||||||
singleton.SendNotification(singleton.Conf.IPChangeNotificationGroupID,
|
singleton.SendNotification(singleton.Conf.IPChangeNotificationGroupID,
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"[%s] %s, %s => %s",
|
"[%s] %s, %s => %s",
|
||||||
"IPChanged",
|
singleton.Localizer.T("IP Changed"),
|
||||||
singleton.ServerList[clientID].Name, singleton.IPDesensitize(singleton.ServerList[clientID].Host.IP),
|
singleton.ServerList[clientID].Name, singleton.IPDesensitize(singleton.ServerList[clientID].Host.IP),
|
||||||
singleton.IPDesensitize(host.IP),
|
singleton.IPDesensitize(host.IP),
|
||||||
),
|
),
|
||||||
|
@ -156,7 +156,7 @@ func checkStatus() {
|
|||||||
// 始终触发模式或上次检查不为失败时触发报警(跳过单次触发+上次失败的情况)
|
// 始终触发模式或上次检查不为失败时触发报警(跳过单次触发+上次失败的情况)
|
||||||
if alert.TriggerMode == model.ModeAlwaysTrigger || alertsPrevState[alert.ID][server.ID] != _RuleCheckFail {
|
if alert.TriggerMode == model.ModeAlwaysTrigger || alertsPrevState[alert.ID][server.ID] != _RuleCheckFail {
|
||||||
alertsPrevState[alert.ID][server.ID] = _RuleCheckFail
|
alertsPrevState[alert.ID][server.ID] = _RuleCheckFail
|
||||||
message := fmt.Sprintf("[%s] %s(%s) %s", "Incident",
|
message := fmt.Sprintf("[%s] %s(%s) %s", Localizer.T("Incident"),
|
||||||
server.Name, IPDesensitize(server.Host.IP), alert.Name)
|
server.Name, IPDesensitize(server.Host.IP), alert.Name)
|
||||||
go SendTriggerTasks(alert.FailTriggerTasks, curServer.ID)
|
go SendTriggerTasks(alert.FailTriggerTasks, curServer.ID)
|
||||||
go SendNotification(alert.NotificationGroupID, message, NotificationMuteLabel.ServerIncident(server.ID, alert.ID), &curServer)
|
go SendNotification(alert.NotificationGroupID, message, NotificationMuteLabel.ServerIncident(server.ID, alert.ID), &curServer)
|
||||||
@ -166,7 +166,7 @@ func checkStatus() {
|
|||||||
} else {
|
} else {
|
||||||
// 本次通过检查但上一次的状态为失败,则发送恢复通知
|
// 本次通过检查但上一次的状态为失败,则发送恢复通知
|
||||||
if alertsPrevState[alert.ID][server.ID] == _RuleCheckFail {
|
if alertsPrevState[alert.ID][server.ID] == _RuleCheckFail {
|
||||||
message := fmt.Sprintf("[%s] %s(%s) %s", "Resolved",
|
message := fmt.Sprintf("[%s] %s(%s) %s", Localizer.T("Resolved"),
|
||||||
server.Name, IPDesensitize(server.Host.IP), alert.Name)
|
server.Name, IPDesensitize(server.Host.IP), alert.Name)
|
||||||
go SendTriggerTasks(alert.RecoverTriggerTasks, curServer.ID)
|
go SendTriggerTasks(alert.RecoverTriggerTasks, curServer.ID)
|
||||||
go SendNotification(alert.NotificationGroupID, message, NotificationMuteLabel.ServerIncidentResolved(server.ID, alert.ID), &curServer)
|
go SendNotification(alert.NotificationGroupID, message, NotificationMuteLabel.ServerIncidentResolved(server.ID, alert.ID), &curServer)
|
||||||
|
@ -34,29 +34,29 @@ func loadCronTasks() {
|
|||||||
var err error
|
var err error
|
||||||
var notificationGroupList []uint64
|
var notificationGroupList []uint64
|
||||||
notificationMsgMap := make(map[uint64]*bytes.Buffer)
|
notificationMsgMap := make(map[uint64]*bytes.Buffer)
|
||||||
for i := 0; i < len(CronList); i++ {
|
for _, cron := range CronList {
|
||||||
// 触发任务类型无需注册
|
// 触发任务类型无需注册
|
||||||
if CronList[i].TaskType == model.CronTypeTriggerTask {
|
if cron.TaskType == model.CronTypeTriggerTask {
|
||||||
Crons[CronList[i].ID] = CronList[i]
|
Crons[cron.ID] = cron
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// 注册计划任务
|
// 注册计划任务
|
||||||
CronList[i].CronJobID, err = Cron.AddFunc(CronList[i].Scheduler, CronTrigger(CronList[i]))
|
cron.CronJobID, err = Cron.AddFunc(cron.Scheduler, CronTrigger(cron))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
Crons[CronList[i].ID] = CronList[i]
|
Crons[cron.ID] = cron
|
||||||
} else {
|
} else {
|
||||||
// 当前通知组首次出现 将其加入通知组列表并初始化通知组消息缓存
|
// 当前通知组首次出现 将其加入通知组列表并初始化通知组消息缓存
|
||||||
if _, ok := notificationMsgMap[CronList[i].NotificationGroupID]; !ok {
|
if _, ok := notificationMsgMap[cron.NotificationGroupID]; !ok {
|
||||||
notificationGroupList = append(notificationGroupList, CronList[i].NotificationGroupID)
|
notificationGroupList = append(notificationGroupList, cron.NotificationGroupID)
|
||||||
notificationMsgMap[CronList[i].NotificationGroupID] = bytes.NewBufferString("")
|
notificationMsgMap[cron.NotificationGroupID] = bytes.NewBufferString("")
|
||||||
notificationMsgMap[CronList[i].NotificationGroupID].WriteString("调度失败的计划任务:[")
|
notificationMsgMap[cron.NotificationGroupID].WriteString(Localizer.T("Tasks failed to register: ["))
|
||||||
}
|
}
|
||||||
notificationMsgMap[CronList[i].NotificationGroupID].WriteString(fmt.Sprintf("%d,", CronList[i].ID))
|
notificationMsgMap[cron.NotificationGroupID].WriteString(fmt.Sprintf("%d,", cron.ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 向注册错误的计划任务所在通知组发送通知
|
// 向注册错误的计划任务所在通知组发送通知
|
||||||
for _, gid := range notificationGroupList {
|
for _, gid := range notificationGroupList {
|
||||||
notificationMsgMap[gid].WriteString("] 这些任务将无法正常执行,请进入后点重新修改保存。")
|
notificationMsgMap[gid].WriteString(Localizer.T("] These tasks will not execute properly. Fix them in the admin dashboard."))
|
||||||
SendNotification(gid, notificationMsgMap[gid].String(), nil)
|
SendNotification(gid, notificationMsgMap[gid].String(), nil)
|
||||||
}
|
}
|
||||||
Cron.Start()
|
Cron.Start()
|
||||||
@ -147,7 +147,7 @@ func CronTrigger(cr *model.Cron, triggerServer ...uint64) func() {
|
|||||||
// 保存当前服务器状态信息
|
// 保存当前服务器状态信息
|
||||||
curServer := model.Server{}
|
curServer := model.Server{}
|
||||||
copier.Copy(&curServer, s)
|
copier.Copy(&curServer, s)
|
||||||
SendNotification(cr.NotificationGroupID, fmt.Sprintf("[任务失败] %s,服务器 %s 离线,无法执行。", cr.Name, s.Name), nil, &curServer)
|
SendNotification(cr.NotificationGroupID, Localizer.Tf("[Task failed] %s: server %s is offline and cannot execute the task", cr.Name, s.Name), nil, &curServer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -172,7 +172,7 @@ func CronTrigger(cr *model.Cron, triggerServer ...uint64) func() {
|
|||||||
// 保存当前服务器状态信息
|
// 保存当前服务器状态信息
|
||||||
curServer := model.Server{}
|
curServer := model.Server{}
|
||||||
copier.Copy(&curServer, s)
|
copier.Copy(&curServer, s)
|
||||||
SendNotification(cr.NotificationGroupID, fmt.Sprintf("[任务失败] %s,服务器 %s 离线,无法执行。", cr.Name, s.Name), nil, &curServer)
|
SendNotification(cr.NotificationGroupID, Localizer.Tf("[Task failed] %s: server %s is offline and cannot execute the task", cr.Name, s.Name), nil, &curServer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
80
service/singleton/i18n.go
Normal file
80
service/singleton/i18n.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package singleton
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/pkg/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
const domain = "nezha"
|
||||||
|
|
||||||
|
var Localizer *i18n.Localizer
|
||||||
|
|
||||||
|
func initI18n() {
|
||||||
|
if err := loadTranslation(); err != nil {
|
||||||
|
log.Printf("NEZHA>> init i18n failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTranslation() error {
|
||||||
|
lang := Conf.Language
|
||||||
|
if lang == "" {
|
||||||
|
lang = "zh_CN"
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := getTranslationArchive(lang)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
Localizer = i18n.NewLocalizer(lang, domain, domain+".zip", data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnUpdateLang(lang string) error {
|
||||||
|
if Localizer.Exists(lang) {
|
||||||
|
Localizer.SetLanguage(lang)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := getTranslationArchive(lang)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
Localizer.AppendIntl(lang, domain, domain+".zip", data)
|
||||||
|
Localizer.SetLanguage(lang)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTranslationArchive(lang string) ([]byte, error) {
|
||||||
|
files := [...]string{
|
||||||
|
fmt.Sprintf("translations/%s/LC_MESSAGES/%s.po", lang, domain),
|
||||||
|
fmt.Sprintf("translations/%s/LC_MESSAGES/%s.mo", lang, domain),
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
w := zip.NewWriter(buf)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
f, err := w.Create(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err := i18n.Translations.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := f.Write(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
@ -322,7 +322,7 @@ func (_NotificationMuteLabel) ServiceStateChanged(serviceId uint64) *string {
|
|||||||
return &label
|
return &label
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_NotificationMuteLabel) ServiceSSL(serviceId uint64, extraInfo string) *string {
|
func (_NotificationMuteLabel) ServiceTLS(serviceId uint64, extraInfo string) *string {
|
||||||
label := fmt.Sprintf("bf::sssl-%d-%s", serviceId, extraInfo)
|
label := fmt.Sprintf("bf::stls-%d-%s", serviceId, extraInfo)
|
||||||
return &label
|
return &label
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ func NewServiceSentinel(serviceSentinelDispatchBus chan<- model.Service) {
|
|||||||
serviceResponseDataStoreCurrentAvgDelay: make(map[uint64]float32),
|
serviceResponseDataStoreCurrentAvgDelay: make(map[uint64]float32),
|
||||||
serviceResponsePing: make(map[uint64]map[uint64]*pingStore),
|
serviceResponsePing: make(map[uint64]map[uint64]*pingStore),
|
||||||
Services: make(map[uint64]*model.Service),
|
Services: make(map[uint64]*model.Service),
|
||||||
sslCertCache: make(map[uint64]string),
|
tlsCertCache: make(map[uint64]string),
|
||||||
// 30天数据缓存
|
// 30天数据缓存
|
||||||
monthlyStatus: make(map[uint64]*model.ServiceResponseItem),
|
monthlyStatus: make(map[uint64]*model.ServiceResponseItem),
|
||||||
dispatchBus: serviceSentinelDispatchBus,
|
dispatchBus: serviceSentinelDispatchBus,
|
||||||
@ -102,7 +102,7 @@ type ServiceSentinel struct {
|
|||||||
serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [service_id] -> 当前服务离线计数
|
serviceResponseDataStoreCurrentAvgDelay map[uint64]float32 // [service_id] -> 当前服务离线计数
|
||||||
serviceResponsePing map[uint64]map[uint64]*pingStore // [service_id] -> ClientID -> delay
|
serviceResponsePing map[uint64]map[uint64]*pingStore // [service_id] -> ClientID -> delay
|
||||||
lastStatus map[uint64]int
|
lastStatus map[uint64]int
|
||||||
sslCertCache map[uint64]string
|
tlsCertCache map[uint64]string
|
||||||
|
|
||||||
ServicesLock sync.RWMutex
|
ServicesLock sync.RWMutex
|
||||||
Services map[uint64]*model.Service
|
Services map[uint64]*model.Service
|
||||||
@ -279,7 +279,7 @@ func (ss *ServiceSentinel) OnServiceDelete(ids []uint64) {
|
|||||||
delete(ss.serviceResponseDataStoreCurrentUp, id)
|
delete(ss.serviceResponseDataStoreCurrentUp, id)
|
||||||
delete(ss.serviceResponseDataStoreCurrentDown, id)
|
delete(ss.serviceResponseDataStoreCurrentDown, id)
|
||||||
delete(ss.serviceResponseDataStoreCurrentAvgDelay, id)
|
delete(ss.serviceResponseDataStoreCurrentAvgDelay, id)
|
||||||
delete(ss.sslCertCache, id)
|
delete(ss.tlsCertCache, id)
|
||||||
delete(ss.serviceStatusToday, id)
|
delete(ss.serviceStatusToday, id)
|
||||||
|
|
||||||
// 停掉定时任务
|
// 停掉定时任务
|
||||||
@ -436,14 +436,14 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
// 延迟超过最大值
|
// 延迟超过最大值
|
||||||
ServerLock.RLock()
|
ServerLock.RLock()
|
||||||
reporterServer := ServerList[r.Reporter]
|
reporterServer := ServerList[r.Reporter]
|
||||||
msg := fmt.Sprintf("[Latency] %s %2f > %2f, Reporter: %s", ss.Services[mh.GetId()].Name, mh.Delay, ss.Services[mh.GetId()].MaxLatency, reporterServer.Name)
|
msg := Localizer.Tf("[Latency] %s %2f > %2f, Reporter: %s", ss.Services[mh.GetId()].Name, mh.Delay, ss.Services[mh.GetId()].MaxLatency, reporterServer.Name)
|
||||||
go SendNotification(notificationGroupID, msg, minMuteLabel)
|
go SendNotification(notificationGroupID, msg, minMuteLabel)
|
||||||
ServerLock.RUnlock()
|
ServerLock.RUnlock()
|
||||||
} else if mh.Delay < ss.Services[mh.GetId()].MinLatency {
|
} else if mh.Delay < ss.Services[mh.GetId()].MinLatency {
|
||||||
// 延迟低于最小值
|
// 延迟低于最小值
|
||||||
ServerLock.RLock()
|
ServerLock.RLock()
|
||||||
reporterServer := ServerList[r.Reporter]
|
reporterServer := ServerList[r.Reporter]
|
||||||
msg := fmt.Sprintf("[Latency] %s %2f < %2f, Reporter: %s", ss.Services[mh.GetId()].Name, mh.Delay, ss.Services[mh.GetId()].MinLatency, reporterServer.Name)
|
msg := Localizer.Tf("[Latency] %s %2f < %2f, Reporter: %s", ss.Services[mh.GetId()].Name, mh.Delay, ss.Services[mh.GetId()].MinLatency, reporterServer.Name)
|
||||||
go SendNotification(notificationGroupID, msg, maxMuteLabel)
|
go SendNotification(notificationGroupID, msg, maxMuteLabel)
|
||||||
ServerLock.RUnlock()
|
ServerLock.RUnlock()
|
||||||
} else {
|
} else {
|
||||||
@ -469,7 +469,7 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
|
|
||||||
reporterServer := ServerList[r.Reporter]
|
reporterServer := ServerList[r.Reporter]
|
||||||
notificationGroupID := ss.Services[mh.GetId()].NotificationGroupID
|
notificationGroupID := ss.Services[mh.GetId()].NotificationGroupID
|
||||||
notificationMsg := fmt.Sprintf("[%s] %s Reporter: %s, Error: %s", StatusCodeToString(stateCode), ss.Services[mh.GetId()].Name, reporterServer.Name, mh.Data)
|
notificationMsg := Localizer.Tf("[%s] %s Reporter: %s, Error: %s", StatusCodeToString(stateCode), ss.Services[mh.GetId()].Name, reporterServer.Name, mh.Data)
|
||||||
muteLabel := NotificationMuteLabel.ServiceStateChanged(mh.GetId())
|
muteLabel := NotificationMuteLabel.ServiceStateChanged(mh.GetId())
|
||||||
|
|
||||||
// 状态变更时,清除静音缓存
|
// 状态变更时,清除静音缓存
|
||||||
@ -501,7 +501,7 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
}
|
}
|
||||||
ss.serviceResponseDataStoreLock.Unlock()
|
ss.serviceResponseDataStoreLock.Unlock()
|
||||||
|
|
||||||
// SSL 证书报警
|
// TLS 证书报警
|
||||||
var errMsg string
|
var errMsg string
|
||||||
if strings.HasPrefix(mh.Data, "SSL证书错误:") {
|
if strings.HasPrefix(mh.Data, "SSL证书错误:") {
|
||||||
// i/o timeout、connection timeout、EOF 错误
|
// i/o timeout、connection timeout、EOF 错误
|
||||||
@ -511,15 +511,15 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
errMsg = mh.Data
|
errMsg = mh.Data
|
||||||
ss.ServicesLock.RLock()
|
ss.ServicesLock.RLock()
|
||||||
if ss.Services[mh.GetId()].Notify {
|
if ss.Services[mh.GetId()].Notify {
|
||||||
muteLabel := NotificationMuteLabel.ServiceSSL(mh.GetId(), "network")
|
muteLabel := NotificationMuteLabel.ServiceTLS(mh.GetId(), "network")
|
||||||
go SendNotification(ss.Services[mh.GetId()].NotificationGroupID, fmt.Sprintf("[SSL] Fetch cert info failed, %s %s", ss.Services[mh.GetId()].Name, errMsg), muteLabel)
|
go SendNotification(ss.Services[mh.GetId()].NotificationGroupID, Localizer.Tf("[TLS] Fetch cert info failed, Reporter: %s, Error: %s", ss.Services[mh.GetId()].Name, errMsg), muteLabel)
|
||||||
}
|
}
|
||||||
ss.ServicesLock.RUnlock()
|
ss.ServicesLock.RUnlock()
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 清除网络错误静音缓存
|
// 清除网络错误静音缓存
|
||||||
UnMuteNotification(ss.Services[mh.GetId()].NotificationGroupID, NotificationMuteLabel.ServiceSSL(mh.GetId(), "network"))
|
UnMuteNotification(ss.Services[mh.GetId()].NotificationGroupID, NotificationMuteLabel.ServiceTLS(mh.GetId(), "network"))
|
||||||
|
|
||||||
var newCert = strings.Split(mh.Data, "|")
|
var newCert = strings.Split(mh.Data, "|")
|
||||||
if len(newCert) > 1 {
|
if len(newCert) > 1 {
|
||||||
@ -527,11 +527,11 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
enableNotify := ss.Services[mh.GetId()].Notify
|
enableNotify := ss.Services[mh.GetId()].Notify
|
||||||
|
|
||||||
// 首次获取证书信息时,缓存证书信息
|
// 首次获取证书信息时,缓存证书信息
|
||||||
if ss.sslCertCache[mh.GetId()] == "" {
|
if ss.tlsCertCache[mh.GetId()] == "" {
|
||||||
ss.sslCertCache[mh.GetId()] = mh.Data
|
ss.tlsCertCache[mh.GetId()] = mh.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
oldCert := strings.Split(ss.sslCertCache[mh.GetId()], "|")
|
oldCert := strings.Split(ss.tlsCertCache[mh.GetId()], "|")
|
||||||
isCertChanged := false
|
isCertChanged := false
|
||||||
expiresOld, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", oldCert[1])
|
expiresOld, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", oldCert[1])
|
||||||
expiresNew, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", newCert[1])
|
expiresNew, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", newCert[1])
|
||||||
@ -539,7 +539,7 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
// 证书变更时,更新缓存
|
// 证书变更时,更新缓存
|
||||||
if oldCert[0] != newCert[0] && !expiresNew.Equal(expiresOld) {
|
if oldCert[0] != newCert[0] && !expiresNew.Equal(expiresOld) {
|
||||||
isCertChanged = true
|
isCertChanged = true
|
||||||
ss.sslCertCache[mh.GetId()] = mh.Data
|
ss.tlsCertCache[mh.GetId()] = mh.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationGroupID := ss.Services[mh.GetId()].NotificationGroupID
|
notificationGroupID := ss.Services[mh.GetId()].NotificationGroupID
|
||||||
@ -551,25 +551,25 @@ func (ss *ServiceSentinel) worker() {
|
|||||||
// 证书过期提醒
|
// 证书过期提醒
|
||||||
if expiresNew.Before(time.Now().AddDate(0, 0, 7)) {
|
if expiresNew.Before(time.Now().AddDate(0, 0, 7)) {
|
||||||
expiresTimeStr := expiresNew.Format("2006-01-02 15:04:05")
|
expiresTimeStr := expiresNew.Format("2006-01-02 15:04:05")
|
||||||
errMsg = fmt.Sprintf(
|
errMsg = Localizer.Tf(
|
||||||
"The SSL certificate will expire within seven days. Expiration time: %s",
|
"The TLS certificate will expire within seven days. Expiration time: %s",
|
||||||
expiresTimeStr,
|
expiresTimeStr,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 静音规则: 服务id+证书过期时间
|
// 静音规则: 服务id+证书过期时间
|
||||||
// 用于避免多个监测点对相同证书同时报警
|
// 用于避免多个监测点对相同证书同时报警
|
||||||
muteLabel := NotificationMuteLabel.ServiceSSL(mh.GetId(), fmt.Sprintf("expire_%s", expiresTimeStr))
|
muteLabel := NotificationMuteLabel.ServiceTLS(mh.GetId(), fmt.Sprintf("expire_%s", expiresTimeStr))
|
||||||
go SendNotification(notificationGroupID, fmt.Sprintf("[SSL] %s %s", serviceName, errMsg), muteLabel)
|
go SendNotification(notificationGroupID, fmt.Sprintf("[TLS] %s %s", serviceName, errMsg), muteLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 证书变更提醒
|
// 证书变更提醒
|
||||||
if isCertChanged {
|
if isCertChanged {
|
||||||
errMsg = fmt.Sprintf(
|
errMsg = Localizer.Tf(
|
||||||
"SSL certificate changed, old: %s, %s expired; new: %s, %s expired.",
|
"TLS certificate changed, old: issuer %s, expires at %s; new: issuer %s, expires at %s",
|
||||||
oldCert[0], expiresOld.Format("2006-01-02 15:04:05"), newCert[0], expiresNew.Format("2006-01-02 15:04:05"))
|
oldCert[0], expiresOld.Format("2006-01-02 15:04:05"), newCert[0], expiresNew.Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
// 证书变更后会自动更新缓存,所以不需要静音
|
// 证书变更后会自动更新缓存,所以不需要静音
|
||||||
go SendNotification(notificationGroupID, fmt.Sprintf("[SSL] %s %s", serviceName, errMsg), nil)
|
go SendNotification(notificationGroupID, fmt.Sprintf("[TLS] %s %s", serviceName, errMsg), nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -601,17 +601,13 @@ func GetStatusCode[T float32 | uint64](percent T) int {
|
|||||||
func StatusCodeToString(statusCode int) string {
|
func StatusCodeToString(statusCode int) string {
|
||||||
switch statusCode {
|
switch statusCode {
|
||||||
case StatusNoData:
|
case StatusNoData:
|
||||||
// return Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "StatusNoData"})
|
return Localizer.T("No Data")
|
||||||
return "No Data"
|
|
||||||
case StatusGood:
|
case StatusGood:
|
||||||
// return Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "StatusGood"})
|
return Localizer.T("Good")
|
||||||
return "Good"
|
|
||||||
case StatusLowAvailability:
|
case StatusLowAvailability:
|
||||||
// return Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "StatusLowAvailability"})
|
return Localizer.T("Low Availability")
|
||||||
return "Low Availability"
|
|
||||||
case StatusDown:
|
case StatusDown:
|
||||||
// return Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "StatusDown"})
|
return Localizer.T("Down")
|
||||||
return "Down"
|
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ func InitTimezoneAndCache() {
|
|||||||
|
|
||||||
// LoadSingleton 加载子服务并执行
|
// LoadSingleton 加载子服务并执行
|
||||||
func LoadSingleton() {
|
func LoadSingleton() {
|
||||||
|
initI18n() // 加载本地化服务
|
||||||
loadNotifications() // 加载通知服务
|
loadNotifications() // 加载通知服务
|
||||||
loadServers() // 加载服务器列表
|
loadServers() // 加载服务器列表
|
||||||
loadCronTasks() // 加载定时任务
|
loadCronTasks() // 加载定时任务
|
||||||
|
Loading…
Reference in New Issue
Block a user