feat(admin): 时间轴只读权威键 + 自排筛选轴 + browser 面板图标(Y2+Y3+Y4 前端一并落)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 时间轴:页首「已过保留期」的**唯一**判据改为 retentionBoundaryReached === true(truncated 只出
  「未显示」);缺席(老后端)一句话都不说;删掉旧的 truncated+retentionEnabled+翻到底 启发式
- Backthink:筛选轴接上服务端 ?trigger=(客户端筛选保留作兜底)+ 自排闸用量行(三态文案)
- plugin/nav.ts 加 browser 图标
- 真机(隔离实例,46/46):翻页不重不漏、三种「必须 false」的权威键语义、**重启后 round 不重复**
⚠️ 未验:真机上没构造出权威键 true(需真有更早轮次被清扫)——别读成「真机验过 true」
This commit is contained in:
AskaEth committed 2026-10-06 21:48:52 +08:00
1 parent e0a6bca501
commit 57d0e8c543
11 files changed
+490 -102

No files matched your search

@@ -3,8 +3,11 @@
// 为什么需要它:时间轴的四条硬口径全是**判定**而非渲染 —— ① 顺序与分页游标(`before`
// 恒向更早走,与展示顺序无关);② **不臆造关系**(去重只认后端给的回合标识,标识缺席
// 也不自己拼一个);③ 状态与文案必须把「无图 / 未收尾 / 完成」分开说;④ 老后端(404)
// 与「没有回合」必须分成两件事。本项目没有前端测试运行器,因此这是无依赖的纯 node 检查
// (与 `flowHistory.test.ts` / `flowSession.test.ts` 同一手法)。
// 与「没有回合」必须分成两件事。⑤(v0.81 追补)**「已过保留期」只认权威键**
// `retentionBoundaryReached === true`:`truncated` / `retentionEnabled` / 「翻到底了没」
// 的任何组合都**不再是**判据,权威键缺席(老后端)⇒ 按最保守话术(一句也不说)。本项目
// 没有前端测试运行器,因此这是无依赖的纯 node 检查(与 `flowHistory.test.ts` /
// `flowSession.test.ts` 同一手法)。
//
// 跑法:
//
@@ -31,7 +34,6 @@ import {
orderRounds,
pageHasOlder,
pageMayHaveOlder,
retentionClaimable,
retentionText,
roundExpanded,
roundGraphs,
@@ -250,14 +252,24 @@ const packed = timelinePageOf({
rounds: [r1],
retentionHours: 168,
truncated: true,
retentionBoundaryReached: true,
});
same(packed.rounds.length, 1, "解包保留回合");
same(packed.retentionHours, 168, "解包保留期");
same(packed.truncated, true, "解包截断标记");
same(packed.retentionBoundaryReached, true, "解包**权威键**(v0.81):不能丢、不能反相");
const packedEmpty = timelinePageOf(null);
same(packedEmpty.rounds.length, 0, "没有响应 ⇒ 空回合");
same(packedEmpty.retentionHours, null, "没有响应 ⇒ 保留期未知(不是 0)");
same(packedEmpty.truncated, false, "没有响应 ⇒ 不宣称「已过保留期」");
same(packedEmpty.truncated, false, "没有响应 ⇒ 不宣称「本次不是全部」");
same(packedEmpty.retentionBoundaryReached, null, "没有响应 ⇒ 权威键为**未知**(不冒充 false)");
// 老后端(v0.80 出口,没有这个键):必须落成 `null`(不知道),**不许**用别的字段回推。
const packedOld = timelinePageOf({ rounds: [r1], retentionHours: 168, truncated: true });
same(
packedOld.retentionBoundaryReached,
null,
"老后端没给权威键 ⇒ 解包成 null(不知道),**不**退回启发式"
);
// ── ⑫ 真跑 api 客户端:URL 形状 + 游标**原样**回传(不换算、不拼 id) ───────
type Call = { url: string; method: string };
@@ -355,18 +367,33 @@ same(
);
same(timelineCursorOf({ nextBefore: null, rounds: [] }), null, "都没有 ⇒ 没有游标(不瞎凑)");
// ── ⑭ 保留期能不能用来解释缺失(`retentionClaimable` 的三支) ────────────────
ok(retentionClaimable({ retentionHours: 168, retentionEnabled: null }), "有小时数且 >0 ⇒ 可以解释");
ok(!retentionClaimable({ retentionHours: 0, retentionEnabled: null }), "≤0 = 配置为不回收 ⇒ 不许说过期");
// ── ⑭ 权威键缺席 = 不知道(老后端不得拿别的字段回推) ────────────────────────
// v0.81 起「已过保留期」的判据**只有** `retentionBoundaryReached === true`:老后端(v0.80
// 出口)没这个键 ⇒ 解包成 `null`,「保留期开着」这件事本身**不足以**支撑那句话。
const oldBackendPage = timelinePageOf({
rounds: [r1],
retentionHours: 168,
retentionEnabled: true,
truncated: true,
});
same(oldBackendPage.retentionBoundaryReached, null, "老后端缺权威键 ⇒ 状态里是 null(不知道)");
// 喂给判定的入参:与 `timelineHints` 的真实入参同形(`canLoadMore` 由取数层算,这里取最保守的 false)。
const oldBackendHints = timelineHints({
truncated: oldBackendPage.truncated,
canLoadMore: false,
retentionBoundaryReached: oldBackendPage.retentionBoundaryReached,
source: oldBackendPage.source,
});
ok(
!retentionClaimable({ retentionHours: 168, retentionEnabled: false }),
"后端显式说没启用 ⇒ **永远**不许说过期(哪怕小时数字段还带着 168)"
!oldBackendHints.expired,
"「保留期启用 + truncated」**不构成**过期的判据(v0.81 明令禁止自己拼)"
);
ok(retentionClaimable({ retentionHours: null, retentionEnabled: true }), "显式说启用了 ⇒ 可以解释");
ok(!retentionClaimable({ retentionHours: null, retentionEnabled: null }), "两样都不知道 ⇒ 不说");
ok(oldBackendHints.hidden, "缺权威键 + truncated ⇒ 只能挂「更早的轮次未显示」");
// ── ⑮ 页首三句话的判定(`timelineHints`):互不冒充 ─────────────────────────
const hintBase = { truncated: true, canLoadMore: false, retentionHours: 168, retentionEnabled: true, source: null };
// 入参只认「后端给的键」:`truncated` / `canLoadMore` / `retentionBoundaryReached` / `source`。
// ⚠️ `retentionHours` / `retentionEnabled` **已不再**是入参(它们去掉了判据资格)。
const hintBase = { truncated: true, canLoadMore: false, retentionBoundaryReached: null, source: null };
/** 三支一起比(对象比较要逐字段,`same` 只比标量)。 */
function hintsOf(h: TimelineHints): string {
return `${h.hidden ? "hidden" : "-"}/${h.expired ? "expired" : "-"}/${h.liveOnly ? "liveOnly" : "-"}`;
@@ -378,19 +405,34 @@ same(
"还有更早可拉 ⇒ 只能「未显示」(**绝不**说「已过保留期」)"
);
same(
hintsOf(timelineHints(hintBase)),
"-/expired/-",
"翻到底仍不是全部 + 保留期启用 ⇒ 才说「已过保留期」"
);
same(
hintsOf(timelineHints({ ...hintBase, retentionHours: 0, retentionEnabled: false })),
hintsOf(timelineHints({ ...hintBase, retentionBoundaryReached: false })),
"hidden/-/-",
"保留期未启用 ⇒ 只能说「未显示」"
"后端明确说 false(本次不是全部但没被清扫)⇒ 说「未显示」"
);
same(
hintsOf(timelineHints({ ...hintBase, source: "live" })),
hintsOf(timelineHints({ ...hintBase, truncated: false, canLoadMore: false, retentionBoundaryReached: true })),
"-/expired/-",
"权威键 true ⇒ 才说「已过保留期」(哪怕这一页 truncated=false:已到最早一端)"
);
same(
hintsOf(timelineHints({ ...hintBase, retentionBoundaryReached: true })),
"-/expired/-",
"权威键 true 时**不再**同时挂「未显示」(一句话,不是两句)"
);
same(
hintsOf(timelineHints({ ...hintBase, source: "live", retentionBoundaryReached: false })),
"hidden/-/liveOnly",
"本页全来自内存(落盘源未接线)⇒ 「只拿到最近一轮」,且**不**说成过期"
"本页全来自内存(落盘源未接线)⇒ 「只拿到最近一轮」"
);
same(
hintsOf(timelineHints({ ...hintBase, source: "live", retentionBoundaryReached: true })),
"hidden/-/liveOnly",
"本页全来自内存时**压住**过期声明(本页没有落盘对照,无从复核 ⇒ 少说)"
);
same(
hintsOf(timelineHints({ truncated: false, canLoadMore: false, retentionBoundaryReached: null, source: null })),
"-/-/-",
"一切未知 / 什么都没说 ⇒ 一句话都不说(最保守话术)"
);
console.log(`flow timeline logic test: ${checks} checks passed`);