2026-07-21 15:27:11 +08:00
|
|
|
|
package projects
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-07-21 16:12:20 +08:00
|
|
|
|
"errors"
|
2026-07-21 15:27:11 +08:00
|
|
|
|
"net/http"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2026-07-21 16:12:20 +08:00
|
|
|
|
"gorm.io/gorm"
|
2026-07-21 15:27:11 +08:00
|
|
|
|
"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 {
|
2026-07-21 16:12:20 +08:00
|
|
|
|
writeCronError(c, err)
|
2026-07-21 15:27:11 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusCreated, cronPlanDTO(*plan))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 16:12:20 +08:00
|
|
|
|
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", "计划任务创建失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 15:27:11 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|