Commit 0305ed05 by Apple\Apple

🚦 feat(uptime-kuma): support custom category names & monitor subgroup rendering

Backend
• controller/uptime_kuma.go
  - Added Group field to Monitor struct to carry publicGroupList.name.
  - Extended status page parsing to capture group Name and inject it into each monitor.
  - Re-worked fetchGroupData loop: aggregate all sub-groups, drop unnecessary pre-allocation/breaks.

Frontend
• web/src/pages/Detail/index.js
  - renderMonitorList now buckets monitors by the new group field and renders a lightweight header per subgroup.
  - Fallback gracefully when group is empty to preserve previous single-list behaviour.

Other
• Expanded anonymous struct definition for statusData.PublicGroupList to include ID/Name, enabling JSON unmarshalling of group names.

Result
Custom CategoryName continues to work while each uptime group’s internal sub-groups are now clearly displayed in the UI, providing finer-grained visibility without impacting performance or existing validation logic.
parent d1431b0c
...@@ -147,6 +147,15 @@ func UpdateOption(c *gin.Context) { ...@@ -147,6 +147,15 @@ func UpdateOption(c *gin.Context) {
}) })
return return
} }
case "console_setting.uptime_kuma_groups":
err = console_setting.ValidateConsoleSettings(option.Value, "UptimeKumaGroups")
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} }
err = model.UpdateOption(option.Key, option.Value) err = model.UpdateOption(option.Key, option.Value)
if err != nil { if err != nil {
......
...@@ -4,9 +4,9 @@ import ( ...@@ -4,9 +4,9 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"net/http" "net/http"
"one-api/setting/console_setting" "one-api/setting/console_setting"
"strconv"
"strings" "strings"
"time" "time"
...@@ -14,45 +14,25 @@ import ( ...@@ -14,45 +14,25 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
type UptimeKumaMonitor struct { const (
ID int `json:"id"` requestTimeout = 30 * time.Second
Name string `json:"name"` httpTimeout = 10 * time.Second
Type string `json:"type"` uptimeKeySuffix = "_24"
} apiStatusPath = "/api/status-page/"
apiHeartbeatPath = "/api/status-page/heartbeat/"
type UptimeKumaGroup struct { )
ID int `json:"id"`
Name string `json:"name"`
Weight int `json:"weight"`
MonitorList []UptimeKumaMonitor `json:"monitorList"`
}
type UptimeKumaHeartbeat struct {
Status int `json:"status"`
Time string `json:"time"`
Msg string `json:"msg"`
Ping *float64 `json:"ping"`
}
type UptimeKumaStatusResponse struct {
PublicGroupList []UptimeKumaGroup `json:"publicGroupList"`
}
type UptimeKumaHeartbeatResponse struct {
HeartbeatList map[string][]UptimeKumaHeartbeat `json:"heartbeatList"`
UptimeList map[string]float64 `json:"uptimeList"`
}
type MonitorStatus struct { type Monitor struct {
Name string `json:"name"` Name string `json:"name"`
Uptime float64 `json:"uptime"` Uptime float64 `json:"uptime"`
Status int `json:"status"` Status int `json:"status"`
Group string `json:"group,omitempty"`
} }
var ( type UptimeGroupResult struct {
ErrUpstreamNon200 = errors.New("upstream non-200") CategoryName string `json:"categoryName"`
ErrTimeout = errors.New("context deadline exceeded") Monitors []Monitor `json:"monitors"`
) }
func getAndDecode(ctx context.Context, client *http.Client, url string, dest interface{}) error { func getAndDecode(ctx context.Context, client *http.Client, url string, dest interface{}) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
...@@ -62,107 +42,113 @@ func getAndDecode(ctx context.Context, client *http.Client, url string, dest int ...@@ -62,107 +42,113 @@ func getAndDecode(ctx context.Context, client *http.Client, url string, dest int
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return ErrTimeout
}
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return ErrUpstreamNon200 return errors.New("non-200 status")
} }
return json.NewDecoder(resp.Body).Decode(dest) return json.NewDecoder(resp.Body).Decode(dest)
} }
func GetUptimeKumaStatus(c *gin.Context) { func fetchGroupData(ctx context.Context, client *http.Client, groupConfig map[string]interface{}) UptimeGroupResult {
cs := console_setting.GetConsoleSetting() url, _ := groupConfig["url"].(string)
uptimeKumaUrl := cs.UptimeKumaUrl slug, _ := groupConfig["slug"].(string)
slug := cs.UptimeKumaSlug categoryName, _ := groupConfig["categoryName"].(string)
if uptimeKumaUrl == "" || slug == "" { result := UptimeGroupResult{
c.JSON(http.StatusOK, gin.H{ CategoryName: categoryName,
"success": true, Monitors: []Monitor{},
"message": "", }
"data": []MonitorStatus{},
}) if url == "" || slug == "" {
return return result
} }
uptimeKumaUrl = strings.TrimSuffix(uptimeKumaUrl, "/") baseURL := strings.TrimSuffix(url, "/")
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second) var statusData struct {
defer cancel() PublicGroupList []struct {
ID int `json:"id"`
client := &http.Client{} Name string `json:"name"`
MonitorList []struct {
statusPageUrl := fmt.Sprintf("%s/api/status-page/%s", uptimeKumaUrl, slug) ID int `json:"id"`
heartbeatUrl := fmt.Sprintf("%s/api/status-page/heartbeat/%s", uptimeKumaUrl, slug) Name string `json:"name"`
} `json:"monitorList"`
var ( } `json:"publicGroupList"`
statusData UptimeKumaStatusResponse }
heartbeatData UptimeKumaHeartbeatResponse
) var heartbeatData struct {
HeartbeatList map[string][]struct {
Status int `json:"status"`
} `json:"heartbeatList"`
UptimeList map[string]float64 `json:"uptimeList"`
}
g, gCtx := errgroup.WithContext(ctx) g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
g.Go(func() error { return getAndDecode(gCtx, client, baseURL+apiStatusPath+slug, &statusData)
return getAndDecode(gCtx, client, statusPageUrl, &statusData)
}) })
g.Go(func() error {
g.Go(func() error { return getAndDecode(gCtx, client, baseURL+apiHeartbeatPath+slug, &heartbeatData)
return getAndDecode(gCtx, client, heartbeatUrl, &heartbeatData)
}) })
if err := g.Wait(); err != nil { if g.Wait() != nil {
switch err { return result
case ErrUpstreamNon200:
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "上游接口出现问题",
})
case ErrTimeout:
c.JSON(http.StatusRequestTimeout, gin.H{
"success": false,
"message": "请求上游接口超时",
})
default:
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": err.Error(),
})
}
return
} }
var monitors []MonitorStatus for _, pg := range statusData.PublicGroupList {
for _, group := range statusData.PublicGroupList { if len(pg.MonitorList) == 0 {
for _, monitor := range group.MonitorList { continue
monitorStatus := MonitorStatus{ }
Name: monitor.Name,
Uptime: 0.0, for _, m := range pg.MonitorList {
Status: 0, monitor := Monitor{
Name: m.Name,
Group: pg.Name,
} }
uptimeKey := fmt.Sprintf("%d_24", monitor.ID) monitorID := strconv.Itoa(m.ID)
if uptime, exists := heartbeatData.UptimeList[uptimeKey]; exists {
monitorStatus.Uptime = uptime if uptime, exists := heartbeatData.UptimeList[monitorID+uptimeKeySuffix]; exists {
monitor.Uptime = uptime
} }
heartbeatKey := fmt.Sprintf("%d", monitor.ID) if heartbeats, exists := heartbeatData.HeartbeatList[monitorID]; exists && len(heartbeats) > 0 {
if heartbeats, exists := heartbeatData.HeartbeatList[heartbeatKey]; exists && len(heartbeats) > 0 { monitor.Status = heartbeats[0].Status
latestHeartbeat := heartbeats[0]
monitorStatus.Status = latestHeartbeat.Status
} }
monitors = append(monitors, monitorStatus) result.Monitors = append(result.Monitors, monitor)
} }
} }
c.JSON(http.StatusOK, gin.H{ return result
"success": true, }
"message": "",
"data": monitors, func GetUptimeKumaStatus(c *gin.Context) {
}) groups := console_setting.GetUptimeKumaGroups()
if len(groups) == 0 {
c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": []UptimeGroupResult{}})
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
defer cancel()
client := &http.Client{Timeout: httpTimeout}
results := make([]UptimeGroupResult, len(groups))
g, gCtx := errgroup.WithContext(ctx)
for i, group := range groups {
i, group := i, group
g.Go(func() error {
results[i] = fetchGroupData(gCtx, client, group)
return nil
})
}
g.Wait()
c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": results})
} }
\ No newline at end of file
...@@ -3,11 +3,10 @@ package console_setting ...@@ -3,11 +3,10 @@ package console_setting
import "one-api/setting/config" import "one-api/setting/config"
type ConsoleSetting struct { type ConsoleSetting struct {
ApiInfo string `json:"api_info"` // 控制台 API 信息 (JSON 数组字符串) ApiInfo string `json:"api_info"` // 控制台 API 信息 (JSON 数组字符串)
UptimeKumaUrl string `json:"uptime_kuma_url"` // Uptime Kuma 服务地址(如 https://status.example.com ) UptimeKumaGroups string `json:"uptime_kuma_groups"` // Uptime Kuma 分组配置 (JSON 数组字符串)
UptimeKumaSlug string `json:"uptime_kuma_slug"` // Uptime Kuma Status Page Slug Announcements string `json:"announcements"` // 系统公告 (JSON 数组字符串)
Announcements string `json:"announcements"` // 系统公告 (JSON 数组字符串) FAQ string `json:"faq"` // 常见问题 (JSON 数组字符串)
FAQ string `json:"faq"` // 常见问题 (JSON 数组字符串)
ApiInfoEnabled bool `json:"api_info_enabled"` // 是否启用 API 信息面板 ApiInfoEnabled bool `json:"api_info_enabled"` // 是否启用 API 信息面板
UptimeKumaEnabled bool `json:"uptime_kuma_enabled"` // 是否启用 Uptime Kuma 面板 UptimeKumaEnabled bool `json:"uptime_kuma_enabled"` // 是否启用 Uptime Kuma 面板
AnnouncementsEnabled bool `json:"announcements_enabled"` // 是否启用系统公告面板 AnnouncementsEnabled bool `json:"announcements_enabled"` // 是否启用系统公告面板
...@@ -16,11 +15,10 @@ type ConsoleSetting struct { ...@@ -16,11 +15,10 @@ type ConsoleSetting struct {
// 默认配置 // 默认配置
var defaultConsoleSetting = ConsoleSetting{ var defaultConsoleSetting = ConsoleSetting{
ApiInfo: "", ApiInfo: "",
UptimeKumaUrl: "", UptimeKumaGroups: "",
UptimeKumaSlug: "", Announcements: "",
Announcements: "", FAQ: "",
FAQ: "",
ApiInfoEnabled: true, ApiInfoEnabled: true,
UptimeKumaEnabled: true, UptimeKumaEnabled: true,
AnnouncementsEnabled: true, AnnouncementsEnabled: true,
......
...@@ -11,8 +11,7 @@ const DashboardSetting = () => { ...@@ -11,8 +11,7 @@ const DashboardSetting = () => {
'console_setting.api_info': '', 'console_setting.api_info': '',
'console_setting.announcements': '', 'console_setting.announcements': '',
'console_setting.faq': '', 'console_setting.faq': '',
'console_setting.uptime_kuma_url': '', 'console_setting.uptime_kuma_groups': '',
'console_setting.uptime_kuma_slug': '',
'console_setting.api_info_enabled': '', 'console_setting.api_info_enabled': '',
'console_setting.announcements_enabled': '', 'console_setting.announcements_enabled': '',
'console_setting.faq_enabled': '', 'console_setting.faq_enabled': '',
......
...@@ -1628,16 +1628,15 @@ ...@@ -1628,16 +1628,15 @@
"常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "FAQ management, providing answers to common questions for users (maximum 50, display latest 20 on the front end)", "常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "FAQ management, providing answers to common questions for users (maximum 50, display latest 20 on the front end)",
"暂无常见问答": "No FAQ", "暂无常见问答": "No FAQ",
"显示最新20条": "Display latest 20", "显示最新20条": "Display latest 20",
"Uptime Kuma 服务地址": "Uptime Kuma service address", "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Uptime Kuma monitoring category management, you can configure multiple monitoring categories for service status display (maximum 20)",
"状态页面 Slug": "Status page slug", "添加分类": "Add Category",
"请输入 Uptime Kuma 服务的完整地址,例如:https://uptime.example.com": "Please enter the complete address of Uptime Kuma, for example: https://uptime.example.com", "分类名称": "Category Name",
"请输入状态页面的 slug 标识符,例如:my-status": "Please enter the slug identifier for the status page, for example: my-status", "Uptime Kuma地址": "Uptime Kuma Address",
"Uptime Kuma 服务地址不能为空": "Uptime Kuma service address cannot be empty", "状态页面Slug": "Status Page Slug",
"请输入有效的 URL 地址": "Please enter a valid URL address", "请输入分类名称,如:OpenAI、Claude等": "Please enter the category name, such as: OpenAI, Claude, etc.",
"状态页面 Slug 不能为空": "Status page slug cannot be empty", "请输入Uptime Kuma服务地址,如:https://status.example.com": "Please enter the Uptime Kuma service address, such as: https://status.example.com",
"Slug 只能包含字母、数字、下划线和连字符": "Slug can only contain letters, numbers, underscores, and hyphens", "请输入状态页面的Slug,如:my-status": "Please enter the slug for the status page, such as: my-status",
"请输入 Uptime Kuma 服务地址": "Please enter the Uptime Kuma service address", "确定要删除此分类吗?": "Are you sure you want to delete this category?",
"请输入状态页面 Slug": "Please enter the status page slug",
"配置": "Configure", "配置": "Configure",
"服务监控地址,用于展示服务状态信息": "service monitoring address for displaying status information", "服务监控地址,用于展示服务状态信息": "service monitoring address for displaying status information",
"服务可用性": "Service Status", "服务可用性": "Service Status",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment