package background import ( "encoding/json" "fmt" "log" "os" "strconv" "strings" "sync" "time" ) // ScheduleRule defines a time-based interval rule. type ScheduleRule struct { Name string `json:"name"` Days []string `json:"days"` TimeRange string `json:"time_range"` Except []string `json:"except"` 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"` } // ScheduleLoader loads the thinking schedule from a JSON file and calculates // the current interval based on time of day and day of week. type ScheduleLoader struct { mu sync.RWMutex path string config *ThinkingScheduleConfig } // NewScheduleLoader creates a loader. Returns nil config if the file does not exist. func NewScheduleLoader(path string) (*ScheduleLoader, error) { l := &ScheduleLoader{path: path} if err := l.load(); err != nil { return l, err } return l, nil } func (l *ScheduleLoader) load() error { data, err := os.ReadFile(l.path) if err != nil { if os.IsNotExist(err) { l.config = nil return nil } return fmt.Errorf("read thinking schedule: %w", err) } if len(data) == 0 { l.config = nil return nil } var cfg ThinkingScheduleConfig if err := json.Unmarshal(data, &cfg); err != nil { l.config = nil return fmt.Errorf("parse thinking schedule: %w", err) } l.mu.Lock() l.config = &cfg l.mu.Unlock() log.Printf("[思考调度] 已加载配置文件: version=%s, default=%dmin, rules=%d", cfg.Version, cfg.DefaultIntervalMinutes, len(cfg.Rules)) return nil } // HasConfig returns true if a schedule config was loaded from file. func (l *ScheduleLoader) HasConfig() bool { l.mu.RLock() defer l.mu.RUnlock() return l.config != nil } // GetInterval returns the thinking interval in minutes for the given time. // Returns 0 if no schedule is loaded (caller should use default). func (l *ScheduleLoader) GetInterval(now time.Time) int { l.mu.RLock() defer l.mu.RUnlock() if l.config == nil { return 0 } weekday := strings.ToLower(now.Weekday().String()) // monday, tuesday, ... currentMinutes := now.Hour()*60 + now.Minute() for _, rule := range l.config.Rules { if !matchDay(weekday, rule.Days) { continue } if !matchTimeRange(currentMinutes, rule.TimeRange) { continue } if matchExceptRange(currentMinutes, rule.Except) { continue } return rule.IntervalMinutes } return l.config.DefaultIntervalMinutes } // matchDay checks if the current weekday is in the rule's days list. func matchDay(currentDay string, days []string) bool { for _, d := range days { if strings.ToLower(d) == currentDay { return true } } return false } // matchTimeRange checks if currentMinutes (0-1439) falls within the time range. // Supports overnight ranges (e.g., 23:00-07:00 where start > end). func matchTimeRange(currentMinutes int, timeRange string) bool { start, end, ok := parseTimeRange(timeRange) if !ok { return false } if start <= end { return currentMinutes >= start && currentMinutes < end } // Overnight range return currentMinutes >= start || currentMinutes < end } // matchExceptRange returns true if currentMinutes falls in any except range. func matchExceptRange(currentMinutes int, exceptRanges []string) bool { for _, er := range exceptRanges { start, end, ok := parseTimeRange(er) if !ok { continue } if start <= end { if currentMinutes >= start && currentMinutes < end { return true } } else { if currentMinutes >= start || currentMinutes < end { return true } } } return false } // parseTimeRange parses "HH:MM-HH:MM" into start and end minutes from midnight. func parseTimeRange(r string) (int, int, bool) { parts := strings.SplitN(r, "-", 2) if len(parts) != 2 { return 0, 0, false } start, ok := parseHM(strings.TrimSpace(parts[0])) if !ok { return 0, 0, false } end, ok := parseHM(strings.TrimSpace(parts[1])) if !ok { return 0, 0, false } return start, end, true } // parseHM parses "HH:MM" into minutes from midnight. func parseHM(s string) (int, bool) { parts := strings.SplitN(s, ":", 2) if len(parts) != 2 { return 0, false } h, err := strconv.Atoi(parts[0]) if err != nil || h < 0 || h > 23 { return 0, false } m, err := strconv.Atoi(parts[1]) if err != nil || m < 0 || m > 59 { return 0, false } return h*60 + m, true }