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 }