112 lines
3.2 KiB
Go
112 lines
3.2 KiB
Go
package projects
|
||
|
||
import (
|
||
"errors"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
"senlinai-agent/backend/internal/httpx"
|
||
"senlinai-agent/backend/internal/models"
|
||
)
|
||
|
||
// CronPlanDTO 是计划任务管理响应;MVP 只管理提醒元数据,不执行自主 Agent。
|
||
type CronPlanDTO struct {
|
||
ID string `json:"id"`
|
||
ProjectID string `json:"projectId"`
|
||
Title string `json:"title"`
|
||
Schedule string `json:"schedule"`
|
||
NextRunAt *time.Time `json:"nextRunAt"`
|
||
Enabled bool `json:"enabled"`
|
||
LastResult string `json:"lastResult"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
UpdatedAt time.Time `json:"updatedAt"`
|
||
}
|
||
|
||
// CronHandler 单独承载计划任务写接口。
|
||
type CronHandler struct {
|
||
service *Service
|
||
}
|
||
|
||
// NewCronHandler 创建计划任务 HTTP registrar。
|
||
func NewCronHandler(service *Service) *CronHandler {
|
||
return &CronHandler{service: service}
|
||
}
|
||
|
||
// Register 注册项目内计划任务创建接口。
|
||
func (h *CronHandler) Register(router gin.IRouter) {
|
||
router.POST("/projects/:id/cron-plans", h.create)
|
||
}
|
||
|
||
func (h *CronHandler) create(c *gin.Context) {
|
||
userID, project, ok := ownedProjectFromRequest(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
var input struct {
|
||
Title string `json:"title"`
|
||
Schedule string `json:"schedule"`
|
||
Enabled bool `json:"enabled"`
|
||
NextRunAt string `json:"nextRunAt"`
|
||
}
|
||
if err := c.ShouldBindJSON(&input); err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效")
|
||
return
|
||
}
|
||
nextRunAt, err := parseCronOptionalTime(input.NextRunAt)
|
||
if err != nil {
|
||
httpx.Error(c, http.StatusBadRequest, "invalid_request", "下次运行时间格式无效")
|
||
return
|
||
}
|
||
plan, err := h.service.CreateCronPlan(userID, project.ID, CreateCronPlanInput{
|
||
Title: input.Title, Schedule: input.Schedule, Enabled: input.Enabled, NextRunAt: nextRunAt,
|
||
})
|
||
if err != nil {
|
||
writeCronError(c, err)
|
||
return
|
||
}
|
||
c.JSON(http.StatusCreated, cronPlanDTO(*plan))
|
||
}
|
||
|
||
func writeCronError(c *gin.Context, err error) {
|
||
switch {
|
||
case errors.Is(err, ErrCronTitleRequired), errors.Is(err, ErrCronScheduleRequired):
|
||
httpx.Error(c, http.StatusBadRequest, "invalid_request", "计划任务参数无效")
|
||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||
httpx.Error(c, http.StatusNotFound, "not_found", "项目不存在")
|
||
default:
|
||
httpx.Error(c, http.StatusInternalServerError, "internal_error", "计划任务创建失败")
|
||
}
|
||
}
|
||
|
||
func cronPlanDTO(plan models.SenlinAgentCronPlan) CronPlanDTO {
|
||
return CronPlanDTO{
|
||
ID: plan.Identity, ProjectID: plan.ProjectIdentity, Title: plan.Title, Schedule: plan.Schedule,
|
||
NextRunAt: projectUTCOptionalTime(plan.NextRunAt), Enabled: plan.Enabled, LastResult: plan.LastResult,
|
||
CreatedAt: plan.CreatedAt.UTC(), UpdatedAt: plan.UpdatedAt.UTC(),
|
||
}
|
||
}
|
||
|
||
func parseCronOptionalTime(value string) (*time.Time, error) {
|
||
trimmed := strings.TrimSpace(value)
|
||
if trimmed == "" {
|
||
return nil, nil
|
||
}
|
||
parsed, err := time.Parse(time.RFC3339, trimmed)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
parsed = parsed.UTC()
|
||
return &parsed, nil
|
||
}
|
||
|
||
func projectUTCOptionalTime(value *time.Time) *time.Time {
|
||
if value == nil {
|
||
return nil
|
||
}
|
||
result := value.UTC()
|
||
return &result
|
||
}
|