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

71 lines
3.2 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.
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
}