Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions cmd/picoclaw/internal/gateway/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,14 @@ func setupCronTool(
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")

// Create cron service
cronService := cron.NewCronService(cronStorePath, nil)

cronService := cron.NewCronService(
cronStorePath,
nil,
cron.CronConfig{
ExecTimeoutMinutes: cfg.Tools.Cron.ExecTimeoutMinutes,
DefaultTimezone: cfg.Tools.Cron.DefaultTimezone,
},
)
// Create and register CronTool
cronTool, err := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, cfg)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion config/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,8 @@
"proxy": ""
},
"cron": {
"exec_timeout_minutes": 5
"exec_timeout_minutes": 5,
"default_timezone": "Asia/Shanghai"
},
"mcp": {
"enabled": false,
Expand Down
3 changes: 2 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,8 @@ type WebToolsConfig struct {
}

type CronToolsConfig struct {
ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
DefaultTimezone string `json:"default_timezone" env:"PICOCLAW_TOOLS_CRON_DEFAULT_TIMEZONE"`
}

type ExecConfig struct {
Expand Down
49 changes: 36 additions & 13 deletions pkg/cron/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import (

"github.com/sipeed/picoclaw/pkg/fileutil"
)
type CronConfig struct {
ExecTimeoutMinutes int `json:"exec_timeout_minutes,omitempty"`
DefaultTimezone string `json:"default_timezone,omitempty"`
}

type CronSchedule struct {
Kind string `json:"kind"`
Expand Down Expand Up @@ -59,20 +63,22 @@ type CronStore struct {
type JobHandler func(job *CronJob) (string, error)

type CronService struct {
storePath string
store *CronStore
onJob JobHandler
mu sync.RWMutex
running bool
stopChan chan struct{}
storePath string
store *CronStore
onJob JobHandler
mu sync.RWMutex
running bool
stopChan chan struct{}
gronx *gronx.Gronx
config CronConfig
}

func NewCronService(storePath string, onJob JobHandler) *CronService {
func NewCronService(storePath string, onJob JobHandler, config CronConfig) *CronService {
cs := &CronService{
storePath: storePath,
onJob: onJob,
gronx: gronx.New(),
config: config,
}
// Initialize and load store on creation
cs.loadStore()
Expand Down Expand Up @@ -263,16 +269,33 @@ func (cs *CronService) computeNextRun(schedule *CronSchedule, nowMS int64) *int6
if schedule.Expr == "" {
return nil
}

// Use gronx to calculate next run time
now := time.UnixMilli(nowMS)

// 3-level fallback: schedule.TZ > config default_timezone > "Asia/Shanghai"
timezoneStr := schedule.TZ
if timezoneStr == "" {
timezoneStr = cs.config.DefaultTimezone
}
if timezoneStr == "" {
timezoneStr = "Asia/Shanghai" // Default fallback
}

// Load the target timezone
targetTZ, err := time.LoadLocation(timezoneStr)
if err != nil {
log.Printf("[cron] failed to load timezone '%s', falling back to UTC: %v", timezoneStr, err)
targetTZ = time.UTC // fallback to UTC on error
}

// Use gronx to calculate next run time based on target timezone
now := time.UnixMilli(nowMS).In(targetTZ)
nextTime, err := gronx.NextTickAfter(schedule.Expr, now, false)
if err != nil {
log.Printf("[cron] failed to compute next run for expr '%s': %v", schedule.Expr, err)
log.Printf("[cron] failed to compute next run for expr '%s' in timezone '%s': %v", schedule.Expr, timezoneStr, err)
return nil
}

nextMS := nextTime.UnixMilli()

// Convert the calculated next time back to UTC Unix milli for storage
nextMS := nextTime.UTC().UnixMilli()
return &nextMS
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/tools/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ func (t *CronTool) addJob(args map[string]any) *ToolResult {
Kind: "cron",
Expr: cronExpr,
}

// Get timezone if present in args
if tz, ok := args["timezone"].(string); ok && tz != "" {
schedule.TZ = tz // Set timezone on cron schedule
}
} else {
return ErrorResult("one of at_seconds, every_seconds, or cron_expr is required")
}
Expand Down