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)——已逐条说明理由
This commit is contained in:
AskaEth committed 2026-10-06 21:48:49 +08:00
1 parent 215eedf8e9
commit 4213b268fe
13 files changed
+1289 -43

No files matched your search

@@ -0,0 +1,70 @@
package backthink
import "time"
// 本文件是自排闸的**只读快照**出口(design/12 §N C.5/C.2 的读侧,切片 C2)。
//
// 为什么需要它:面板此前**看不到**「本窗口还能自排几次」—— 那个数字只出现在
// 被拒绝那一轮的审计文案里(`backthink.self_wake.capped` 的 result),也就是
// 「已经被拒一次」之后才第一次可见。C.5 的闸本来就维护了一份**只为上报**的镜像
// (selfWakeGate.granted),本文件把它接出来,判定仍然只由 limiter 做
// (§C.5:「判定始终由 limiter 做;两者用同一个时间源与同一个窗口」)。
//
// 🔴 本文件**不做**任何写入、不碰滑窗件(`session.RateLimiter` 至今没有用量读口,
// 属别人的文件面):镜像在 selfWakeGate 里已经有了,读它即可。
// SelfWakeStatus 是自排闸的只读快照:本窗口已用多少、上限多少、窗口多长,
// 以及容量 1 的待跑槽是否被占用。
//
// 它**不参与任何判定**(判定始终由滑窗与待跑槽做),因此读它永远不改变闸的状态。
type SelfWakeStatus struct {
// Running 报告自排是否真的能排:未 Start / 已 Stop ⇒ false(此刻 WakeNow
// 会返回 ErrSelfWakeUnavailable)。面板据此显示「未运行」而不是「0 次」。
Running bool
// Pending 报告容量 1 的待跑槽是否已占用(有一轮自排排着队还没跑)。
Pending bool
// Used 是本窗口已用额度,与收据 `SelfWakeReceipt.Used` **同一口径**
// (同一次取数:过期的记录先被丢弃,再计数)。
Used int
// Limit 与 WindowSec 是**进程生效**的上限与窗口(已 normalize;`≤0` 的那份
// 配置不会走到这里,见 cfg.go 的 normalize 注释)。
Limit int
WindowSec int
// DefaultLimit / DefaultWindowSec 是代码默认值;LimitIsDefault / WindowIsDefault
// 报告生效值**恰好等于**默认值。
//
// ⚠️ 它**不是**「配置文件里没写这个键」的断言(与 config-facts 的 isDefault
// 同一条诚实口径:网关不读配置文件,也无从区分「配了 4」与「没配」)。
DefaultLimit int
DefaultWindowSec int
LimitIsDefault bool
WindowIsDefault bool
}
// SelfWakeStatus 取一张自排闸快照。第二个返回值为 false 表示**闸不在**
// (只有零值 / 未经 `New` 构造的 Backthink 会这样)—— 调用方**不得**把零值
// 当成事实(不伪造「0 / 0」)。
func (b *Backthink) SelfWakeStatus() (SelfWakeStatus, bool) {
gate, running := b.wakeGate()
if gate == nil {
return SelfWakeStatus{}, false
}
gate.mu.Lock()
// usedLocked 顺手丢弃过期记录,因此这里读到的与判定用的是同一个口径。
used := gate.usedLocked()
pending := gate.pending != nil
limit, window := gate.limit, gate.window
gate.mu.Unlock()
return SelfWakeStatus{
Running: running,
Pending: pending,
Used: used,
Limit: limit,
WindowSec: int(window / time.Second),
DefaultLimit: defaultSelfWakeLimit,
DefaultWindowSec: int(defaultSelfWakeWindow / time.Second),
LimitIsDefault: limit == defaultSelfWakeLimit,
WindowIsDefault: window == defaultSelfWakeWindow,
}, true
}