Files
Cyrene_v2/internal/services/gateway/admin_backthink_selfwake.go
T
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

119 lines
5.5 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.
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)
}