new-api/controller/channel_monitor.go
zizi 564a9619ed feat: add channel monitor admin API
Add admin endpoints to manage monitors, run probes, and inspect history and rollups.
2026-05-20 13:44:06 +08:00

373 lines
9.4 KiB
Go

package controller
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type createChannelMonitorRequest struct {
Name string `json:"name"`
ChannelId int `json:"channel_id"`
TargetUrl string `json:"target_url"`
PrimaryModel string `json:"primary_model"`
ExtraModels string `json:"extra_models"`
CheckInterval int `json:"check_interval"`
Timeout int `json:"timeout"`
RequestTemplates string `json:"request_templates"`
CustomHeaders string `json:"custom_headers"`
CustomBody string `json:"custom_body"`
Status *int `json:"status"`
}
type updateChannelMonitorRequest struct {
Name *string `json:"name"`
ChannelId *int `json:"channel_id"`
TargetUrl *string `json:"target_url"`
PrimaryModel *string `json:"primary_model"`
ExtraModels *string `json:"extra_models"`
CheckInterval *int `json:"check_interval"`
Timeout *int `json:"timeout"`
RequestTemplates *string `json:"request_templates"`
CustomHeaders *string `json:"custom_headers"`
CustomBody *string `json:"custom_body"`
Status *int `json:"status"`
}
func GetChannelMonitors(c *gin.Context) {
pageInfo := getChannelMonitorPageQuery(c)
monitors, total, err := model.GetChannelMonitors(pageInfo.GetPage(), pageInfo.GetPageSize())
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(monitors)
common.ApiSuccess(c, pageInfo)
}
func CreateChannelMonitor(c *gin.Context) {
var req createChannelMonitorRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiError(c, err)
return
}
monitor := &model.ChannelMonitor{
Name: strings.TrimSpace(req.Name),
ChannelId: req.ChannelId,
TargetUrl: strings.TrimSpace(req.TargetUrl),
PrimaryModel: strings.TrimSpace(req.PrimaryModel),
ExtraModels: strings.TrimSpace(req.ExtraModels),
CheckInterval: req.CheckInterval,
Timeout: req.Timeout,
RequestTemplates: strings.TrimSpace(req.RequestTemplates),
CustomHeaders: strings.TrimSpace(req.CustomHeaders),
CustomBody: req.CustomBody,
Status: 1,
}
if req.Status != nil {
monitor.Status = *req.Status
}
if monitor.CheckInterval == 0 {
monitor.CheckInterval = 300
}
if monitor.Timeout == 0 {
monitor.Timeout = 30
}
if err := validateChannelMonitor(monitor); err != nil {
common.ApiErrorMsg(c, err.Error())
return
}
if err := monitor.Insert(); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, monitor)
}
func GetChannelMonitor(c *gin.Context) {
monitor, ok := getChannelMonitorFromParam(c)
if !ok {
return
}
common.ApiSuccess(c, monitor)
}
func UpdateChannelMonitor(c *gin.Context) {
monitor, ok := getChannelMonitorFromParam(c)
if !ok {
return
}
var req updateChannelMonitorRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiError(c, err)
return
}
if req.Name != nil {
monitor.Name = strings.TrimSpace(*req.Name)
}
if req.ChannelId != nil {
monitor.ChannelId = *req.ChannelId
}
if req.TargetUrl != nil {
monitor.TargetUrl = strings.TrimSpace(*req.TargetUrl)
}
if req.PrimaryModel != nil {
monitor.PrimaryModel = strings.TrimSpace(*req.PrimaryModel)
}
if req.ExtraModels != nil {
monitor.ExtraModels = strings.TrimSpace(*req.ExtraModels)
}
if req.CheckInterval != nil {
monitor.CheckInterval = *req.CheckInterval
}
if req.Timeout != nil {
monitor.Timeout = *req.Timeout
}
if req.RequestTemplates != nil {
monitor.RequestTemplates = strings.TrimSpace(*req.RequestTemplates)
}
if req.CustomHeaders != nil {
monitor.CustomHeaders = strings.TrimSpace(*req.CustomHeaders)
}
if req.CustomBody != nil {
monitor.CustomBody = *req.CustomBody
}
if req.Status != nil {
monitor.Status = *req.Status
}
if err := validateChannelMonitor(monitor); err != nil {
common.ApiErrorMsg(c, err.Error())
return
}
if err := monitor.Update(); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, monitor)
}
func DeleteChannelMonitor(c *gin.Context) {
monitor, ok := getChannelMonitorFromParam(c)
if !ok {
return
}
monitor.Status = 0
if err := monitor.Update(); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
func RunChannelMonitorCheck(c *gin.Context) {
monitor, ok := getChannelMonitorFromParam(c)
if !ok {
return
}
if err := service.RunMonitorCheck(monitor); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, gin.H{"message": "检查已完成"})
}
func GetChannelMonitorHistory(c *gin.Context) {
monitor, ok := getChannelMonitorFromParam(c)
if !ok {
return
}
pageInfo := getChannelMonitorPageQuery(c)
startTime, ok := parseChannelMonitorOptionalTime(c, "start_time")
if !ok {
return
}
endTime, ok := parseChannelMonitorOptionalTime(c, "end_time")
if !ok {
return
}
histories, total, err := model.GetMonitorHistory(
monitor.Id,
pageInfo.GetPage(),
pageInfo.GetPageSize(),
strings.TrimSpace(c.Query("model_name")),
strings.TrimSpace(c.Query("status")),
startTime,
endTime,
)
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(histories)
common.ApiSuccess(c, pageInfo)
}
func GetChannelMonitorRollup(c *gin.Context) {
monitor, ok := getChannelMonitorFromParam(c)
if !ok {
return
}
startDate := strings.TrimSpace(c.Query("start_date"))
endDate := strings.TrimSpace(c.Query("end_date"))
if err := validateChannelMonitorDateRange(startDate, endDate); err != nil {
common.ApiErrorMsg(c, err.Error())
return
}
rollups, err := model.GetMonitorDailyRollups(
monitor.Id,
strings.TrimSpace(c.Query("model_name")),
startDate,
endDate,
)
if err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, rollups)
}
func getChannelMonitorFromParam(c *gin.Context) (*model.ChannelMonitor, bool) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
common.ApiError(c, err)
return nil, false
}
monitor, err := model.GetChannelMonitorByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
common.ApiErrorMsg(c, "渠道监控不存在")
return nil, false
}
common.ApiError(c, err)
return nil, false
}
return monitor, true
}
func getChannelMonitorPageQuery(c *gin.Context) *common.PageInfo {
pageInfo := common.GetPageQuery(c)
if c.Query("p") == "" {
if page, err := strconv.Atoi(c.Query("page")); err == nil && page > 0 {
pageInfo.Page = page
}
}
return pageInfo
}
func validateChannelMonitor(monitor *model.ChannelMonitor) error {
if strings.TrimSpace(monitor.Name) == "" {
return errors.New("name 不能为空")
}
if strings.TrimSpace(monitor.TargetUrl) == "" {
return errors.New("target_url 不能为空")
}
if strings.TrimSpace(monitor.PrimaryModel) == "" {
return errors.New("primary_model 不能为空")
}
if monitor.CheckInterval < 15 || monitor.CheckInterval > 3600 {
return errors.New("check_interval 必须在 15 到 3600 之间")
}
if monitor.Timeout < 1 || monitor.Timeout > 300 {
return errors.New("timeout 必须在 1 到 300 之间")
}
if monitor.Status != 0 && monitor.Status != 1 {
return errors.New("status 必须是 0 或 1")
}
if err := service.ValidateChannelMonitorTargetURL(monitor.TargetUrl); err != nil {
return fmt.Errorf("target_url 不合法: %w", err)
}
if err := validateChannelMonitorJSONStringArray(monitor.ExtraModels, "extra_models"); err != nil {
return err
}
if err := validateChannelMonitorJSONStringArray(monitor.RequestTemplates, "request_templates"); err != nil {
return err
}
if err := validateChannelMonitorJSONHeaders(monitor.CustomHeaders); err != nil {
return err
}
return nil
}
func validateChannelMonitorJSONStringArray(raw string, field string) error {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
var values []string
if err := common.Unmarshal([]byte(raw), &values); err != nil {
return fmt.Errorf("%s 必须是 JSON 字符串数组", field)
}
for _, value := range values {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s 不能包含空字符串", field)
}
}
return nil
}
func validateChannelMonitorJSONHeaders(raw string) error {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
var headers map[string]string
if err := common.Unmarshal([]byte(raw), &headers); err != nil {
return errors.New("custom_headers 必须是 JSON 字符串对象")
}
for key := range headers {
if strings.TrimSpace(key) == "" {
return errors.New("custom_headers 不能包含空 header 名")
}
}
return nil
}
func parseChannelMonitorOptionalTime(c *gin.Context, key string) (*time.Time, bool) {
value := strings.TrimSpace(c.Query(key))
if value == "" {
return nil, true
}
parsed, err := time.Parse(time.RFC3339, value)
if err != nil {
common.ApiErrorMsg(c, key+" 必须是 RFC3339 格式")
return nil, false
}
return &parsed, true
}
func validateChannelMonitorDateRange(startDate string, endDate string) error {
if startDate != "" {
if _, err := time.Parse("2006-01-02", startDate); err != nil {
return errors.New("start_date 必须是 YYYY-MM-DD 格式")
}
}
if endDate != "" {
if _, err := time.Parse("2006-01-02", endDate); err != nil {
return errors.New("end_date 必须是 YYYY-MM-DD 格式")
}
}
return nil
}