Files
Cyrene_v2/internal/config/runtimeconfig/backthink.go
T
AskaEth ed5005307a feat(backthink): 自排 C1-2 本体 + TriggerUnknown 修 fail-open(V1)
- selfwake.go:WakeNow + 待跑槽容量 1(先探槽合并不过闸、槽空才过闸、同锁)+ 闸 4 轮/小时
  ($DATA_DIR/config/backthink.json,零新 env 键)+ 审计四条 what + 启动日志两键 + 组合根接线
- G-A(主人已批):TriggerUnknown 作零值、既有两值编号后移(语义不动),未知触发走**最紧分支**
- 5 个变异自检(拆闸/≤0 未回落/过闸挪到探槽前=G1 失效形态/槽无界/零值挪走)
⚠️ 服务侧已就绪但**她仍调不到**(自排工具面属 C1-3)⇒ 不得写「C 已完成」
2026-10-06 20:25:04 +08:00

84 lines
4.2 KiB
Go
Raw 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.
// backthink.go 是后台思考(backthink)的**可变源**:`$DATA_DIR/config/backthink.json`
// (design/12 §N C.5 + design/13 §2.1「运行期生成的配置进 `$DATA_DIR/config/`,
// 新增配置家族从一开始就只写这里,不落老位置」)。
//
// 边界划清(与 websearch.go 同源):
//
// 1. 本文件只管**文件形状 + 读取**:它不认识 `backthink.Config`,也不认识
// 「≤0 回落默认」这条裁决 —— 那是服务侧 normalize 的事。这里只如实把盘上的
// 数字交出去(零值 = 没配,由调用方用代码默认值兜)。
// 2. **只读**:写路径(面板保存 + admin 端点)不在 C1 内,属 C2。因此本文件
// 刻意没有 `Save*`:多一个没人调用的写口,就多一条没人验证的落盘语义。
// 3. **零新 env 键**(主人 2026-10-06 `.env` 冻结红线):自排的成本旋钮只从这里来,
// 不进 `.env` / `.env.example`,也不进面板的事实卡(那张表的键名被模板钉住)。
//
// 读取口径:文件缺失或全空白 = 还没配过(零值 + nil,**不是错误**);
// 存在但解不开 = 返回错误,调用方记 WARN 并回落**代码默认值** —— 绝不静默变成
// 「无上限」(design/12 §N C.5 的 G3 同一条纪律:坏配置不许把成本保护关掉)。
package runtimeconfig
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
// backthinkFileName 是文件名(家族命名 = 服务/模块名 + .json)。
const backthinkFileName = "backthink.json"
// backthinkConfigSubdir 是落点的第一段目录(`$DATA_DIR/config/backthink.json`)。
// 与 web-search 插件各自声明一份而不是共用一个常量:两处的目录段虽然今天相同,
// 但一旦某个家族换位置,共用常量会让另一处**静默**跟着搬家。
const backthinkConfigSubdir = "config"
// ErrInvalidBackthink 是 backthink.json 结构不合法时的分类错误(errors.Is 可判类)。
var ErrInvalidBackthink = errors.New("runtimeconfig: invalid backthink config")
// BackgroundTuning 是 `backthink.json` 的形状:她自己排起一轮思考的成本旋钮。
//
// 字段名与 design/12 §N C.5 的规格逐字一致(camelCase 的 JSON 键,文件是给人看、
// 给人改的)。**没有版本字段**:这个家族只有这两个数字,等它真的长形状时再加版本,
// 现在加一个永远为 0 的字段只是噪音。
type BackgroundTuning struct {
// SelfWakePerWindow 是每窗口允许的自排轮数。零/负 = 未声明或笔误 ⇒
// 由服务侧 normalize 回落代码默认(**没有「关闭上限」语义**)。
SelfWakePerWindow int `json:"selfWakePerWindow"`
// SelfWakeWindowSec 是执行上述上限的滑动窗口秒数。零/负 ⇒ 同上回落默认。
SelfWakeWindowSec int `json:"selfWakeWindowSec"`
}
// DefaultBackthinkPath 返回后台思考调优的落点:`$DATA_DIR/config/backthink.json`。
func DefaultBackthinkPath(dataDir string) string {
return filepath.Join(dataDir, backthinkConfigSubdir, backthinkFileName)
}
// LoadBackgroundTuning 读取 backthink.json(**只读**)。
//
// 文件缺失或全空白 ⇒ 零值 + nil:调用方用代码默认值(`normalize` 会把 ≤0 回落默认),
// 因此「没配过」与「配成 0」得到同一个结果,正是这条裁决要的。
//
// 存在但解不开 ⇒ 返回错误。调用方应记一条带 source 的 WARN 并回落默认;
// **不得覆盖**那个文件(损坏的文件可以人工救回,程序盖掉它就丢了人工写的意图)。
//
// 它不建目录、不读老位置:新家族没有老位置(design/13 §2.1 第 5 条)。
func LoadBackgroundTuning(path string) (BackgroundTuning, error) {
raw, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return BackgroundTuning{}, nil
}
return BackgroundTuning{}, fmt.Errorf("runtimeconfig: read %s: %w", path, err)
}
if strings.TrimSpace(string(raw)) == "" {
return BackgroundTuning{}, nil
}
var t BackgroundTuning
if err := json.Unmarshal(raw, &t); err != nil {
return BackgroundTuning{}, fmt.Errorf("%w: decode %s: %w", ErrInvalidBackthink, path, err)
}
return t, nil
}