Files
Cyrene_v2/internal/services/gateway/admin_backthink_urgency.go
T
AskaEth 34cef95e17 feat(gateway): 配置只读事实面 + 后台思考行「档位/穿透」(契约 v0.68)
- 新增 GET /api/v1/admin/config-facts:记忆整理 7 键 + 安静时段 2 键的**进程生效值** + 声明默认值(反射读 default 标签,不读 .env)
- 面板:记忆整理卡 + 安静时段卡(Miuix 只读),文案按新语义「只服务主动搭话,不静默回复」
- 后台思考行加 urgency(**只能是下限**,存档不可分 high/critical)+ quietHoursBypassed(与既有 bypassed 是两件事)
- 红线实测:往响应里塞 JWT ⇒ TestAdminConfigFacts_NeverLeaksSecretValues 打红(连断言消息都把整个响应打出来了)
- 前端:configFacts 24 checks / ConfigFactsCards 29 / BackthinkUrgencyTags 19 全过
- 契约 v0.68(纯增量:B12 新端点 + B8 两字段 + §4.10 形状与语义 + §7.4-17 下限限定)
2026-10-05 13:54:56 +08:00

71 lines
3.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 (
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink"
"git.yeij.top/AskaEth/Cyrene_v2/internal/urgency"
)
// 本文件把「这一轮后台思考有多紧急、是不是穿透了安静时段」变成**存档里可证**的
// 事实(切片 P4)。它刻意只读 `backthink.Record` 已有的字段 —— 本片不动
// `internal/services/backthink/**`(通用紧急度通道的落库属那片的面):
//
// - `Record.ShouldPush` 是那一轮的打扰判定结果;
// - `Record.At` 是那一轮的墙钟时间(库里存 UTC,取钟点前必须换回本地
// 时区,因为安静时段是**本地钟点**窗口);
// - 安静时段窗口取**进程生效**的 `QUIET_START_HOUR`/`QUIET_END_HOUR`
// (`config.Background`,与 backthink 的 cfg 同源:组合根把同一对值交给它)。
//
// 由 `backthink.Decide` 的判定级联(`decide.go` 的 baseDecide)可证:
//
// - 安静时段内被**放行** ⇒ 该轮重要度必然 ≥ `ImportanceHigh`(更低档位在安静
// 时段一律被扣下)⇒ 这一轮**穿透了安静时段**,档位**至少**是 `warning`
// (high)。这就是 `quietHoursBypassed` 的全部依据,不是启发式。
// - 安静时段内被**扣下** ⇒ 档位 < high ⇒ `routine`,未穿透。
// - 安静时段**之外** ⇒ 判定根本不看档位(「想不想说」由她自己的结论决定),
// 并且推送成功的行其 `PushResult.Detail` 已被投递说明覆盖 ⇒ 存档里没有任何
// 字段能反推档位。此时**不下发** urgency(面板显示「未知」),绝不猜。
//
// 因此 `AdminBackthinkItem.Urgency` 的语义是「**存档可证的档位下限**」而不是
// 「精确档位」:`warning` 读作「≥ high(含 critical)」。要做到精确档位,需要
// backthink 把每轮的 `Import` 落进 `Record`(本片不做,已在契约 §4.10(c) 与
// §7.4-17 如实登记)。
// backthinkQuietOutcome 是「该轮与安静时段的关系」的推导结果。
type backthinkQuietOutcome struct {
// urgency 是档位下限标签(urgency.Level 的词汇),无法证明时为空串。
urgency string
// bypassed 报告该轮是否穿透了安静时段(安静时段内仍被放行)。
bypassed bool
}
// backthinkQuietOutcomeOf 推导一条存档记录可证的紧急度下限与穿透标记。
//
// `at` 为零值(时间戳格式错乱的行,见 backthink.store.parseTime 的兜底)时一律
// 返回「无事实」:宁可不显示,也不把零值时间当成凌晨而谎报一轮穿透。
func backthinkQuietOutcomeOf(rec backthink.Record, startHour, endHour int) backthinkQuietOutcome {
if rec.At.IsZero() {
return backthinkQuietOutcome{}
}
if !quietHoursActiveAt(startHour, endHour, rec.At.Local()) {
return backthinkQuietOutcome{}
}
if rec.ShouldPush {
return backthinkQuietOutcome{urgency: urgency.LevelWarning.String(), bypassed: true}
}
return backthinkQuietOutcome{urgency: urgency.LevelRoutine.String()}
}
// quietHoursActiveAt 报告 at 是否落在安静时段窗口内。口径与既有三处逐字一致
// (`backthink.decide.inQuietHours`、`channelbridge` 轻思考门、
// `memory.TidyConfig.inQuietHours`):窗口是半开的 `[start, end)`,可跨午夜;
// 两端相等即关闭(`23/23` 与 `0/0` 都得到「从不生效」)。
func quietHoursActiveAt(startHour, endHour int, at time.Time) bool {
h := at.Hour()
if startHour <= endHour {
return h >= startHour && h < endHour
}
return h >= startHour || h < endHour
}