package config import ( "encoding/json" "fmt" "os" "sync" ) // ScheduleRule defines a time-based interval rule. type ScheduleRule struct { Name string `json:"name"` Days []string `json:"days"` // monday, tuesday, wednesday, thursday, friday, saturday, sunday TimeRange string `json:"time_range"` // "HH:MM-HH:MM" Except []string `json:"except"` // ["HH:MM-HH:MM", ...] IntervalMinutes int `json:"interval_minutes"` } // ThinkingScheduleConfig is the full schedule configuration. type ThinkingScheduleConfig struct { Version string `json:"version"` DefaultIntervalMinutes int `json:"default_interval_minutes"` Rules []ScheduleRule `json:"rules"` } // DefaultThinkingScheduleConfig returns the default schedule with two rules. func DefaultThinkingScheduleConfig() *ThinkingScheduleConfig { return &ThinkingScheduleConfig{ Version: "1.0", DefaultIntervalMinutes: 5, Rules: []ScheduleRule{ { Name: "night", Days: []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, TimeRange: "23:00-07:00", IntervalMinutes: 30, }, { Name: "weekday_work", Days: []string{"monday", "tuesday", "wednesday", "thursday", "friday"}, TimeRange: "09:00-17:00", Except: []string{"12:00-14:00", "15:00-15:30"}, IntervalMinutes: 30, }, }, } } // ThinkingScheduleStore persists the schedule config to a JSON file. type ThinkingScheduleStore struct { mu sync.RWMutex path string config *ThinkingScheduleConfig } // NewThinkingScheduleStore creates a store, creating the file with defaults if it does not exist. func NewThinkingScheduleStore(path string) (*ThinkingScheduleStore, error) { s := &ThinkingScheduleStore{ path: path, config: nil, } if err := s.load(); err != nil { return nil, err } return s, nil } func (s *ThinkingScheduleStore) load() error { data, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { s.config = DefaultThinkingScheduleConfig() return s.save() } return fmt.Errorf("read thinking schedule file: %w", err) } if len(data) == 0 { s.config = DefaultThinkingScheduleConfig() return s.save() } var cfg ThinkingScheduleConfig if err := json.Unmarshal(data, &cfg); err != nil { return fmt.Errorf("parse thinking schedule: %w", err) } if cfg.Version == "" { cfg.Version = "1.0" } if cfg.DefaultIntervalMinutes <= 0 { cfg.DefaultIntervalMinutes = 5 } if cfg.Rules == nil { cfg.Rules = []ScheduleRule{} } s.config = &cfg return nil } func (s *ThinkingScheduleStore) save() error { data, err := json.MarshalIndent(s.config, "", " ") if err != nil { return fmt.Errorf("marshal thinking schedule: %w", err) } tmpPath := s.path + ".tmp" if err := os.WriteFile(tmpPath, data, 0640); err != nil { return fmt.Errorf("write thinking schedule: %w", err) } return os.Rename(tmpPath, s.path) } // GetConfig returns the current config (read-only). func (s *ThinkingScheduleStore) GetConfig() *ThinkingScheduleConfig { s.mu.RLock() defer s.mu.RUnlock() return s.config } // SetConfig validates and persists a new config. func (s *ThinkingScheduleStore) SetConfig(cfg *ThinkingScheduleConfig) error { if cfg == nil { return fmt.Errorf("配置不能为空") } if cfg.DefaultIntervalMinutes <= 0 { cfg.DefaultIntervalMinutes = 5 } if cfg.Version == "" { cfg.Version = "1.0" } if cfg.Rules == nil { cfg.Rules = []ScheduleRule{} } for _, r := range cfg.Rules { if r.IntervalMinutes <= 0 { return fmt.Errorf("规则 %q 间隔分钟必须大于 0", r.Name) } if r.TimeRange == "" { return fmt.Errorf("规则 %q 缺少 time_range", r.Name) } } s.mu.Lock() defer s.mu.Unlock() s.config = cfg return s.save() } // HasConfig returns true if a config is loaded. func (s *ThinkingScheduleStore) HasConfig() bool { s.mu.RLock() defer s.mu.RUnlock() return s.config != nil }