From 9ae9583be6292fda3b035500e504a0c7e77bea45 Mon Sep 17 00:00:00 2001 From: anthrodjear Date: Thu, 7 May 2026 13:39:19 +0300 Subject: [PATCH] feat(backend): add cron job REST API (list, add, delete, enable, disable) --- web/backend/api/cron.go | 152 ++++++++++++++++++++++++++++++++++++++ web/backend/api/router.go | 6 ++ 2 files changed, 158 insertions(+) create mode 100644 web/backend/api/cron.go diff --git a/web/backend/api/cron.go b/web/backend/api/cron.go new file mode 100644 index 000000000..add3a0464 --- /dev/null +++ b/web/backend/api/cron.go @@ -0,0 +1,152 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/cron" +) + +// registerCronRoutes registers cron job API routes on the ServeMux. +func (h *Handler) registerCronRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/cron/jobs", h.handleListCronJobs) + mux.HandleFunc("POST /api/cron/jobs", h.handleAddCronJob) + mux.HandleFunc("DELETE /api/cron/jobs/{id}", h.handleDeleteCronJob) + mux.HandleFunc("POST /api/cron/jobs/{id}/enable", h.handleEnableCronJob) + mux.HandleFunc("POST /api/cron/jobs/{id}/disable", h.handleDisableCronJob) +} + +type cronJobResponse struct { + Jobs []cron.CronJob `json:"jobs"` +} + +type cronAddRequest struct { + Name string `json:"name" binding:"required"` + EveryMS *int64 `json:"every_ms,omitempty"` + CronExpr string `json:"cron_expr,omitempty"` + Message string `json:"message" binding:"required"` + Channel string `json:"channel,omitempty"` + To string `json:"to,omitempty"` +} + +// handleListCronJobs lists all cron jobs from the cron service. +func (h *Handler) handleListCronJobs(w http.ResponseWriter, r *http.Request) { + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + storePath := cfg.WorkspacePath() + "/cron/jobs.json" + cs := cron.NewCronService(storePath, nil) + if err := cs.Load(); err != nil { + http.Error(w, fmt.Sprintf("Failed to load cron store: %v", err), http.StatusInternalServerError) + return + } + jobs := cs.ListJobs(true) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(cronJobResponse{Jobs: jobs}) +} + +// handleAddCronJob adds a new cron job. +func (h *Handler) handleAddCronJob(w http.ResponseWriter, r *http.Request) { + var req cronAddRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) + return + } + if req.Name == "" || req.Message == "" { + http.Error(w, "name and message are required", http.StatusBadRequest) + return + } + var schedule cron.CronSchedule + if req.EveryMS != nil { + schedule = cron.CronSchedule{Kind: "every", EveryMS: req.EveryMS} + } else if req.CronExpr != "" { + schedule = cron.CronSchedule{Kind: "cron", Expr: req.CronExpr} + } else { + http.Error(w, "either every_ms or cron_expr must be specified", http.StatusBadRequest) + return + } + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + storePath := cfg.WorkspacePath() + "/cron/jobs.json" + cs := cron.NewCronService(storePath, nil) + job, err := cs.AddJob(req.Name, schedule, req.Message, req.Channel, req.To) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to add job: %v", err), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{"job": job, "status": "ok"}) +} + +// handleDeleteCronJob deletes a cron job by ID. +func (h *Handler) handleDeleteCronJob(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if id == "" { + http.Error(w, "job id is required", http.StatusBadRequest) + return + } + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + storePath := cfg.WorkspacePath() + "/cron/jobs.json" + cs := cron.NewCronService(storePath, nil) + if removed := cs.RemoveJob(id); !removed { + http.Error(w, "job not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +// handleEnableCronJob enables a cron job by ID. +func (h *Handler) handleEnableCronJob(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if id == "" { + http.Error(w, "job id is required", http.StatusBadRequest) + return + } + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + storePath := cfg.WorkspacePath() + "/cron/jobs.json" + cs := cron.NewCronService(storePath, nil) + if job := cs.EnableJob(id, true); job == nil { + http.Error(w, "job not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +// handleDisableCronJob disables a cron job by ID. +func (h *Handler) handleDisableCronJob(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if id == "" { + http.Error(w, "job id is required", http.StatusBadRequest) + return + } + cfg, err := config.LoadConfig(h.configPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError) + return + } + storePath := cfg.WorkspacePath() + "/cron/jobs.json" + cs := cron.NewCronService(storePath, nil) + if job := cs.EnableJob(id, false); job == nil { + http.Error(w, "job not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} \ No newline at end of file diff --git a/web/backend/api/router.go b/web/backend/api/router.go index 76f63607e..fd5608bd4 100644 --- a/web/backend/api/router.go +++ b/web/backend/api/router.go @@ -107,6 +107,12 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) { // WeCom QR login flow h.registerWecomRoutes(mux) + + // Agent management + h.registerAgentRoutes(mux) + + // Cron job management + h.registerCronRoutes(mux) } // Shutdown gracefully shuts down the handler, stopping the gateway if it was started by this handler.