Files
AskaEth 4213b268fe feat(backthink): 自排的成本可见面(C2 读侧,Y3)
- GET /api/v1/admin/backthink 加可选 ?trigger=(闭集=内建触发标签 + 本服务订阅的事件主题;
  未知/空 ⇒ 400 invalid_trigger,不静默忽略;合法值下推到 SQL ⇒ limit 是匹配条数)
- 新端点 GET /api/v1/admin/backthink/self-wake(writeAuth):闸的只读镜像
  {available,running,reason?,configFile?,gate{pending,used,limit,windowSec,default*,*IsDefault}}
  三态分开(未接线 / 有配置未运行 / 运行中),读它**不消耗额度**
- 零新 env 键;旋钮源 = $DATA_DIR/config/backthink.json(**写面未做**,前置=组合根接线,已登记)
- 键名沿用设计与现有代码的 selfWakePerWindow(派工书写错成 selfWakeLimit,未改键名以免废掉主人已写的配置)
- 5 个变异(未知值静默忽略/关掉下推/恒报运行中/白名单摘键/前端把看不到画成 0/0)
⚠️ 本片改了 4 个文件面外文件(admin_query.go/routes.go/api.ts/adminSystem.ts)——已逐条说明理由
2026-10-06 21:48:49 +08:00

98 lines
5.3 KiB
Go
Raw Permalink 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"
// BackthinkConfigRelPath 是落点相对 `$DATA_DIR` 的路径(`config/backthink.json`)。
//
// 单独导出它,是为了让面板能**如实**告诉主人「改哪个文件」,而不必把部署相关的
// 绝对路径塞进接口形状(绝对路径随部署而变,由占用它的部署自己拼 `$DATA_DIR`)。
// 它与 `DefaultBackthinkPath` 取自同一对常量(目录段 + 文件名),因此不会漂移。
const BackthinkConfigRelPath = backthinkConfigSubdir + "/" + backthinkFileName
// ErrInvalidBackthink 是 backthink.json 结构不合法时的分类错误(errors.Is 可判类)。
var ErrInvalidBackthink = errors.New("runtimeconfig: invalid backthink config")
// BackgroundTuning 是 `backthink.json` 的形状:她自己排起一轮思考的成本旋钮。
//
// 字段名与 design/12 §N C.5 的规格逐字一致(camelCase 的 JSON 键,文件是给人看、
// 给人改的)。**没有版本字段**:这个家族只有这两个数字,等它真的长形状时再加版本,
// 现在加一个永远为 0 的字段只是噪音。
//
// ⚠️ **写路径未实现(切片 C2 如实登记)**:本文件至今只有读口。C.5 把「面板保存 +
// `SaveBackgroundTuning` + admin 端点」列为 C2,而面板写路径要经 design/09 §5 的
// 面板声明机制(`Plugins.Declare` + 动作分发),那需要组合根接线(`cmd/cyrene`)
// 与一个 `backthink` 面板声明 —— 都不在 C2 的文件面内。因此 C2 只交付**读侧**:
// 运行期生效的旋钮经 `GET /api/v1/admin/backthink/self-wake` 可见(含代码默认值),
// 改动仍要**人手改这个文件 + 重启**(与 `QUIET_*` 同口径:启动期读一次)。
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
}