feat(gateway)!: 后台思考档位精确化(+ 面板三态 + 契约 v0.69/v0.70)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

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

No files matched your search

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