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 }