主人 2026-10-07 两条口径:「.env 只做兜底用,后端自动创建独立的面板配置文件
之后就走面板配置文件,这些键优先从面板配置上取」+「频率必须调,可以调到 1 小时
或者三小时,或者动态分配(白天短、静默期长)」。
此前 $DATA_DIR/config/memory-tidy.json 只有读路径(装配期读一次 + 启动播种):
主人要改频率只能改文件 + 重启后端。本切片补上「读回来 → 改 → 落盘 → 当场生效」。
后端(读写端点,契约待塔登记 v0.92):
- GET /api/v1/admin/memories/tidy/tuning 读盘上 7 键值 + 取值规则 specs +
缺位时 .env 档的进程生效值(与 /admin/config-facts 同源)
- PUT 同路径:三态补丁(键缺位 = 不改动 / 合法值 = 采用 / 非法值 = 400 且盘上一
个字节都不动)+ null 清除哨兵(删键 ⇒ 回到下一档)+ writeAuth + 审计
memory.tidy.tuning;值一个都没变时不落盘,文件解不开时拒绝覆盖(409)
- 落盘走 runtimeconfig.PatchMemoryTidyTuning(原子写 temp+rename,与
SeedMemoryTidy/SaveWebSearch 同族同手法);补丁语义保留下受管的其它键
(t1Judge/t1PerWindow/autoApplyPerRound 原样保留)
- 热生效:保存后按「面板 > .env > 代码默认」重算并叠到运行期对象;整理档会
重排心跳(否则新周期要等旧周期那一觉睡完),审查档当场改 ingestGate/tempTtlSec。
未接线/失败时如实回 hot:false + hotError,绝不把「落盘了」说成「生效了」
前端(Miuix):
- 记忆管理页新增「记忆整理调优」卡:7 键表单(中文标签、单位、留空⇒跟随谁、
env 档当前值、坏值就地报错并拦住保存、保存后回显从磁盘读回的真值)
- 试算卡的 recallMode 按契约 v0.91 如实渲染:embedding / lexical(已降级)/
mixed(部分分区降级)/ ""(无候选可比)——空串不再渲染成空标签;并把
error.code=tidy_recall_unavailable 单列一句(「尝试过但向量源不可用」,与
「没有候选可比」显式区分)
验证:go test ./internal/{services/gateway,runtime/memory,config/runtimeconfig}
全绿;三态/清除/坏文件拒写/同值不重写/热生效接线各有用例,并逐条做过变异自证;
tsc --noEmit + vite build 通过;两个 esbuild 前端测试 39 + 34 + 41 checks 通过。
101 lines
5.6 KiB
Go
101 lines
5.6 KiB
Go
package memory
|
||
|
||
import (
|
||
"log/slog"
|
||
"time"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||
)
|
||
|
||
// 本文件是**入库前审查调优面**的解析点:把 `$DATA_DIR/config/memory-tidy.json`
|
||
// 的面板旋钮叠到 ReviewConfig 上 —— 与 tidy_tuning.go 对 TidyConfig 做的事同源、
|
||
// 同一条口径(主人 2026-10-07:「**`.env` 只做兜底用** —— 后端自动创建独立的面板
|
||
// 配置文件之后就走面板配置文件,这些键优先从面板配置上取」)。
|
||
//
|
||
// 为什么解析放在 memory 而不是 runtimeconfig:runtimeconfig 只负责**文件形状与
|
||
// 读取**(它的头注释把这条边界写死了),而「缺位/坏值怎么回落」属于 ReviewConfig
|
||
// 的归一化语义 —— 与 normalizeReviewConfig 同一层,因此两者可以一起被单测覆盖。
|
||
//
|
||
// 为什么不是「读盘时逐键容错」:整份文件解不开(含某键类型写错)时**全部键一起**
|
||
// 回落,这是本文件家族既有的粒度(见 runtimeconfig.LoadMemoryTidyTuning 与它的
|
||
// 用例③);本文件处理的是**类型正确但值不可用**(非正数)那一档。
|
||
|
||
// ApplyReviewTuning 把面板调优旋钮叠到 ReviewConfig 上,返回叠加后的配置
|
||
// (**未归一化**:调用方随后交给 NewPreInsertReviewer / normalizeReviewConfig
|
||
// 填代码默认值,与组合根对 TidyConfig 的既有顺序一致)。
|
||
//
|
||
// 目前只有一键,逐键独立,三档优先级是:
|
||
//
|
||
// 键 面板(memory-tidy.json) 下一档(.env) 代码默认
|
||
// tempTtlSec tempTtlSec **无此键**(结构性为空) 604800(7 天)
|
||
//
|
||
// 🔴 **中档(`.env`)在结构上是空的**,这是**刻意**的、不是漏配:`.env` 已冻结
|
||
// (design/13 §2.1「不加、不改、不删」),而「多久算临时」是本次新增的能力,
|
||
// 它在 `.env` 里**从来就没有键**。因此实际生效链是「面板 > 代码默认」——报回报时
|
||
// 不许把它写成「面板 > .env > 代码默认」而让读者以为 `.env` 那档还有东西可调。
|
||
//
|
||
// 坏值(非正秒数)**绝不写进 cfg**:回落代码默认 + 记一条 WARN。一个 ≤0 的 TTL
|
||
// 会让每条 defer 记忆**落库即过期**(读侧立刻隐藏、后台清扫下一轮就删)——那是
|
||
// 坏配置而不是「不过期」,但也不许因此让启动失败(design/03 §9.6)。
|
||
func ApplyReviewTuning(cfg ReviewConfig, panel runtimeconfig.MemoryTidyTuning, log *slog.Logger) ReviewConfig {
|
||
log = logger.WithSource(baseTidyLog(log), "memory/review")
|
||
|
||
if sec := panelPositiveTTLSec(panel.TempTtlSec, "tempTtlSec", log); sec > 0 {
|
||
cfg.TempTTL = time.Duration(sec) * time.Second
|
||
}
|
||
return cfg
|
||
}
|
||
|
||
// panelPositiveTTLSec 解析**临时记忆 TTL** 旋钮,返回 0 表示「不覆盖」(调用方原样
|
||
// 留着上一档的值,最终由 normalizeReviewConfig 落到代码默认 7 天)。
|
||
//
|
||
// 三种情形分得很清,因为它们该有不同的观测形状(与 tidy_tuning.go 的
|
||
// panelPositiveSec 逐字同构):
|
||
//
|
||
// - `nil`(键没写)⇒ 0,**静默**:没配不是错,正是「代码默认兜底」那一档;
|
||
// - 非正数(写了 0 或负数)⇒ 0 + **WARN**:它会让每条临时记忆落库即过期
|
||
// (与「不过期」恰好相反),属于坏配置,必须留下痕迹;
|
||
// - 正数 ⇒ 原值。
|
||
func panelPositiveTTLSec(v *int, key string, log *slog.Logger) int {
|
||
if v == nil {
|
||
return 0
|
||
}
|
||
if *v <= 0 {
|
||
log.Warn("memory-tidy.json 的临时记忆 TTL 不可用,回落代码默认(非正 TTL 会让临时记忆落库即过期)",
|
||
"key", key, "value", *v, "fallback_sec", tempTtlSec)
|
||
return 0
|
||
}
|
||
return *v
|
||
}
|
||
|
||
// ApplyPanelTuning 把面板值**当场**叠到已经在跑的审查服务上(切片 MEMPANEL / M4)。
|
||
//
|
||
// 它只碰面板管得到、且**在 `.env` 里结构性没有出处**的两个键(`ingestGate` /
|
||
// `tempTtlSec`)—— 因此重算起点就是这两个字段的**零值**(中档为空),不需要一份
|
||
// 「`.env` 快照」,也不会因为重算而误伤别的键:`Enabled` / `QueueSize` / `ScanLimit` /
|
||
// `Timeout` / `Threshold` / T1 四件套一律**原样不动**(它们来自部署配置,不是面板旋钮;
|
||
// 面板端点也拒收它们)。
|
||
//
|
||
// 口径与装配期逐字一致(`wireMemoryReview` → `NewPreInsertReviewer`):面板写了就用,
|
||
// 缺位/坏值由 `ApplyReviewTuning`(WARN + 不覆盖)与 `normalizeReviewConfig`(代码
|
||
// 默认)接住 —— 这两处仍是**唯一**实现点,本方法不另写一套回落规则。
|
||
//
|
||
// 🔴 并发边界(如实登记):写 `r.cfg` 的两处在 `r.mu` 下,但 `preinsert.go` /
|
||
// `preinsert_gate.go` 读 `r.cfg` 的几处**没有**取 `r.mu`(本切片 scope 不含那两个
|
||
// 文件,故未一并收口)。这两个字段分别是一个 bool 与一个 int64,在 64 位平台上单字长
|
||
// 写入不会撕裂,但严格意义上这仍是「写方持锁、读方不持锁」的窄窗口。把 cfg 读改成
|
||
// 访问器(或换成原子快照)是后续应做的一次收口,已在回报里登记。
|
||
func (r *PreInsertReviewer) ApplyPanelTuning(panel runtimeconfig.MemoryTidyTuning) error {
|
||
next := normalizeReviewConfig(ApplyReviewTuning(ReviewConfig{}, panel, r.log))
|
||
r.mu.Lock()
|
||
r.cfg.DisableIngestGate = panel.IngestGateOff()
|
||
r.cfg.TempTTL = next.TempTTL
|
||
gateOn := !r.cfg.DisableIngestGate
|
||
ttl := r.cfg.TempTTL
|
||
r.mu.Unlock()
|
||
r.log.Info("memory review panel tuning applied",
|
||
"ingest_gate", gateOn, "temp_ttl", ttl.String(), "source", "admin panel")
|
||
return nil
|
||
}
|