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:
1 parent
215eedf8e9
commit
4213b268fe
13 files changed
+1289
-43
No files matched your search
@@ -45,6 +45,14 @@ ALTER TABLE backthink_records ADD COLUMN urgency TEXT NOT NULL DEFAULT ''`
|
||||
const createRecordsIndexSQL = `
|
||||
CREATE INDEX IF NOT EXISTS idx_backthink_records_at ON backthink_records(at)`
|
||||
|
||||
// createRecordsTriggerIndexSQL 支撑按触发源筛选的那条查询
|
||||
// (`WHERE trigger = ? ORDER BY id DESC`)。索引条目是 (trigger, rowid),而主键
|
||||
// `id` 就是 rowid ⇒ SQLite 能直接用一次索引范围扫描(反向)同时满足筛选与排序。
|
||||
// 它是**本片新增读路径**的配套:没有它,筛选就是全表扫描 + 排序(表按心跳每 15 分钟
|
||||
// 增长一行,一年就是三万余行)。
|
||||
const createRecordsTriggerIndexSQL = `
|
||||
CREATE INDEX IF NOT EXISTS idx_backthink_records_trigger ON backthink_records(trigger)`
|
||||
|
||||
const insertRecordSQL = `
|
||||
INSERT INTO backthink_records
|
||||
(trigger, at, conclusion, should_push, push_pushed, push_route, push_detail, push_error, urgency)
|
||||
@@ -56,6 +64,19 @@ FROM backthink_records
|
||||
ORDER BY id DESC
|
||||
LIMIT ?`
|
||||
|
||||
// selectRecordsByTriggerSQL 与 selectRecordsSQL 逐字同形,只多一个按
|
||||
// `Record.Trigger` 的**服务端**筛选(参数化,不拼字符串)。
|
||||
//
|
||||
// 为什么下推到 SQL 而不是「取一页再在内存里筛」:那样 `limit` 就变成「这一页里
|
||||
// 恰好匹配的条数」,面板会看到「自我唤醒 0 条」而库里其实有 —— 一个静默的假空值
|
||||
// (design/12 §N C.6 的服务端筛选要的正是「筛不到 ≠ 没发生」)。
|
||||
const selectRecordsByTriggerSQL = `
|
||||
SELECT id, trigger, at, conclusion, should_push, push_pushed, push_route, push_detail, push_error, urgency
|
||||
FROM backthink_records
|
||||
WHERE trigger = ?
|
||||
ORDER BY id DESC
|
||||
LIMIT ?`
|
||||
|
||||
// recordsTableName 是本模块自建表的物理名(DDL 与判列共用一处)。
|
||||
const recordsTableName = "backthink_records"
|
||||
|
||||
@@ -115,6 +136,9 @@ func NewStore(ctx context.Context, db *sql.DB, log *slog.Logger) (*Store, error)
|
||||
if _, err := db.ExecContext(ctx, createRecordsIndexSQL); err != nil {
|
||||
return nil, fmt.Errorf("backthink: ensure backthink_records index: %w", err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, createRecordsTriggerIndexSQL); err != nil {
|
||||
return nil, fmt.Errorf("backthink: ensure backthink_records trigger index: %w", err)
|
||||
}
|
||||
return &Store{db: db, log: log}, nil
|
||||
}
|
||||
|
||||
@@ -207,9 +231,7 @@ func (s *Store) Append(ctx context.Context, r Record) (int64, error) {
|
||||
// defaultListLimit。urgency 列的空串如实读成「未知」(见
|
||||
// Record.Urgency),不做任何补齐或推断。
|
||||
func (s *Store) List(ctx context.Context, limit int) ([]Record, error) {
|
||||
if limit <= 0 {
|
||||
limit = defaultListLimit
|
||||
}
|
||||
limit = normListLimit(limit)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -218,8 +240,40 @@ func (s *Store) List(ctx context.Context, limit int) ([]Record, error) {
|
||||
return nil, fmt.Errorf("backthink: list records: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanRecords(rows, limit)
|
||||
}
|
||||
|
||||
records := make([]Record, 0, limit)
|
||||
// ListByTrigger 返回**某一触发源**的最近记录,最新的在前:语义与 List 逐字相同,
|
||||
// 只多一个参数化的 `trigger = ?`(design/12 §N C.6 的服务端筛选)。
|
||||
//
|
||||
// ⚠️ 它按**原始落库值**精确匹配(不模糊、不前缀):`Record.Trigger` 对事件轮是事件
|
||||
// 主题字符串,对内置轮是 `heartbeat` / `startup` / `self_wake` —— 合法取值由
|
||||
// `IsTriggerValue`(trigger_domain.go 的唯一真源)判定,本层不做二次裁剪。
|
||||
func (s *Store) ListByTrigger(ctx context.Context, trigger string, limit int) ([]Record, error) {
|
||||
limit = normListLimit(limit)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx, selectRecordsByTriggerSQL, trigger, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("backthink: list records by trigger: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanRecords(rows, limit)
|
||||
}
|
||||
|
||||
// normListLimit 把非正的 limit 归一为 defaultListLimit(两条读路径同一口径)。
|
||||
func normListLimit(limit int) int {
|
||||
if limit <= 0 {
|
||||
return defaultListLimit
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
// scanRecords 是两条读路径共用的行解码(抽出来是为了让「列序 / 空串语义」只有
|
||||
// 一处真源:再加第三条查询时不会悄悄错位)。
|
||||
func scanRecords(rows *sql.Rows, capacity int) ([]Record, error) {
|
||||
records := make([]Record, 0, capacity)
|
||||
for rows.Next() {
|
||||
var (
|
||||
r Record
|
||||
|
||||
Reference in new issue
Block a user