feat(memory): 整理频率走面板配置(面板 > env > 默认)+ 分时段动态间隔

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

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

No files matched your search

+52 -2
View File
@@ -23,6 +23,23 @@
// 额度出口。额度必须能按现网节奏调(且不许为它开 env 键),因此落在这里;
// **零值不表示「无限制」**,只表示「没配、用代码默认值」。
//
// 2026-10-07 再追加 `intervalSec` / `quietIntervalSec` / `quietStartHour` /
// `quietEndHour`(**整理频率**·主人两条口径:「**`.env` 只做兜底用** —— 后端自动
// 创建独立的面板配置文件之后就走面板配置文件,这些键优先从面板配置上取」;
// 「**频率必须调**,可以调到 1 小时或者三小时,**或者动态分配**,比如白天间隔短
// 一点,晚上到了静默时期稍微长一点」):
//
// - 这四项**逐键**走「面板 > `.env` > 代码默认」三档(唯一实现点是
// `memory.ApplyTidyScheduleTuning`):面板键在 ⇒ 用面板值;缺位 ⇒ 用 `.env`
// 的 `MEMORY_TIDY_INTERVAL_SEC` / `MEMORY_TIDY_QUIET_*_HOUR`(quietIntervalSec
// 没有 env 键,缺位 = 等于 `intervalSec`);两档都没有 ⇒ 代码默认
// (86400 / 23:00-07:00,见 memory 侧的 normalizeTidyConfig)。
// - 因此**没有新增任何 `.env` 键**(`.env` 已冻结,design/13 §2.1):上面三个
// env 键是既有的,本切片只是让面板能覆盖它们。
// - **坏值不得让整理停摆或忙轮询**:键写了非正数 ⇒ 回落下一档 + WARN
// (≤0 的周期不是「没配」,而是一个会让整理变成空转的值);整个文件解不开
// ⇒ 四个键连同其它键一起回落 `.env` 并记 WARN(既有口径,见 LoadMemoryTidyTuning)。
//
// 边界与 backthink.go 同源(那是同一族的手续):
//
// 1. 本文件只管**文件形状 + 读取**:它不认识 `TidyConfig`,也不认识「零值回落
@@ -108,6 +125,35 @@ type MemoryTidyTuning struct {
// `3600` 比要求主人记住 Go 的时长语法更不容易写错,也与既有
// `$DATA_DIR/config/backthink.json` 的 `*_SEC` 口径一致。
T1WindowSec int `json:"t1WindowSec"`
// IntervalSec 是**非安静时段**的整理周期(秒)。nil = 没配 ⇒ 用 `.env` 的
// `MEMORY_TIDY_INTERVAL_SEC`;两档都没有 ⇒ 代码默认 86400(每天 1 次)。
//
// 用**指针**的理由与 AutoApply 同源,但多一层:数值键的零值(0)在这里是
// **坏值**而不是「一个合法的 0 秒周期」——`0` 会让整理退化成空转,因此
// 「键没写」(静默回落)与「写了 0」(WARN + 回落)必须可区分。
IntervalSec *int `json:"intervalSec"`
// QuietIntervalSec 是**安静时段**的整理周期(秒)——主人要的「动态分配:
// 白天间隔短一点,晚上到了静默时期稍微长一点」就落在这个键上。
//
// nil = 没配 ⇒ **等于 intervalSec**(缺省 = 不因时段换档,而不是「夜里不跑」);
// 写非正数 ⇒ 同一档回落 + WARN(理由同 IntervalSec)。
//
// 它**没有 env 键**(`.env` 已冻结,design/13 §2.1):安静时段分档是本次新增
// 的能力,`.env` 那一档只有 `MEMORY_TIDY_INTERVAL_SEC`,因此它的「下一档」就是
// 上面那个已解析出来的 intervalSec。
QuietIntervalSec *int `json:"quietIntervalSec"`
// QuietStartHour / QuietEndHour 是安静时段的两端(**本地钟点**),语义与既有
// `inQuietHours` 逐字一致:窗口可跨午夜;两端相等且非 0 = 关闭窗口。
//
// 用**指针**是必须的(不像 autoApplyPerRound 那样用零值当「没配」):**0 是
// 合法钟点**(如 `0-7` 的凌晨窗口、`23-0` 到午夜的窗口),用 int 会让「显式
// 写 0」与「没写」不可区分,于是任何只写一端的文件都会被静默改掉另一端。
//
// nil = 没配 ⇒ 用 `.env` 的 `MEMORY_TIDY_QUIET_START_HOUR` / `..._END_HOUR`;
// 两端都没配 ⇒ 代码默认 23:00-07:00。
QuietStartHour *int `json:"quietStartHour"`
QuietEndHour *int `json:"quietEndHour"`
}
// AutoApplyOff 报告自动档是否被这份配置**显式关掉**。nil(没配)返回 false ——
@@ -138,8 +184,12 @@ func DefaultMemoryTidyPath(dataDir string) string {
// 文件缺失或全空白 ⇒ 零值 + nil:调用方用代码默认值(normalizeTidyConfig 会把
// 非正的上限回落默认),因此「没配过」与「配成 0」得到同一个结果。
//
// 存在但解不开 ⇒ 返回错误。调用方应记一条带 source 的 WARN 并回落默认;
// **不得覆盖**那个文件(损坏的文件可以人工救回,程序盖掉它就丢了人工写的意图)。
// 存在但解不开 ⇒ 返回错误。调用方应记一条带 source 的 WARN 并回落**下一档**
// (调度四键落 `.env` 值、开关类落代码默认)——注意这是**整份文件**的粒度:一个键
// 类型写错(如 `"intervalSec": "3600"`)会让全部键一起回落,这是既有口径
// (`TestApplyMemoryTidyTuning_ReadsDataDirConfig` 的用例③钉的就是它),而不是
// 逐键容错。**不得覆盖**那个文件(损坏的文件可以人工救回,程序盖掉它就丢了人工
// 写的意图)。
//
// 它不建目录、不读老位置:新家族没有老位置(design/13 §2.1 第 5 条)。
func LoadMemoryTidyTuning(path string) (MemoryTidyTuning, error) {