Files
AskaEth d680390300 feat(memory): 整理频率走面板配置(面板 > env > 默认)+ 分时段动态间隔
主人 2026-10-07:「.env 只做兜底…优先从面板配置上取」+「频率必须调…或者动态
分配,比如白天间隔短一点 晚上到了静默时期稍微长一点」。

- memory-tidy.json 新增四键:intervalSec / quietIntervalSec / quietStartHour /
  quietEndHour(*int + 指针判定 => 显式 0 是合法值、缺位才回落)
- 优先级逐键三档:面板 > env(既有键,未增删)> 代码默认;坏值不覆盖 + WARN,
  且不启动失败
- 动态间隔由纯函数 intervalAt(at) 决定,调度侧每一拍现算 => 跑完按当时时段重算
- 🔴 语义变更(有意,代码注释已登记,待补设计修订):安静时段从「唯一可跑时段」
  变成「降频时段」—— 旧的「安静窗外一律 continue」被删,否则 intervalSec 永远
  是空壳(白天唤醒必被跳过);窗口仍决定档位与补跑判据
- 6 个变异(含「加回旧的那道门」=> 端到端用例红)
2026-10-07 19:21:32 +08:00

88 lines
4.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package memory
import (
"log/slog"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
)
// 本文件是**整理频率**的解析点:把 `$DATA_DIR/config/memory-tidy.json` 的四个调度
// 旋钮叠到 env 侧的 TidyConfig 上,实现主人 2026-10-07 的两条口径:
//
// - 「**`.env` 只做兜底用** —— 后端自动创建独立的面板配置文件之后就走面板配置
// 文件,这些键优先从面板配置上取」⇒ 逐键 **面板 > `.env` > 代码默认**;
// - 「**频率必须调**,可以调到 1 小时或者三小时,**或者动态分配**」⇒ 周期分两档
// (非安静时段的 intervalSec / 安静时段的 quietIntervalSec,见 intervalAt)。
//
// 为什么解析放在 memory 而不是 runtimeconfig:runtimeconfig 只负责**文件形状与
// 读取**(它的头注释把这条边界写死了),而「缺位/坏值怎么回落」属于 TidyConfig 的
// 归一化语义 —— 与 normalizeTidyConfig 同一层,因此两者可以一起被单测覆盖。
//
// 为什么不是「读盘时逐键容错」:整份文件解不开(含某键类型写错)时**四个键一起**
// 回落 `.env` 并记 WARN,这是本文件家族既有的粒度(见
// runtimeconfig.LoadMemoryTidyTuning 与它的用例③);本文件处理的是**类型正确但值
// 不可用**(非正数)那一档。
// ApplyTidyScheduleTuning 把面板调度旋钮叠到 env 侧的 TidyConfig 上,返回叠加后的
// 配置(**未归一化**:调用方随后交给 NewTidier / normalizeTidyConfig 填代码默认值,
// 与组合根的既有顺序一致)。
//
// 逐键独立,三档优先级在四个键上各自成立:
//
// 键 面板(memory-tidy.json) 下一档(.env / 派生) 代码默认
// intervalSec intervalSec MEMORY_TIDY_INTERVAL_SEC 86400
// quietIntervalSec quietIntervalSec 派生:= intervalSec 同左
// quietStartHour quietStartHour MEMORY_TIDY_QUIET_START_HOUR 23
// quietEndHour quietEndHour MEMORY_TIDY_QUIET_END_HOUR 7
//
// 「面板缺位 ⇒ 用 `.env`」这一步在实现上是**不覆盖**:cfg 进函数时已经带着 env 的
// 值(TidyConfigFromConfig),面板没写就原样留着;「`.env` 也缺位 ⇒ 代码默认」由
// 随后的 normalizeTidyConfig 收尾(env 的 default 标签在 config.Load 那一层已经
// 兜过一道,两道都缺时落到 defaultTidyIntervalSec / 23-07)。
//
// 坏值(非正数的秒数)**绝不写进 cfg**:回落下一档 + 记一条 WARN。一个 ≤0 的周期会
// 让心跳退化成空转,因此它不是「没配」而是坏配置 —— 但也不许因此让启动失败。
func ApplyTidyScheduleTuning(cfg TidyConfig, panel runtimeconfig.MemoryTidyTuning, log *slog.Logger) TidyConfig {
log = logger.WithSource(baseTidyLog(log), "memory/tidy")
if sec := panelPositiveSec(panel.IntervalSec, "intervalSec", log); sec > 0 {
cfg.IntervalSec = sec
// Interval 与 IntervalSec 同时归零:两者是「同一旋钮的两种写法」
// (设置了 Interval 时忽略 IntervalSec),只改一个会让旧值报复性生效。
cfg.Interval = 0
}
if sec := panelPositiveSec(panel.QuietIntervalSec, "quietIntervalSec", log); sec > 0 {
cfg.QuietIntervalSec = sec
cfg.QuietInterval = 0
}
// 钟点用「指针是否有值」而不是符号判据:**0 是合法钟点**(0-7 的凌晨窗口、
// 23-0 到午夜的窗口),因此显式写的 0 必须原样生效,缺位才回落 env。
if panel.QuietStartHour != nil {
cfg.QuietStartHour = *panel.QuietStartHour
}
if panel.QuietEndHour != nil {
cfg.QuietEndHour = *panel.QuietEndHour
}
return cfg
}
// panelPositiveSec 解析一个**正数秒数**旋钮,返回 0 表示「不覆盖」(调用方原样留着
// env 值)。三种情形分得很清,因为它们该有不同的观测形状:
//
// - `nil`(键没写)⇒ 0,**静默**:没配不是错,正是「`.env` 兜底」这一档;
// - 非正数(写了 0 或负数)⇒ 0 + **WARN**:它会被读成「一直跑」而不是「不跑」,
// 属于坏配置,必须留下痕迹(否则主人以为配了 0 就等于关了,实际相反);
// - 正数 ⇒ 原值。
func panelPositiveSec(v *int, key string, log *slog.Logger) int {
if v == nil {
return 0
}
if *v <= 0 {
log.Warn("memory-tidy.json 的调度旋钮值不可用,回落 .env(非正周期会让心跳空转)",
"key", key, "value", *v)
return 0
}
return *v
}