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 已登记):实时旧记录刷新前显示「档位未知」、刷新后显示「≥ 例常」——同一条记录两路下的已知差异
This commit is contained in:
1 parent
f9c5b56a2f
commit
a0abf0199a
11 files changed
+390
-131
No files matched your search
@@ -145,7 +145,7 @@ export function BackthinkModule(): React.ReactNode {
|
||||
|
||||
<Notice>数据源:/ws/chat 下行 backthink / thinking 事件(实时,主驱动)+ 打开时一次拉取 GET /api/v1/admin/backthink 历史,两者按 id 去重、按时间倒序,新记录增量插入(不整页重渲)。「推送结果」的 route / detail / bypassed 两条路径同名同义:历史拉取取 AdminBackthinkItem 的同名键,实时帧取后端原始 PushResult 的 Route / Detail / Bypassed(解析器已读,见 lib/wsParse.ts#parseBackthinkItem,兼容 camelCase 变体),因此本次会话新到的行同样显示推送路由与推送说明,不再只显示「已推送/未推送」。链路导图取自后台思考会话(sess-backthink)的 graph_update 快照 —— 后端完整链路图始终发布,与管理端 Flow 页的「当前会话」图相互独立(裁决 16.4.2)。干预为后端 M10+ 提供,当前为只读展示。
|
||||
<br />
|
||||
新增的「穿透安静时段 / 紧急度」标记(契约 v0.68):穿透 = 这一轮落在安静时段窗口内、仍然被放行发出(只有 high 及以上档位做得到);紧急度是后端 urgency 档位,且只是「档位下限」(显示「≥ 高」= 至少 high,记录里分不出 high 与 critical)。显示「档位未知」是如实的:轮次不在安静时段窗口内时,持久化记录不保存档位 —— 它不是「例常」。⚠️ 实时帧走的是 bus 上原始的 backthink.Record,不带这两个字段,因此本次会话新到的行先显示「档位未知」,重开页面后由历史拉取补上真值。</Notice>
|
||||
新增的「穿透安静时段 / 紧急度」标记(契约 v0.70):穿透 = 这一轮落在安静时段窗口内、仍然被放行发出(只有 high 及以上档位做得到);紧急度**三态画得不一样** —— 把下限当精确值、把未知当「例常」都是把猜当事实:① **精确档位**(「例常」/「紧急(高)」/「紧急·安全域」)= 该轮实际生效的档位,随记录落库;② **≥ 下限**(带 `urgencyLowerBound` 的旧行:该行没有存档档位,此值由安静时段窗口规则推导 —— 「≥ 高」只表示至少 high、可能更高);③ **档位未知** = 存档里没有这个事实,**不是**「例常」。⚠️ 实时帧带上记录里的精确档位(`backthink.Record` 自 X2a 起带 `urgency`),但**不带** `urgencyLowerBound` / `quietHoursBypassed`(后两者由网关按安静时段窗口推导)⇒ 本次会话新到的行先显示精确档位与推送结果,穿透标记与旧行的下限标记要等重开页面由历史拉取补齐。</Notice>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// 后台思考行「穿透安静时段 / 紧急度」标记的**真实渲染**测试(契约 v0.68 §4.10(b))。
|
||||
// 后台思考行「穿透安静时段 / 紧急度」标记的**真实渲染**测试(契约 v0.70 §4.10(b))。
|
||||
//
|
||||
// 为什么需要它:后端明明标了「这一轮穿透了安静时段」,面板不画出来就等于没做;而
|
||||
// 「档位未知」与「例常」**必须**画得不一样 —— 把未知画成事实正是这次要避免的坑。
|
||||
// 为什么需要它:后端明明标了「这一轮穿透了安静时段」、或明明给了档位,面板不画出来就等于
|
||||
// 没做;而**精确档位** / **≥ 下限** / **未知** 三态**必须**画得不一样 —— 把下限画成精确
|
||||
// 值、把未知画成「例常」,正是这条链路要避免的两个坑。
|
||||
//
|
||||
// 跑法(同 channels/accountRow.render.test.ts):
|
||||
//
|
||||
@@ -17,7 +18,13 @@ import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import type { BackthinkItem } from "../../lib/types";
|
||||
import { BackthinkUrgencyTags } from "./BackthinkUrgencyTags";
|
||||
import { URGENCY_UNKNOWN_LABEL, QUIET_HOURS_BYPASS_LABEL, urgencyTag, quietHoursBypassTag } from "./backthinkUrgency";
|
||||
import {
|
||||
URGENCY_UNKNOWN_LABEL,
|
||||
URGENCY_LOWER_BOUND_PREFIX,
|
||||
QUIET_HOURS_BYPASS_LABEL,
|
||||
urgencyTag,
|
||||
quietHoursBypassTag,
|
||||
} from "./backthinkUrgency";
|
||||
|
||||
let checks = 0;
|
||||
|
||||
@@ -38,40 +45,64 @@ function htmlFor(over: Partial<BackthinkItem>): string {
|
||||
return renderToStaticMarkup(createElement(BackthinkUrgencyTags, { item: item(over) }));
|
||||
}
|
||||
|
||||
// ── ① 穿透行:安静时段内被放行 ⇒ 同时出「穿透安静时段」与档位下限「≥ 高」 ──────
|
||||
const bypassed = htmlFor({ urgency: "warning", quietHoursBypassed: true, shouldNotify: true, pushed: true });
|
||||
ok(has(bypassed, QUIET_HOURS_BYPASS_LABEL), "穿透的行必须画出「穿透安静时段」");
|
||||
ok(has(bypassed, "紧急(≥ 高)"), "档位下限必须显示为「≥ 高」,不能假装是精确档位");
|
||||
ok(!has(bypassed, URGENCY_UNKNOWN_LABEL), "有档位的行不得同时显示「档位未知」");
|
||||
/** 标签在 HTML 里是元素唯一内容 ⇒ 用 `>标签<` 判定「画出来的就是这个词」。 */
|
||||
function tag(text: string): string {
|
||||
return `>${text}<`;
|
||||
}
|
||||
|
||||
// ── ② 安静时段内被扣下 ⇒ 例常,且**不得**画成穿透 ──────────────────────────
|
||||
const held = htmlFor({ urgency: "routine", shouldNotify: false });
|
||||
ok(has(held, "例常"), "被扣下的轮次显示「例常」");
|
||||
ok(!has(held, QUIET_HOURS_BYPASS_LABEL), "被扣下的轮次绝不能显示穿透");
|
||||
// ── ① 精确档位(新行,`urgencyLowerBound` 缺席)⇒ 不带下限前缀 ────────────────
|
||||
const exactWarn = htmlFor({ urgency: "warning", quietHoursBypassed: true, shouldNotify: true, pushed: true });
|
||||
ok(has(exactWarn, tag("紧急(高)")), "精确 warning 必须显示为「紧急(高)」");
|
||||
ok(!has(exactWarn, tag(URGENCY_LOWER_BOUND_PREFIX + "紧急(高)")), "精确档位绝不能带「≥ 」下限前缀");
|
||||
ok(has(exactWarn, QUIET_HOURS_BYPASS_LABEL), "穿透的行必须画出「穿透安静时段」");
|
||||
ok(!has(exactWarn, URGENCY_UNKNOWN_LABEL), "有档位的行不得同时显示「档位未知」");
|
||||
const exactCritical = htmlFor({ urgency: "critical", quietHoursBypassed: true });
|
||||
ok(has(exactCritical, tag("紧急·安全域")), "精确 critical 显示安全域标签");
|
||||
ok(urgencyTag({ urgency: "critical" }).tone === "err", "critical 用 err 色调");
|
||||
ok(urgencyTag({ urgency: "warning" }).tone === "warn", "精确 warning 用 warn 色调");
|
||||
ok(!has(htmlFor({ urgency: "routine", shouldNotify: false }), QUIET_HOURS_BYPASS_LABEL), "没标记穿透就不画穿透");
|
||||
ok(!has(htmlFor({ urgency: "routine", shouldNotify: false }), tag(URGENCY_LOWER_BOUND_PREFIX + "例常")), "精确 routine 不带下限前缀");
|
||||
|
||||
// ── ③ 键缺席(窗口外的轮次 / 实时帧)⇒ 显示「档位未知」,不是「例常」 ────────
|
||||
// ── ② ≥ 下限(旧行,`urgencyLowerBound:true`)⇒ 必须与精确档位画得不一样 ──────
|
||||
const lowerWarn = htmlFor({ urgency: "warning", urgencyLowerBound: true, quietHoursBypassed: true });
|
||||
ok(has(lowerWarn, tag(URGENCY_LOWER_BOUND_PREFIX + "紧急(高)")), "下限 warning 必须显示为「≥ 紧急(高)」");
|
||||
ok(!has(lowerWarn, tag("紧急(高)")), "下限**不得**与精确档位同形(否则读者会把下限当精确值)");
|
||||
ok(has(lowerWarn, QUIET_HOURS_BYPASS_LABEL), "旧行的穿透标记照画");
|
||||
const lowerRoutine = htmlFor({ urgency: "routine", urgencyLowerBound: true, shouldNotify: false });
|
||||
ok(has(lowerRoutine, tag(URGENCY_LOWER_BOUND_PREFIX + "例常")), "下限 routine 显示「≥ 例常」");
|
||||
ok(!has(lowerRoutine, tag("例常")), "下限 routine 不得与精确「例常」同形");
|
||||
ok(
|
||||
urgencyTag({ urgency: "warning", urgencyLowerBound: true }).label !== urgencyTag({ urgency: "warning" }).label,
|
||||
"同一个档位词:下限与精确的标签必须不同",
|
||||
);
|
||||
|
||||
// ── ③ 未知(键缺席)⇒ 显示「档位未知」,绝不写成「例常」 ─────────────────────
|
||||
const unknown = htmlFor({ shouldNotify: true, pushed: true });
|
||||
ok(has(unknown, `>${URGENCY_UNKNOWN_LABEL}<`), "没有档位时标签正是「档位未知」");
|
||||
ok(!has(unknown, ">例常<"), "标签绝不能是「例常」——那就是把猜当事实");
|
||||
ok(has(unknown, tag(URGENCY_UNKNOWN_LABEL)), "没有档位时标签正是「档位未知」");
|
||||
ok(!has(unknown, tag("例常")), "标签绝不能是「例常」——那就是把猜当事实");
|
||||
ok(!has(unknown, QUIET_HOURS_BYPASS_LABEL), "没有穿透标记时不画穿透");
|
||||
ok(urgencyTag({}).label === URGENCY_UNKNOWN_LABEL, "缺档位时标签是未知");
|
||||
ok(!has(htmlFor({ urgency: "", urgencyLowerBound: true }), URGENCY_LOWER_BOUND_PREFIX), "空串档位是「未知」,不因带下限标记就编一个档位");
|
||||
|
||||
// ── ④ 后端加了本面板不认识的档位 ⇒ 原样显示,不猜更好听的词 ─────────────────
|
||||
const exotic = urgencyTag({ urgency: "emergency" });
|
||||
ok(exotic.label === "emergency", "未知档位原样显示");
|
||||
ok(!has(htmlFor({ urgency: "emergency" }), ">例常<"), "未知档位不得被解释成「例常」");
|
||||
ok(urgencyTag({ urgency: "emergency", urgencyLowerBound: true }).label === "≥ emergency", "未知档位同样带下限前缀");
|
||||
ok(!has(htmlFor({ urgency: "emergency" }), tag("例常")), "未知档位不得被解释成「例常」");
|
||||
|
||||
// ── ⑤ 安全域档位有独立标签与色调 ────────────────────────────────────────────
|
||||
ok(has(htmlFor({ urgency: "critical", quietHoursBypassed: true }), "紧急·安全域"), "critical 显示安全域标签");
|
||||
ok(urgencyTag({ urgency: "critical" }).tone === "err", "critical 用 err 色调");
|
||||
ok(urgencyTag({ urgency: "warning" }).tone === "warn", "warning(下限)用 warn 色调");
|
||||
// ── ⑤ 两种色调 / 穿透标记的显式语义 ────────────────────────────────────────
|
||||
ok(urgencyTag({ urgency: "routine" }).tone === "neutral", "routine 用中性色调");
|
||||
ok(urgencyTag({}).label === URGENCY_UNKNOWN_LABEL, "缺档位时标签是未知");
|
||||
ok(urgencyTag({ urgency: "warning", urgencyLowerBound: true }).tone === "warn", "下限 warning 仍用 warn 色调");
|
||||
ok(quietHoursBypassTag({ quietHoursBypassed: false }) === null, "显式 false 不画穿透(后端只发 true,双向都不画)");
|
||||
ok(quietHoursBypassTag({}) === null, "键缺席不画穿透");
|
||||
|
||||
// ── ⑥ 两个标记同源可追:提示文字解释「为什么是这个档位」 ─────────────────────
|
||||
ok(has(bypassed, "title="), "标记带悬停说明(title),让人能追到判据");
|
||||
ok(has(urgencyTag({ urgency: "warning" }).hint, "至少 high") || has(urgencyTag({ urgency: "warning" }).hint, "high"), "档位下限的说明必须点明它只是下限");
|
||||
// ── ⑥ 悬停说明必须说清「为什么是精确 / 为什么只能是下限 / 为什么未知」 ────────
|
||||
ok(has(exactWarn, "title="), "标记带悬停说明(title),让人能追到判据");
|
||||
ok(has(urgencyTag({ urgency: "warning", urgencyLowerBound: true }).hint, "至少 high"), "下限的说明必须点明它只是下限");
|
||||
ok(has(urgencyTag({ urgency: "warning", urgencyLowerBound: true }).hint, "不是精确档位"), "下限的说明必须点明它不是精确档位");
|
||||
ok(has(urgencyTag({ urgency: "warning", urgencyLowerBound: true }).hint, "没有存档档位"), "下限的说明必须写清为什么旧行只能是下限");
|
||||
ok(!has(urgencyTag({ urgency: "warning" }).hint, "下限"), "精确档位的说明不得自称下限");
|
||||
ok(has(urgencyTag({}).hint, "不是「例常」"), "未知的说明必须点明它不是「例常」");
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`BackthinkUrgencyTags.render: ${checks} checks passed`);
|
||||
@@ -7,8 +7,10 @@ import { quietHoursBypassTag, urgencyTag } from "./backthinkUrgency";
|
||||
* 为什么单独抽成组件:它是**画出来才算数**的一条(后端字段对了、面板没显示等于没
|
||||
* 做),因此需要一个真实渲染的测试盯着它 —— 见 BackthinkUrgencyTags.render.test.ts。
|
||||
*
|
||||
* ⚠️ 实时帧(bus 上原始 `backthink.Record`)目前不带档位 ⇒ 本次会话新到的行显示
|
||||
* 「档位未知」,重开页面后由历史拉取(`GET /admin/backthink`)补上真值。 */
|
||||
* ⚠️ 实时帧(bus 上原始 `backthink.Record`)自 X2a 起带 `urgency`(该轮实际生效的
|
||||
* **精确**档位),因此实时行也显示档位;`urgencyLowerBound` 与 `quietHoursBypassed`
|
||||
* 只由历史拉取(`GET /admin/backthink`)下发(两者都是网关按安静时段窗口推导的,
|
||||
* 不在记录里)⇒ 本次会话新到的旧行可能先显示「档位未知」,重开页面后由历史补齐。 */
|
||||
export function BackthinkUrgencyTags({ item }: { item: BackthinkItem }): React.ReactNode {
|
||||
const urgency = urgencyTag(item);
|
||||
const bypass = quietHoursBypassTag(item);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { BackthinkItem } from "../../lib/types";
|
||||
|
||||
/**
|
||||
* 后台思考行「紧急度 / 穿透安静时段」标记的**纯逻辑**(契约 v0.68 §4.10(b))。
|
||||
* 后台思考行「紧急度 / 穿透安静时段」标记的**纯逻辑**(契约 v0.70 §4.10(b))。
|
||||
*
|
||||
* 后端 `urgency` 的语义是**存档可证的档位下限**(`warning` = 至少 high 档,可能更
|
||||
* 高),不是精确档位;而窗口之外的轮次存档里没有档位,键**缺席**。因此本层:
|
||||
* - 缺席 ⇒ 显示「档位未知」(**不是**「例常」——把未知画成事实就是这次要避免的坑);
|
||||
* - 未知取值 ⇒ 原样显示后端给的词,不猜一个更好听的;
|
||||
* 三种态必须画得**不一样**,否则就是把猜当事实:
|
||||
* - **精确档位**:后端 `urgency` 就是该轮实际生效的档位(随记录落库,`urgencyLowerBound` 缺席);
|
||||
* - **≥ 下限**:`urgencyLowerBound:true` ⇒ 该行没有落库档位(旧行 / 未走到档位判定点的行),
|
||||
* 此值由安静时段窗口规则推导:`≥ 高` = 至少 high、可能更高;`≥ 例常` = 只确定不低于最低档;
|
||||
* - **未知**:`urgency` 键缺席 ⇒ 存档里没有这个事实(**不是**「例常」)。
|
||||
* - `quietHoursBypassed` 只在 true 时渲染「穿透安静时段」。
|
||||
*/
|
||||
|
||||
@@ -15,40 +16,62 @@ export type UrgencyTagTone = "ok" | "warn" | "err" | "neutral" | "accent";
|
||||
export interface UrgencyTag {
|
||||
label: string;
|
||||
tone: UrgencyTagTone;
|
||||
/** 悬停说明(为什么是这个档位 / 为什么未知),给人看的口径。 */
|
||||
/** 悬停说明(为什么是这个档位 / 为什么只是下限 / 为什么未知),给人看的口径。 */
|
||||
hint: string;
|
||||
}
|
||||
|
||||
/** 档位未知时的标签(键缺席 = 后端没有这个事实)。 */
|
||||
export const URGENCY_UNKNOWN_LABEL = "档位未知";
|
||||
|
||||
/** 下限标签的前缀:推导值一律带它,保证与精确档位**一眼可辨**。 */
|
||||
export const URGENCY_LOWER_BOUND_PREFIX = "≥ ";
|
||||
|
||||
/** 穿透安静时段的标签。 */
|
||||
export const QUIET_HOURS_BYPASS_LABEL = "穿透安静时段";
|
||||
|
||||
/** urgencyTag 把后端 `urgency` 映射成标签;键缺席时给「档位未知」。 */
|
||||
export function urgencyTag(item: Pick<BackthinkItem, "urgency">): UrgencyTag {
|
||||
/** 精确档位的标签:后端 `urgency` 词汇 → 人话。 */
|
||||
const EXACT_LABEL: Record<string, string> = {
|
||||
routine: "例常",
|
||||
warning: "紧急(高)",
|
||||
critical: "紧急·安全域",
|
||||
};
|
||||
|
||||
/** 精确档位的色调(安全域 err / 高 warn / 其余中性)。 */
|
||||
function exactTone(level: string): UrgencyTagTone {
|
||||
if (level === "critical") return "err";
|
||||
if (level === "warning") return "warn";
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
/** 下限的悬停说明:点明它是怎么推出来的、以及为什么它**不是**精确值。 */
|
||||
const LOWER_BOUND_HINT =
|
||||
"这一行没有存档档位(档位列出现之前的旧记录,或从未走到档位判定点的行)⇒ 精确档位已无从得知," +
|
||||
"此值是安静时段窗口规则推导出的下限:窗口内被放行 ⇒ 至少 high(可能是安全域);" +
|
||||
"被扣下 ⇒ 只确定不低于最低档。不是精确档位。";
|
||||
|
||||
/** urgencyTag 把后端 `urgency` + `urgencyLowerBound` 映射成标签。 */
|
||||
export function urgencyTag(item: Pick<BackthinkItem, "urgency" | "urgencyLowerBound">): UrgencyTag {
|
||||
const level = item.urgency;
|
||||
switch (level) {
|
||||
case "routine":
|
||||
return { label: "例常", tone: "neutral", hint: "安静时段内被扣下的轮次:档位低于 high,不穿透" };
|
||||
case "warning":
|
||||
return {
|
||||
label: "紧急(≥ 高)",
|
||||
tone: "warn",
|
||||
hint: "安静时段内仍被放行 ⇒ 档位至少 high;记录里无法区分 high 与 critical,故显示下限",
|
||||
};
|
||||
case "critical":
|
||||
return { label: "紧急·安全域", tone: "err", hint: "安全域事件(可穿透工作/忙碌与安静时段)" };
|
||||
case undefined:
|
||||
case "":
|
||||
return {
|
||||
label: URGENCY_UNKNOWN_LABEL,
|
||||
tone: "neutral",
|
||||
hint: "这一轮的档位在存档里不可证(轮次不在安静时段窗口内,记录不保存档位)——不是「例常」",
|
||||
};
|
||||
default:
|
||||
return { label: level, tone: "neutral", hint: "后端给了本面板不认识的档位,原样显示" };
|
||||
if (level === undefined || level === "") {
|
||||
return {
|
||||
label: URGENCY_UNKNOWN_LABEL,
|
||||
tone: "neutral",
|
||||
hint: "这一轮的档位在存档里不存在(该行没有落库档位,且不在安静时段窗口内 —— 窗口之外判定不看档位)——不是「例常」",
|
||||
};
|
||||
}
|
||||
if (item.urgencyLowerBound === true) {
|
||||
return {
|
||||
// 未知取值原样显示(后端加了本面板不认识的档位时不猜一个更好听的词),但下限前缀照加。
|
||||
label: URGENCY_LOWER_BOUND_PREFIX + (EXACT_LABEL[level] ?? level),
|
||||
tone: exactTone(level),
|
||||
hint: LOWER_BOUND_HINT,
|
||||
};
|
||||
}
|
||||
const exact = EXACT_LABEL[level];
|
||||
if (exact === undefined) {
|
||||
return { label: level, tone: "neutral", hint: "后端给了本面板不认识的档位,原样显示" };
|
||||
}
|
||||
return { label: exact, tone: exactTone(level), hint: `该轮实际生效的档位(精确值):${level}` };
|
||||
}
|
||||
|
||||
/** quietHoursBypassTag 只在后端确实标记了穿透时返回标签。 */
|
||||
|
||||
Reference in new issue
Block a user