package backthink import ( "context" "time" ) // 本文件承载心跳节拍(design/12 §N A):安静窗口内降频、窗口外恢复。 // // 两条不变量(照 design/12 §N 写死,改它必须先改设计): // - **窗口只降低周期性心跳的频率**;事件驱动通路(onEvent → think) // 完全不经这里,因此它照旧立刻触发、照样穿透安静时段(§N A2)。 // - **寂静 ≠ 停摆**:`QuietInterval ≤ 0` 只表示「关闭降频」(回落普通间隔), // 绝不表示「半夜完全不思考」(§N A3)。 // quietHourMin 与 quietHourMax 是安静时段钟点的合法取值域(0..23)。 // 越界即视为窗口非法 ⇒ 不降频(design/12 §N A3)。 const ( quietHourMin = 0 quietHourMax = 23 ) // quietIntervalEnabled 报告是否配置了降频间隔。`≤0` = 关闭降频: // 这是「回落普通间隔」而不是「半夜不思考」,故这里只做一次符号判定。 func (b *Backthink) quietIntervalEnabled() bool { return b.cfg.QuietInterval > 0 } // quietWindowDisabled 报告安静窗口是否**不可用**(两端相等非 0,或钟点非法)。 // 口径与既有三处一致之处在于「相等非 0 = 关闭」;「非法钟点」是本服务按 // design/12 §N A3 补的:越界钟点在既有 inQuietHours 里会算出荒谬窗口 // (如 5..25 恒真),把降频错误地全时段打开,因此这里先判非法 ⇒ 不降频。 func (b *Backthink) quietWindowDisabled() bool { start, end := b.cfg.QuietStartHour, b.cfg.QuietEndHour if !validQuietHour(start) || !validQuietHour(end) { return true } return start == end } // validQuietHour 报告钟点是否落在合法取值域 0..23(含两端)。 func validQuietHour(hour int) bool { return hour >= quietHourMin && hour <= quietHourMax } // heartbeatInterval 按**当前时刻**选心跳间隔:落在安静窗口内用 // QuietInterval,否则用 Interval(design/12 §N A1)。 // // 它是纯函数式的(只读 cfg 与入参),因此 tickLoop 可以在**每一拍之前** // 重算它——跨过窗口边界后下一拍自动切换,而不是启动时定死一次(§N A3)。 // 安静判定直接复用既有的 inQuietHours,跨午夜口径与另外三处逐字一致。 func (b *Backthink) heartbeatInterval(at time.Time) time.Duration { if !b.quietIntervalEnabled() || b.quietWindowDisabled() { return b.cfg.Interval } if b.inQuietHours(at) { return b.cfg.QuietInterval } return b.cfg.Interval } // tickLoop 是心跳 goroutine。按裁决 07.4.4(design/12 §1),它在进程 // 启动时立即思考一次——产出一句拟人化的启动问候—— // 随后稳定为每拍一轮思考;ctx 被取消时退出。可接受的副作用是每次进程重启都会唤醒 // 一轮即时思考。 // // 节拍在**每拍前**用 heartbeatInterval(b.now()) 重算(design/12 §N A3): // 因此心跳在安静窗口内降频、出窗口后自动恢复。用 time.Timer 而非 // 固定 Ticker 正是为了这个「每拍换间隔」的能力。 func (b *Backthink) tickLoop(ctx context.Context) { defer b.wg.Done() // 启动即思考:不等满一个心跳周期。已取消的上下文直接退出,不落一条失败的 // 启动记录。 select { case <-ctx.Done(): return default: } b.think(ctx, startupTrigger()) for { timer := time.NewTimer(b.heartbeatInterval(b.now())) select { case <-ctx.Done(): timer.Stop() return case <-timer.C: b.think(ctx, heartbeatTrigger()) } } }