feat(backend): add cron job REST API (list, add, delete, enable, disable)

This commit is contained in:
anthrodjear 2026-05-07 13:39:19 +03:00
parent 8dd22efe65
commit 9ae9583be6
2 changed files with 158 additions and 0 deletions

152
web/backend/api/cron.go Normal file
View file

@ -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"})
}

View file

@ -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.