Files
Cyrene_v2/internal/services/gateway/admin_backthink_urgency.go
T
AskaEth a0abf0199a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway)!: 后台思考档位精确化(+ 面板三态 + 契约 v0.69/v0.70)
- gateway:优先读落库的 rec.Urgency(精确档位,与安静时段窗口无关);仅空串才回落到既有下限推导,并打 urgencyLowerBound 标记(**只在推导值上为 true**)
- 面板三态:精确(例常/紧急(高)/紧急·安全域)|下限(一律加 ≥ 前缀)|未知;下限悬停说明写清「为什么旧行只能是下限」
- 实时帧零协议改动:Record 的 JSON 键就是 urgency ⇒ 帧自然带上;wsParse 早已防御性读取(只加一处 urgencyLowerBound 防御读)
- 契约:v0.69(directMsgPerMin 透传修复,逐字照抄附录 A)与 v0.70(精确化 + 更正 §4.10(b)「实时帧不带这两键」)一起登记
- 变异实测:旧行不标下限 ⇒ 3 条测试红(结构体对比);前端去掉 ≥ ⇒ 渲染测试红
- ⚠️ 诚实边界(§7.3 已登记):实时旧记录刷新前显示「档位未知」、刷新后显示「≥ 例常」——同一条记录两路下的已知差异
2026-10-05 14:15:55 +08:00

82 lines
4.3 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"
)
// 本文件把「这一轮后台思考有多紧急、是不是穿透了安静时段」变成**存档里可证**的
// 事实。档位有两个来源,**优先级明确**(X2b):
//
// 1. **精确档位**:`Record.Urgency` 由 backthink 在档位判定点原样落库,是那一轮
// **实际生效**的档位。非空 ⇒ 直接下发,**不标记下限**,也不受安静时段窗口约束
// (它与 `At` 无关,是独立落库的事实,因此时间戳坏掉的行同样如实下发)。
// 2. **下限推导**(只对 `Record.Urgency` 为空的行:该列出现之前的旧行,以及从未走到
// 档位判定点的行 —— 系统事件降级、思考失败):用安静时段窗口规则推导,
// 结果**必须**带 `urgencyLowerBound` 标记,绝不能被读成精确档位。推导依据:
// - `Record.ShouldPush` 是那一轮的打扰判定结果;
// - `Record.At` 是那一轮的墙钟时间(库里存 UTC,取钟点前必须换回本地时区,
// 因为安静时段是**本地钟点**窗口);
// - 安静时段窗口取**进程生效**的 `QUIET_START_HOUR`/`QUIET_END_HOUR`
// (`config.Background`,与 backthink 的 cfg 同源:组合根把同一对值交给它)。
//
// 由 `backthink.Decide` 的判定级联(`decide.go` 的 baseDecide)可证:
// - 安静时段内被**放行** ⇒ 该轮重要度必然 ≥ `ImportanceHigh`(更低档位在安静
// 时段一律被扣下)⇒ 档位**至少** warning(high)。这就是 `quietHoursBypassed`
// 的全部依据,不是启发式。
// - 安静时段内被**扣下** ⇒ 档位不足以放行(也可能是她自己的结论要求不推,
// 见 `Decide` 的首个分支)⇒ 只能给出「不低于最低档」这个下限。
// - 安静时段**之外** ⇒ 判定根本不看档位(「想不想说」由她自己的结论决定),
// 并且推送成功的行其 `PushResult.Detail` 已被投递说明覆盖 ⇒ 存档里没有任何
// 字段能反推档位。此时**不下发** urgency(面板显示「未知」),绝不猜。
//
// fail-safe:`At` 为零值(时间戳格式错乱的行,见 backthink.store.parseTime 的兜底)
// 时不判下限、也不判穿透 —— 宁可不显示,也不把零值时间当成凌晨而谎报一轮穿透。
// backthinkQuietOutcome 是「该轮紧急度与安静时段的关系」的推导结果。
type backthinkQuietOutcome struct {
// urgency 是档位标签(urgency.Level 的词汇),无事实时为空串。
urgency string
// urgencyLowerBound 报告 urgency 是**推导出的下限**而不是该轮的精确档位。
// 只在旧行(没有落库档位)上为 true;精确档位与「无档位」都是 false。
urgencyLowerBound bool
// bypassed 报告该轮是否穿透了安静时段(安静时段内仍被放行)。
bypassed bool
}
// backthinkQuietOutcomeOf 推导一条存档记录可证的紧急度与穿透标记。
func backthinkQuietOutcomeOf(rec backthink.Record, startHour, endHour int) backthinkQuietOutcome {
// 精确档位优先:X2a 起该列随每轮落库,读到即事实(与窗口、与 At 都无关)。
out := backthinkQuietOutcome{urgency: rec.Urgency}
if rec.At.IsZero() || !quietHoursActiveAt(startHour, endHour, rec.At.Local()) {
return out
}
if rec.ShouldPush {
out.bypassed = true
}
if out.urgency != "" {
return out
}
// 旧行:窗口规则给出的**下限**,必须带上标记。
out.urgencyLowerBound = true
out.urgency = urgency.LevelRoutine.String()
if rec.ShouldPush {
out.urgency = urgency.LevelWarning.String()
}
return out
}
// 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
}