package gateway import ( "net/http" "git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink" ) // 本文件是 `GET /api/v1/admin/backthink/self-wake` 的**只读用量可见面** // (design/12 §N C.5 的读侧,切片 C2)。 // // 为什么要有它:C.5 的「本窗口已用 N / 上限 M」此前**只**出现在被拒那一轮的审计文案里 // (`backthink.self_wake.capped` 的 result)—— 也就是「已经被拒过一次」才第一次看得见。 // 面板要能提前告诉主人「还能自排几次」,就得有一个只读快照出口。 // // 它是**纯读**:不写库、不落审计、不碰闸(读快照永远不改变闸的状态),与列表端点同门 // 同档(writeAuth = owner|admin;含她自排的成本口径与运行状态,不是给 viewer 的监控投影)。 // // ⚠️ 它**不是**写口:调这两个旋钮要改 `$DATA_DIR/config/backthink.json` + 重启 // (启动期读一次,与 `QUIET_*` 同口径)。面板写路径未做,原因见 runtimeconfig // `BackgroundTuning` 的注释(要经 design/09 §5 的面板声明机制,需组合根接线)。 // selfWakeStatusReader 是自排闸只读快照的**可选**读口:生产实现 // `*backthink.Backthink` 满足它。用可选接口的理由同 backthinkTriggerLister // (不改共享契约、不弄坏既有测试替身);不满足时如实回「本实现不提供读口」。 type selfWakeStatusReader interface { SelfWakeStatus() (backthink.SelfWakeStatus, bool) } // 编译期钉住生产实现满足本可选口(形状漂移在编译期立刻显形,而不是等到运行期 // 悄悄变成「本实现不提供自排状态读口」)。 var _ selfWakeStatusReader = (*backthink.Backthink)(nil) // AdminSelfWakeGate 是自排闸的线上投影:**进程生效**的上限 / 窗口 + 本窗口用量 + // 待跑槽占用,外带代码默认值(供面板区分「默认 / 已改」)。 type AdminSelfWakeGate struct { // Pending 报告容量 1 的待跑槽是否占用(有一轮自排排着队还没跑)。 Pending bool `json:"pending"` // Used 是本窗口已用额度(与工具收据 `SelfWakeReceipt.Used` 同一口径)。 Used int `json:"used"` // Limit / WindowSec 是**进程生效**的上限与窗口(已按「≤ 0 回落默认」normalize)。 Limit int `json:"limit"` WindowSec int `json:"windowSec"` // DefaultLimit / DefaultWindowSec 是代码默认值;LimitIsDefault / WindowIsDefault // 报告生效值**恰好等于**默认值。 // // ⚠️ 它**不是**「配置文件里没写这个键」的断言:网关**不读** `backthink.json` // (与 config-facts 的 isDefault 同一条诚实口径 —— 无从不区分「配了 4」与「没配」)。 DefaultLimit int `json:"defaultLimit"` DefaultWindowSec int `json:"defaultWindowSec"` LimitIsDefault bool `json:"limitIsDefault"` WindowIsDefault bool `json:"windowIsDefault"` } // AdminSelfWakeStatus 是 `GET /api/v1/admin/backthink/self-wake` 的响应。 // // 三态**必须可区分**(面板不得把「看不到」画成「0 次」): // - `available:false` ⇒ 本进程给不出快照(服务未接线 / 实现不提供读口 / 闸不在), // 此时 `gate` **缺席**、`reason` 说明原因; // - `available:true, running:false` ⇒ 已接线但不在运行(未启动或已停止):`gate` // 仍带配置面数字(上限/窗口/默认值),但此刻她排不出新的一轮 —— `reason` 说明; // - `available:true, running:true` ⇒ 正常态,`reason` 缺席。 type AdminSelfWakeStatus struct { Available bool `json:"available"` Running bool `json:"running"` Reason string `json:"reason,omitempty"` // ConfigFile 是「要改哪个文件」的相对落点(`config/backthink.json`,相对 `$DATA_DIR`): // 绝对路径随部署而变,接口只给相对落点,由面板拼上下文说明。 ConfigFile string `json:"configFile,omitempty"` Gate *AdminSelfWakeGate `json:"gate,omitempty"` } // adminSelfWakeStatus 下发自排闸的只读快照(见文件头注释)。 func (s *Server) adminSelfWakeStatus(w http.ResponseWriter, r *http.Request) { // 未接线:如实现状作答(与 adminListBackthink 返回空列表同口径:200 + 诚实说明, // 不假装有一个 0/0 的闸)。 if s.backthink == nil { writeJSON(w, http.StatusOK, AdminSelfWakeStatus{ Reason: "后台思考服务未接线:本进程不产生自排轮", }) return } reader, ok := s.backthink.(selfWakeStatusReader) if !ok { writeJSON(w, http.StatusOK, AdminSelfWakeStatus{ Reason: "当前后端的后台思考实现不提供自排状态读口", }) return } st, ok := reader.SelfWakeStatus() if !ok { writeJSON(w, http.StatusOK, AdminSelfWakeStatus{ Reason: "自排闸不在(后台思考服务未构造):没有可报的用量", }) return } out := AdminSelfWakeStatus{ Available: true, Running: st.Running, ConfigFile: runtimeconfig.BackthinkConfigRelPath, Gate: &AdminSelfWakeGate{ Pending: st.Pending, Used: st.Used, Limit: st.Limit, WindowSec: st.WindowSec, DefaultLimit: st.DefaultLimit, DefaultWindowSec: st.DefaultWindowSec, LimitIsDefault: st.LimitIsDefault, WindowIsDefault: st.WindowIsDefault, }, } if !st.Running { out.Reason = "后台思考未在运行(未启动或已停止):此刻她排不出新的一轮" } writeJSON(w, http.StatusOK, out) }