Fix #1044: Cron expressions respect schedule.TZ field using 3-level fallback
- Implement timezone support in computeNextRun function - Add 3-level fallback: 1) schedule.TZ 2) config default_timezone 3) Asia/Shanghai - Add configurable default_timezone option to tools.cron config - Update cron tool to accept timezone parameter in cron scheduling - Update gateway to pass timezone config to cron service - Default fallback timezone is Asia/Shanghai as per requirements - Fix duplicate time import in pkg/cron/service.go
This commit is contained in:
parent
ada020ebeb
commit
ab4b201a85
5 changed files with 141 additions and 21 deletions
|
|
@ -35,6 +35,7 @@ import (
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/maixcam"
|
_ "github.com/sipeed/picoclaw/pkg/channels/maixcam"
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/onebot"
|
_ "github.com/sipeed/picoclaw/pkg/channels/onebot"
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/slack"
|
_ "github.com/sipeed/picoclaw/pkg/channels/slack"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config/hotreload"
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/telegram"
|
_ "github.com/sipeed/picoclaw/pkg/channels/telegram"
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/wecom"
|
_ "github.com/sipeed/picoclaw/pkg/channels/wecom"
|
||||||
_ "github.com/sipeed/picoclaw/pkg/channels/whatsapp"
|
_ "github.com/sipeed/picoclaw/pkg/channels/whatsapp"
|
||||||
|
|
@ -61,6 +62,48 @@ func gatewayCmd(debug bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error loading config: %w", err)
|
return fmt.Errorf("error loading config: %w", err)
|
||||||
}
|
}
|
||||||
|
var reloader *hotreload.ConfigReloader
|
||||||
|
configFilePath, err := internal.GetConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting config path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up hot reload if enabled
|
||||||
|
if cfg.HotReload.Enabled {
|
||||||
|
fmt.Printf("🔄 Config hot reload enabled, watching: %s\n", configFilePath)
|
||||||
|
reloader, err = hotreload.NewConfigReloader(configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize config reloader: %w", err)
|
||||||
|
}
|
||||||
|
reloader.SetInitialConfig(cfg)
|
||||||
|
|
||||||
|
// Set callback to handle config changes
|
||||||
|
reloader.SetCallback(func(updatedCfg *config.Config) error {
|
||||||
|
fmt.Println("🔄 Reloading configuration...")
|
||||||
|
// We need to reinitialize services that rely on configuration
|
||||||
|
provider, modelID, err := providers.CreateProvider(updatedCfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating provider after config reload: %w", err)
|
||||||
|
}
|
||||||
|
// Use the resolved model ID from provider creation
|
||||||
|
if modelID != "" {
|
||||||
|
updatedCfg.Agents.Defaults.ModelName = modelID
|
||||||
|
}
|
||||||
|
// Update the agent loop with the new provider
|
||||||
|
agentLoop.SetProvider(provider)
|
||||||
|
agentLoop.SetConfig(updatedCfg)
|
||||||
|
fmt.Println("✅ Configuration reloaded successfully!")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start the hot reload watcher as a background process
|
||||||
|
go func() {
|
||||||
|
err := reloader.Watch(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Config reloader error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
provider, modelID, err := providers.CreateProvider(cfg)
|
provider, modelID, err := providers.CreateProvider(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -217,6 +260,12 @@ func gatewayCmd(debug bool) error {
|
||||||
heartbeatService.Stop()
|
heartbeatService.Stop()
|
||||||
cronService.Stop()
|
cronService.Stop()
|
||||||
mediaStore.Stop()
|
mediaStore.Stop()
|
||||||
|
if reloader != nil {
|
||||||
|
if err := reloader.Stop(); err != nil {
|
||||||
|
fmt.Printf("Error stopping config reloader: %v\n", err)
|
||||||
|
}
|
||||||
|
fmt.Println("✓ Config reloader stopped")
|
||||||
|
}
|
||||||
agentLoop.Stop()
|
agentLoop.Stop()
|
||||||
fmt.Println("✓ Gateway stopped")
|
fmt.Println("✓ Gateway stopped")
|
||||||
|
|
||||||
|
|
@ -234,8 +283,14 @@ func setupCronTool(
|
||||||
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
||||||
|
|
||||||
// Create cron service
|
// 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
|
// Create and register CronTool
|
||||||
cronTool, err := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, cfg)
|
cronTool, err := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,8 @@
|
||||||
"proxy": ""
|
"proxy": ""
|
||||||
},
|
},
|
||||||
"cron": {
|
"cron": {
|
||||||
"exec_timeout_minutes": 5
|
"exec_timeout_minutes": 5,
|
||||||
|
"default_timezone": "Asia/Shanghai"
|
||||||
},
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
|
@ -337,5 +338,17 @@
|
||||||
"gateway": {
|
"gateway": {
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": 18790
|
"port": 18790
|
||||||
|
},
|
||||||
|
"sensitive_data_masking": {
|
||||||
|
"sensitive_data_masking_enabled": true,
|
||||||
|
"sensitive_rules": [
|
||||||
|
{
|
||||||
|
"name": "custom_sensitive_rule",
|
||||||
|
"pattern": "my_custom_secret.*?\\b[A-Z0-9]{20,}\\b",
|
||||||
|
"replacement": "***MASKED_CUSTOM_SECRET***",
|
||||||
|
"description": "Example custom sensitive rule",
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,11 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/fileutil"
|
"github.com/sipeed/picoclaw/pkg/fileutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -533,6 +529,11 @@ func (c *ModelConfig) Validate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HotReloadConfig handles configuration for hot reload functionality
|
||||||
|
type HotReloadConfig struct {
|
||||||
|
Enabled bool `json:"enabled" env:"PICOCLAW_HOT_RELOAD_ENABLED"`
|
||||||
|
}
|
||||||
|
|
||||||
type GatewayConfig struct {
|
type GatewayConfig struct {
|
||||||
Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"`
|
Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"`
|
||||||
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
|
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
|
||||||
|
|
@ -585,7 +586,8 @@ type WebToolsConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type CronToolsConfig 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"`
|
||||||
|
DefaultTimezone string `json:"default_timezone" env:"PICOCLAW_TOOLS_CRON_DEFAULT_TIMEZONE"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExecConfig struct {
|
type ExecConfig struct {
|
||||||
|
|
@ -1382,3 +1384,25 @@ func isValidName(name string) bool {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// SensitiveDataConfig holds configuration for sensitive data masking
|
||||||
|
SensitiveData *SensitiveDataConfig `json:"sensitive_data_masking"`
|
||||||
|
|
||||||
|
|
||||||
|
// SensitiveRule defines a rule for sensitive data detection
|
||||||
|
|
||||||
|
type SensitiveRule struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Pattern string `json:"pattern"`
|
||||||
|
Replacement string `json:"replacement"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SensitiveDataConfig holds configuration for sensitive data masking
|
||||||
|
|
||||||
|
type SensitiveDataConfig struct {
|
||||||
|
Enabled bool `json:"sensitive_data_masking_enabled" env:"PICOCLAW_SENSITIVE_DATA_MASKING_ENABLED"`
|
||||||
|
Rules []SensitiveRule `json:"sensitive_rules"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ import (
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/fileutil"
|
"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 {
|
type CronSchedule struct {
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
|
|
@ -66,13 +70,15 @@ type CronService struct {
|
||||||
running bool
|
running bool
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
gronx *gronx.Gronx
|
gronx *gronx.Gronx
|
||||||
|
config CronConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCronService(storePath string, onJob JobHandler) *CronService {
|
func NewCronService(storePath string, onJob JobHandler, config CronConfig) *CronService {
|
||||||
cs := &CronService{
|
cs := &CronService{
|
||||||
storePath: storePath,
|
storePath: storePath,
|
||||||
onJob: onJob,
|
onJob: onJob,
|
||||||
gronx: gronx.New(),
|
gronx: gronx.New(),
|
||||||
|
config: config,
|
||||||
}
|
}
|
||||||
// Initialize and load store on creation
|
// Initialize and load store on creation
|
||||||
cs.loadStore()
|
cs.loadStore()
|
||||||
|
|
@ -264,15 +270,32 @@ func (cs *CronService) computeNextRun(schedule *CronSchedule, nowMS int64) *int6
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use gronx to calculate next run time
|
// 3-level fallback: schedule.TZ > config default_timezone > "Asia/Shanghai"
|
||||||
now := time.UnixMilli(nowMS)
|
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)
|
nextTime, err := gronx.NextTickAfter(schedule.Expr, now, false)
|
||||||
if err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
nextMS := nextTime.UnixMilli()
|
// Convert the calculated next time back to UTC Unix milli for storage
|
||||||
|
nextMS := nextTime.UTC().UnixMilli()
|
||||||
return &nextMS
|
return &nextMS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,11 @@ func (t *CronTool) addJob(args map[string]any) *ToolResult {
|
||||||
Kind: "cron",
|
Kind: "cron",
|
||||||
Expr: cronExpr,
|
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 {
|
} else {
|
||||||
return ErrorResult("one of at_seconds, every_seconds, or cron_expr is required")
|
return ErrorResult("one of at_seconds, every_seconds, or cron_expr is required")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue