feat(memory): 启动时自动播种面板配置文件(缺失才写、绝不覆盖)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

主人 2026-10-07:「后端自动创建独立的面板配置文件要做」。

- 启动路径(wireMemoryTidy → applyMemoryTidyTuning 之后)若
  $DATA_DIR/config/memory-tidy.json 不存在或全空白 => 写一份「当时生效值」
  (十键:既有六键 + 频率四键),此后面板接管
- 已存在(非空)=> 跳过,该分支没有任何写盘语句;损坏 => WARN + 按 .env 跑、
  一个字不写盘(不「修好它」)
- 幂等(第二次启动必落到跳过分支);原子写(tmp 0600 + rename)
- 落盘前完整性闸:缺键 / 非正周期 / 钟点越界 => WARN + 不落盘
- 值来自 TidyTuningSnapshot(先跑两个归一化再翻回正向键)=> 播种那一刻运行
  行为零变化,有强断言(归一化后配置逐字相等)
- 零新增依赖、零 env 键变更;5 个变异(含「让已存在走写盘」=> 红)
This commit is contained in:
AskaEth committed 2026-10-07 20:38:37 +08:00
1 parent d680390300
commit 23b6e3fe1e
8 files changed
+1156 -6

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
package memory
import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
)
// 本文件是「启动播种」的**值来源**:把 `$DATA_DIR/config/memory-tidy.json` 十键的
// **当前生效值**拍成一份完整快照,交给 runtimeconfig.SeedMemoryTidy 落盘。
//
// 它回应主人 2026-10-07 的两条口径:
//
// - 「**`.env` 只做兜底用** —— 后端**自动创建**独立的面板配置文件之后就走面板
// 配置文件,这些键优先从面板配置上取」⇒ 播种的时机与「此后以面板为准」;
// - 「后端**自动创建**独立的面板配置文件**要做**」⇒ 这份快照就是「自动创建」的
// 内容,**零新 env 键**(`.env` 已冻结,design/13 §2.1)。
//
// 为什么放在 memory 而不是 runtimeconfig:runtimeconfig 只管**文件形状与读写**,
// 「缺位/坏值怎么回落、默认值是多少」属于 TidyConfig / ReviewConfig 的归一化语义
// (与 normalizeTidyConfig / normalizeReviewConfig 同一层)。
//
// 为什么快照取**归一化之后**的值:主人要的是「播种那一刻行为零变化」——把当时真正
// 生效的值写成文件,读回来叠上去之后(面板这一档等于生效值本身)结果逐字相同。
// 若快照取的是「未归一化的中间值」(比如四键还是 0),写下去的就是一份**比现状更空**
// 的配置,只是侥幸等价;取归一化后的值,等价性才是结构性的(见
// TestTidyTuningSnapshot_PanelRoundTripKeepsEffectiveConfig)。
//
// 为什么只有一处实现:十键的默认值在 memory 侧各只有一处定义
// (tidy_config.go / preinsert_config.go 的 default* 常量),快照直接复用那两个
// normalize,**不另写一份默认值** —— 否则播种写下去的就是第二份「差不多」的默认值,
// 会与运行期悄悄分叉。
//
// 十键与它们的生效来源:
//
// 键 生效来源(无面板文件时) 快照值
// autoApply DisableAutoApply 的反向(代码默认开) true
// autoApplyPerRound defaultTidyAutoApplyPerRound 50
// ingestGate DisableIngestGate 的反向(默认开) true
// t1Judge DisableT1 的反向(默认开) true
// t1PerWindow defaultT1PerWindow 120
// t1WindowSec defaultT1Window 3600
// intervalSec TidyConfig.IntervalSec(env 或 86400) 归一化值
// quietIntervalSec 缺省 = intervalSec 归一化值
// quietStartHour TidyConfig.QuietStartHour(env 或 23) 归一化值
// quietEndHour TidyConfig.QuietEndHour(env 或 7) 归一化值
// TidyTuningSnapshot 拍下十键的**当前生效值**,返回一份可直接落盘的面板快照。
//
// 入参是**已经叠过面板层的**两个配置(组合根的次序:TidyConfigFromConfig →
// ApplyTidyScheduleTuning → 本函数;审查侧同理),本函数只做最后一步归一化并翻成
// 文件形状 —— 它不读盘、不读 env、不认识面板文件:
//
// - tidy 侧四键(intervalSec / quietIntervalSec / quietStartHour / quietEndHour)
// 与自动档两键(autoApply / autoApplyPerRound);
// - review 侧四键(ingestGate / t1Judge / t1PerWindow / t1WindowSec):
// 这四项在 `.env` 里**没有出处**(`.env` 已冻结),只由面板文件与代码默认决定,
// 而播种只发生在**面板文件不存在**时 —— 那正是「零值 + normalizeReviewConfig」
// 这一条路径,与运行期(wireMemoryReview 的三个 helper)逐字一致。
//
// 两个开关是**反向**字段(DisableX),快照要把它们翻回正向的 JSON 键:文件里的
// `"autoApply": true` 才是「没配时到底开不开」的如实陈述。
func TidyTuningSnapshot(tidy TidyConfig, review ReviewConfig) runtimeconfig.MemoryTidyTuning {
tidy = normalizeTidyConfig(tidy)
review = normalizeReviewConfig(review)
autoApply := !tidy.DisableAutoApply
ingestGate := !review.DisableIngestGate
t1Judge := !review.DisableT1
intervalSec := tidy.IntervalSec
quietIntervalSec := tidy.QuietIntervalSec
quietStartHour := tidy.QuietStartHour
quietEndHour := tidy.QuietEndHour
return runtimeconfig.MemoryTidyTuning{
AutoApply: &autoApply,
AutoApplyPerRound: tidy.AutoApplyPerRound,
IngestGate: &ingestGate,
T1Judge: &t1Judge,
T1PerWindow: review.T1PerWindow,
T1WindowSec: int(review.T1Window.Seconds()),
IntervalSec: &intervalSec,
QuietIntervalSec: &quietIntervalSec,
QuietStartHour: &quietStartHour,
QuietEndHour: &quietEndHour,
}
}