@te-river/opencode-alibabatokenplan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,418 @@
1
+ // TUI 插件入口(03-D4 展示矩阵)。default export { id, tui }。
2
+ //
3
+ // 渲染方式:不经 JSX 编译链——直接用 @opentui/solid 的运行时原语
4
+ // (createElement/setProp/effect/insert/createComponent) 手工构建,与 Solid 编译器
5
+ // 产物同构(quota dist/tui.js 头部实证宿主吃这套导出)。
6
+
7
+ import { createElement, setProp, effect, insert, createComponent } from "@opentui/solid";
8
+ import { createSignal, onCleanup } from "solid-js";
9
+ import type { StatusFile, WindowView } from "../core/types";
10
+ import { loadConfig, type AtpConfig } from "../core/status";
11
+ import { windowState, fmtHMS, fmtShortDur } from "../core/rules";
12
+ import { fmtFactor, homeSummaryLine, sourceBadgeLine } from "../core/text";
13
+ import { isCrossVerified, canonicalize } from "../core/merge";
14
+ import { QODER_INSALE_FACTORS, QODER_SNAPSHOT_DATE } from "../core/qoder-snapshot";
15
+ import { loadStatusView, type StatusView } from "./read-status";
16
+
17
+ const PKG_ID = "@te-river/opencode-alibabatokenplan";
18
+ const POLL_MS = 30_000;
19
+ const EVENT_DEBOUNCE_MS = [150, 600]; // quota 同款节奏
20
+
21
+ // ─────────────────────────── M3:全模块唯一 1Hz 时钟 ───────────────────────────
22
+ // tui() 顶层 createSignal(now) + 仅一个 setInterval,双清理(Solid root onCleanup +
23
+ // api.lifecycle.onDispose);sidebar/dialog/home 组件只在响应式 accessor 里读 getNow()
24
+ // 订阅该信号,不再各自建局部 setInterval(旧实现每次重挂槽位/对话框都泄漏一个定时器)。
25
+ let getNow: () => number = () => Date.now(); // tui() 未运行时的兜底(槽件不会先于 tui() 渲染)
26
+ let stopSharedTicker: (() => void) | null = null;
27
+
28
+ // ─────────────────────────── 无 JSX 渲染助手 ───────────────────────────
29
+
30
+ type Child = string | number | boolean | null | undefined | (() => unknown) | object | Child[];
31
+ interface HProps { [k: string]: unknown }
32
+
33
+ /** h(type, props, ...children)。props 里函数值=响应式 accessor(编译器等价)。 */
34
+ function h(type: string, props: HProps | null, ...children: Child[]): any {
35
+ const el: any = createElement(type);
36
+ for (const [k, v] of Object.entries(props ?? {})) {
37
+ if (k === "ref" && typeof v === "function") { (v as (e: unknown) => void)(el); continue; }
38
+ if (typeof v === "function" && !k.startsWith("on")) {
39
+ // 模拟 babel solid 输出:_$effect(_$p => _$setProp(el, k, expr, _$p))
40
+ effect((p: unknown) => { setProp(el, k, (v as () => unknown)(), p); });
41
+ } else {
42
+ setProp(el, k, v);
43
+ }
44
+ }
45
+ for (const c of children) insertChild(el, c);
46
+ return el;
47
+ }
48
+
49
+ function insertChild(parent: any, c: Child): void {
50
+ if (c === null || c === undefined || c === false || c === "") return;
51
+ if (Array.isArray(c)) { for (const x of c) insertChild(parent, x); return; }
52
+ insert(parent, c as any);
53
+ }
54
+
55
+ /** 一行文本;content 可为字符串或 accessor(每秒 tick 就地更新,不重建节点)。 */
56
+ function line(text: string | (() => string), style: HProps = {}): any {
57
+ const props: HProps = { wrapMode: "word", ...style, content: text };
58
+ return h("text", props);
59
+ }
60
+
61
+ // ─────────────────────────── provider 门控(03-D4 / 02 伪码双保险) ───────────────────────────
62
+
63
+ const TP_PROVIDER_RE = /^alibaba-token-plan(-cn)?$/i;
64
+ const TP_HOST_RE = /token-plan\.[a-z0-9-]+\.maas\.aliyuncs\.com/i;
65
+
66
+ function isTPProviderID(id: unknown): boolean {
67
+ return typeof id === "string" && TP_PROVIDER_RE.test(id);
68
+ }
69
+
70
+ function sessionProviderID(api: any, sessionID: string): string | undefined {
71
+ try {
72
+ const sess: any = api.state.session.get(sessionID);
73
+ const direct = sess?.data?.model?.providerID ?? sess?.model?.providerID ?? sess?.providerID;
74
+ if (typeof direct === "string" && direct) return direct;
75
+ } catch {}
76
+ try {
77
+ const msgs = api.state.session.messages(sessionID) ?? [];
78
+ for (let i = msgs.length - 1; i >= 0; i--) {
79
+ const mi: any = msgs[i]?.info ?? msgs[i];
80
+ if (typeof mi?.providerID === "string" && mi.providerID) return mi.providerID;
81
+ }
82
+ } catch {}
83
+ return undefined;
84
+ }
85
+
86
+ function sessionIsTokenPlan(api: any, sessionID: string): boolean {
87
+ const pid = sessionProviderID(api, sessionID);
88
+ if (isTPProviderID(pid)) return true;
89
+ if (!pid) return false;
90
+ try {
91
+ const p: any = (api.state.provider as any[])?.find((x) => x?.id === pid || x?.provider?.id === pid);
92
+ const base = p?.options?.baseURL ?? p?.provider?.options?.baseURL ?? "";
93
+ return TP_HOST_RE.test(String(base));
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ function anyTokenPlanProvider(api: any): boolean {
100
+ try {
101
+ for (const p of (api.state.provider as any[]) ?? []) {
102
+ if (isTPProviderID(p?.id) || isTPProviderID(p?.provider?.id)) return true;
103
+ const base = p?.options?.baseURL ?? p?.provider?.options?.baseURL ?? "";
104
+ if (TP_HOST_RE.test(String(base))) return true;
105
+ }
106
+ } catch {}
107
+ return false;
108
+ }
109
+
110
+ // ─────────────────────────── 数据视图(轮询 + 事件去抖) ───────────────────────────
111
+
112
+ interface ViewState {
113
+ get: () => StatusView;
114
+ refresh: () => void;
115
+ cfg: AtpConfig;
116
+ }
117
+
118
+ function createViewState(api: any): ViewState {
119
+ const cfg = loadConfig();
120
+ const [view, setView] = createSignal<StatusView>(loadStatusView());
121
+ const refresh = () => { try { setView(loadStatusView()); } catch {} };
122
+ const timer = setInterval(refresh, POLL_MS);
123
+ timer.unref?.();
124
+ const offs: Array<() => void> = [];
125
+ let t1: any, t2: any;
126
+ const debounced = () => {
127
+ clearTimeout(t1); clearTimeout(t2);
128
+ t1 = setTimeout(refresh, EVENT_DEBOUNCE_MS[0]);
129
+ t2 = setTimeout(refresh, EVENT_DEBOUNCE_MS[1]);
130
+ };
131
+ for (const ev of ["message.updated", "session.idle", "tui.session.select"] as const) {
132
+ try { offs.push(api.event.on(ev as any, debounced)); } catch {}
133
+ }
134
+ onCleanup(() => {
135
+ clearInterval(timer); clearTimeout(t1); clearTimeout(t2);
136
+ for (const off of offs) { try { off(); } catch {} }
137
+ });
138
+ return { get: view, refresh, cfg };
139
+ }
140
+
141
+ // ─────────────────────────── sidebar 面板(≤8 行) ───────────────────────────
142
+
143
+ function wState(w: { start: string; end: string }, now: number) {
144
+ return windowState(w, new Date(now));
145
+ }
146
+
147
+ function currentWindow(st: StatusFile, now: number): WindowView | undefined {
148
+ return [...st.windows].sort((a, b) => {
149
+ const aa = wState(a, now).active ? 0 : 1, bb = wState(b, now).active ? 0 : 1;
150
+ if (aa !== bb) return aa - bb;
151
+ return new Date(a.nextBoundaryAt).getTime() - new Date(b.nextBoundaryAt).getTime();
152
+ })[0];
153
+ }
154
+
155
+ function sessionModelName(api: any, sessionID: string): string | undefined {
156
+ try {
157
+ const sess: any = api.state.session.get(sessionID);
158
+ const m = sess?.data?.model?.modelID ?? sess?.model?.modelID ?? sess?.model?.id;
159
+ if (m) return canonicalize(String(m));
160
+ const msgs = api.state.session.messages(sessionID) ?? [];
161
+ for (let i = msgs.length - 1; i >= 0; i--) {
162
+ const mi: any = msgs[i]?.info ?? msgs[i];
163
+ if (mi?.modelID) return canonicalize(String(mi.modelID));
164
+ }
165
+ } catch {}
166
+ return undefined;
167
+ }
168
+
169
+ function strikethroughText(s: string): string {
170
+ return s.replace(/[^\u0300-\u036f]/g, (c) => c + "\u0336"); // 终端删除线模拟(D4)
171
+ }
172
+
173
+ function buildSidebar(api: any, vs: ViewState, sessionID: string, collapsed: () => boolean, toggle: () => void): any {
174
+ const theme = () => api.theme.current;
175
+ const now = getNow; // M3:订阅全模块唯一 1Hz 信号(不再局部 setInterval)
176
+
177
+ const header = h("box", { style: { flexDirection: "row", gap: 1 }, onMouseUp: toggle },
178
+ line("Token Plan 优惠", { fg: () => theme().text }),
179
+ line(
180
+ () => (vs.get().degraded ? `静态快照(截至${QODER_SNAPSHOT_DATE})` : sourceBadgeLine(vs.get().status)),
181
+ { fg: () => (vs.get().degraded ? theme().textMuted : theme().success) },
182
+ ),
183
+ );
184
+
185
+ const body = h("box", { style: { flexDirection: "column" } });
186
+ // 外层=数据变化时重建行;行内 content=accessor 每秒就地走字
187
+ insert(body, () => {
188
+ const sv = vs.get();
189
+ if (collapsed()) {
190
+ return line(() => "已折叠 · /tokenplan 查看", { fg: theme().textMuted });
191
+ }
192
+ const s = sv.status;
193
+ const w = currentWindow(s, now());
194
+ if (!w) return line("暂无已知优惠窗口", { fg: theme().textMuted });
195
+ const star = w.models[0];
196
+ const rows: any[] = [
197
+ line(() => `${wState(w, now()).active ? "▸" : "▹"} ${w.label} ${w.start}–${w.end}${star?.effective ? " " + star.effective : ""}`, {
198
+ fg: () => (wState(w, now()).active ? theme().success : theme().text),
199
+ }),
200
+ line(() => `${fmtHMS(new Date(w.nextBoundaryAt).getTime() - now())} ${wState(w, now()).active ? "后结束·进行中" : "后开始"}`, {
201
+ fg: () => (wState(w, now()).active ? theme().success : theme().warning),
202
+ }),
203
+ ];
204
+ const model = sessionModelName(api, sessionID);
205
+ if (model) {
206
+ const entry = s.discounts.find((d) => d.model === model);
207
+ const qf = QODER_INSALE_FACTORS[model];
208
+ const factorRules = (entry?.applies ?? []).filter(
209
+ (r) => r.unit === "credits-factor" && r.factor !== undefined &&
210
+ (r.kind === "time-window" && r.window ? wState(r.window, now()).active : true),
211
+ );
212
+ const cur = factorRules[factorRules.length - 1];
213
+ const fStr = fmtFactor(cur?.factor ?? qf?.factor) ?? "—";
214
+ const bStr = fmtFactor(cur?.baseFactor ?? qf?.factor);
215
+ const struck = bStr && bStr !== fStr ? strikethroughText(bStr) + " " : ""; // 划线价=字符串相等判断
216
+ rows.push(
217
+ line(
218
+ () =>
219
+ `当前 ${model} ${struck}${fmtFactor(cur?.factor ?? QODER_INSALE_FACTORS[model]?.factor)}${
220
+ cur ? ` ${cur.label}${isCrossVerified(cur) ? " [双源印证]" : ""}` : " Credits倍率"
221
+ }`,
222
+ { fg: () => theme().accent },
223
+ ),
224
+ );
225
+ }
226
+ for (const o of s.windows.filter((x) => x.id !== w.id).slice(0, 2)) {
227
+ rows.push(
228
+ line(() => {
229
+ const ws = wState(o, now());
230
+ return `${o.label} · ${ws.active ? "剩" : "距"} ${fmtShortDur(new Date(o.nextBoundaryAt).getTime() - now())}${ws.active ? "结束" : "开始"}`;
231
+ }, { fg: () => theme().textMuted }),
232
+ );
233
+ }
234
+ if (sv.degraded) rows.push(line(`快照可能失效 · 数据截至 ${QODER_SNAPSHOT_DATE}`, { fg: () => theme().warning }));
235
+ return h("box", { style: { flexDirection: "column" } }, ...rows.slice(0, 6));
236
+ });
237
+
238
+ return h("box", { style: { flexDirection: "column" } }, header, body);
239
+ }
240
+
241
+ // ─────────────────────────── /tokenplan Dialog ───────────────────────────
242
+
243
+ const TABS = [
244
+ { key: "windows", title: "窗口倒计时", desc: "各优惠窗口与下一边界(每秒走字)" },
245
+ { key: "models", title: "模型倍率", desc: "百炼目录价 + Qoder 在卖倍率" },
246
+ { key: "limited", title: "限时优惠", desc: "flat-cut 明细(两单位不互算)" },
247
+ { key: "free", title: "免费活动", desc: "free-calls / policy 与时效声明" },
248
+ { key: "about", title: "口径说明", desc: "数据源、汇率、快照红线" },
249
+ ] as const;
250
+
251
+ function openMainDialog(api: any, vs: ViewState): void {
252
+ try {
253
+ api.ui.dialog.replace(() =>
254
+ createComponent(api.ui.DialogSelect as any, {
255
+ title: `Token Plan 优惠(Qoder 快照截至 ${QODER_SNAPSHOT_DATE})`,
256
+ options: TABS.map((t) => ({ title: t.title, value: t.key, description: t.desc })),
257
+ current: (() => { try { return api.kv.get("atp.lastDialogTab", "windows"); } catch { return "windows"; } })(),
258
+ onSelect: (opt: any) => {
259
+ try { api.kv.set("atp.lastDialogTab", opt.value); } catch {}
260
+ openTabDialog(api, vs, String(opt.value));
261
+ },
262
+ }),
263
+ );
264
+ api.ui.dialog.setSize("medium");
265
+ } catch {
266
+ try { api.ui.toast({ variant: "error", message: "Token Plan 面板打开失败" }); } catch {}
267
+ }
268
+ }
269
+
270
+ function openTabDialog(api: any, vs: ViewState, tab: string): void {
271
+ const theme = () => api.theme.current;
272
+ const now = getNow; // M3:订阅共享 1Hz 时钟
273
+
274
+ const rowsFor = (): string[] => {
275
+ const s = vs.get().status;
276
+ const tt = now();
277
+ switch (tab) {
278
+ case "windows":
279
+ return s.windows.flatMap((w) => {
280
+ const ws = wState(w, tt);
281
+ const head = `${w.label} [${w.start}–${w.end}] ${ws.active ? "进行中" : "未开始"} · ${ws.active ? "距结束" : "距开始"} ${fmtHMS(new Date(w.nextBoundaryAt).getTime() - tt)}`;
282
+ const rows = w.models.map(
283
+ (m) =>
284
+ ` ${m.model} ${m.regular && m.regular !== m.effective ? strikethroughText(m.regular) + " " : ""}${m.effective ?? ""} ${m.badge ?? ""}${isCrossVerified(m) ? " [双源印证]" : ""}`,
285
+ );
286
+ return [head, ...rows];
287
+ });
288
+ case "models":
289
+ return [
290
+ "— 百炼目录(Credits/百万tok)—",
291
+ ...s.textModels.map((m) => `${m.model} 输入 ${m.input} 缓存 ${m.cache} 输出 ${m.output}${m.bands ? `(${m.bands})` : ""}${m.creditsFactor ? ` · Qoder ${m.creditsFactor}` : ""}${m.badge ? ` · ${m.badge.label}` : ""}`),
292
+ "— Qoder 在卖倍率(快照,非优惠)—",
293
+ ...Object.entries(QODER_INSALE_FACTORS).map(([k, v]) => `${k} ${fmtFactor(v.factor)}${v.note ? ` (${v.note})` : ""}`),
294
+ ];
295
+ case "limited":
296
+ return s.offers
297
+ .filter((r) => r.kind === "flat-cut")
298
+ .flatMap((r) => [
299
+ `${r.models.join(", ")} ×${r.factor ?? "-"}(${r.unit === "credits-factor" ? "Qoder Credits" : "百炼 ¥"})${r.batchStack !== undefined ? ` Batch叠加×${r.batchStack}` : ""}`,
300
+ ...(r.note ? [" " + r.note] : []),
301
+ ]);
302
+ case "free":
303
+ return s.notices.map((n) => `[${n.kind}] ${n.text}${n.expired ? "(已过期,快照可能失效)" : ""} · ${n.source}`);
304
+ default:
305
+ return [
306
+ `百炼目录源: ${s.sources.catalog}(live→cli→snapshot 三级回退)`,
307
+ `Qoder 快照: ${s.sources.qoderSnapshot}(静态,不调用任何 Qoder 私有端点)`,
308
+ `换算注记: 1 元 = ${s.creditsPerYuan} Credits(${s.creditsRateNote})`,
309
+ "两套单位永不互算:Credits 倍率与 ¥ 价格倍率各居其 facet",
310
+ "折后消耗显示「消耗/Cost,单位 Credit」;runtimeConfig 会变实际消耗",
311
+ s.quota.available ? `额度: 实时(${s.quota.checkedAt})` : `额度: ${s.quota.hint ?? "不可用"}`,
312
+ ];
313
+ }
314
+ };
315
+
316
+ try {
317
+ api.ui.dialog.replace(() => {
318
+ const scroll = h("scrollbox", { style: { flexGrow: 1, flexDirection: "column" } });
319
+ insert(scroll, () => {
320
+ const container = h("box", { style: { flexDirection: "column" } });
321
+ for (const r of rowsFor()) insertChild(container, line(r));
322
+ return container;
323
+ });
324
+ return h(
325
+ "box",
326
+ { style: { flexDirection: "column", height: "100%" } },
327
+ line(`${TABS.find((x) => x.key === tab)?.title ?? tab} · 数据截至 ${QODER_SNAPSHOT_DATE} · esc 关闭`, { fg: () => theme().textMuted }),
328
+ scroll,
329
+ );
330
+ });
331
+ api.ui.dialog.setSize("large");
332
+ } catch {
333
+ try { api.ui.toast({ variant: "error", message: "Token Plan 明细打开失败" }); } catch {}
334
+ }
335
+ }
336
+
337
+ // ─────────────────────────── 插件入口 ───────────────────────────
338
+
339
+ const tui = async (api: any, options?: unknown, _meta?: unknown) => {
340
+ const g = globalThis as any;
341
+ if (g.__atpTuiLoaded) return; // 双实例守卫(kv 键统一 atp. 命名空间)
342
+ g.__atpTuiLoaded = true;
343
+
344
+ // ── M3:全模块唯一 1Hz ticker,本函数顶层注册一次;双清理=Solid root onCleanup + 宿主 onDispose ──
345
+ const [nowSig, setNowSig] = createSignal(Date.now());
346
+ getNow = nowSig;
347
+ const sharedTick = setInterval(() => setNowSig(Date.now()), 1000);
348
+ sharedTick.unref?.();
349
+ let stopped = false;
350
+ stopSharedTicker = () => { if (stopped) return; stopped = true; clearInterval(sharedTick); };
351
+ try { onCleanup(stopSharedTicker); } catch {} // 宿主未包 Solid root 时静默,靠 onDispose 兜底
352
+ const stopTicker = stopSharedTicker;
353
+
354
+ const vs = createViewState(api);
355
+ let cfg = vs.cfg;
356
+ try {
357
+ const extra = typeof options === "string" ? undefined : (options as any);
358
+ if (extra && typeof extra === "object") {
359
+ for (const k of ["sidebar", "webpanel", "toast", "refresh"]) {
360
+ const v = extra[k];
361
+ if (v === undefined) continue;
362
+ // m7:toast 等布尔/标量键直接赋值(旧 `if(extra[k]) Object.assign(cfg[k], v)` 对 boolean
363
+ // 既丢 false 又静默失效),对象键才做浅合并
364
+ if (typeof v !== "object" || v === null) (cfg as any)[k] = v;
365
+ else Object.assign((cfg as any)[k] ?? {}, v);
366
+ }
367
+ }
368
+ } catch {}
369
+ void cfg;
370
+
371
+ const collapsed = () => { try { return !!api.kv.get("atp.collapsed", false); } catch { return false; } };
372
+ const toggleCollapsed = () => { try { api.kv.set("atp.collapsed", !collapsed()); } catch {} };
373
+
374
+ api.slots.register({
375
+ order: cfg.sidebar.order,
376
+ slots: {
377
+ sidebar_content(_ctx: any, props: any) {
378
+ const sid = String(props?.session_id ?? "");
379
+ if (!sid) return null;
380
+ const tp = sessionIsTokenPlan(api, sid);
381
+ if (!tp && !cfg.sidebar.showNonTokenPlan) return null; // 默认非 TP 整块隐藏(D4 取舍)
382
+ if (!tp) {
383
+ // showNonTokenPlan=true → 弱化一行
384
+ return line(() => {
385
+ const w = currentWindow(vs.get().status, Date.now());
386
+ return w ? `Token Plan: ${w.label} ${wState(w, Date.now()).active ? "进行中" : w.start + " 开始"}` : "Token Plan: 无已知窗口";
387
+ }, { fg: () => api.theme.current.textMuted });
388
+ }
389
+ return buildSidebar(api, vs, sid, collapsed, toggleCollapsed);
390
+ },
391
+ home_bottom() {
392
+ if (!anyTokenPlanProvider(api)) return null; // 门控=state.provider 含任一 TP 条目
393
+ // M3:复用共享 1Hz 时钟(摘要粒度到分钟,逐秒重算成本可忽略),不再局部 setInterval
394
+ return line(() => homeSummaryLine(vs.get().status, new Date(getNow())), { fg: () => api.theme.current.textMuted });
395
+ },
396
+ },
397
+ });
398
+
399
+ const dispose = api.keymap.registerLayer({
400
+ commands: [{
401
+ namespace: "palette",
402
+ name: "tokenplan.main",
403
+ title: "Token Plan 优惠总览",
404
+ desc: `百炼+Qoder 快照优惠窗口/倍率/倒计时(数据截至 ${QODER_SNAPSHOT_DATE})`,
405
+ category: "Token Plan",
406
+ slashName: "tokenplan",
407
+ run: () => { vs.refresh(); openMainDialog(api, vs); },
408
+ }],
409
+ bindings: [],
410
+ });
411
+ api.lifecycle.onDispose(() => {
412
+ try { stopTicker(); } catch {} // M3:宿主生命周期终止时停掉共享 ticker
413
+ try { dispose(); } catch {}
414
+ });
415
+ };
416
+
417
+ export default { id: PKG_ID, tui };
418
+ export const AliBabaTokenPlanTui = tui;
@@ -0,0 +1,45 @@
1
+ // TUI 只读侧数据源:~/.cache/opencode/alibabatokenplan/status.json(30s 轮询+事件去抖在
2
+ // index.tsx 做),缺席/损坏/陈旧 → 内嵌 Qoder 快照降级(契约 §2 读写契约)。
3
+
4
+ import { buildStatus, readStatusFile, type StatusRead } from "../core/status";
5
+ import { windowState } from "../core/rules";
6
+ import type { StatusFile } from "../core/types";
7
+
8
+ export interface StatusView {
9
+ status: StatusFile;
10
+ /** true = status.json 不可用/陈旧,当前渲染的是随包内嵌的 Qoder 静态快照 */
11
+ degraded: boolean;
12
+ }
13
+
14
+ /** m4:stale/降级时不把文件里的绝对 nextBoundaryAt 走字——它可能已落在过去(倒计时冻结 00:00:00)。
15
+ * 改为按各窗口 start/end(HH:MM, tz) 用本地 now 重算下一边界;activeNow/boundaryKind 同步刷新。 */
16
+ function recomputeWindows(status: StatusFile, now: Date): StatusFile {
17
+ return {
18
+ ...status,
19
+ windows: status.windows.map((w) => {
20
+ const ws = windowState(w, now);
21
+ return { ...w, activeNow: ws.active, boundaryKind: ws.boundaryKind, nextBoundaryAt: ws.nextBoundaryAt };
22
+ }),
23
+ };
24
+ }
25
+
26
+ /** 随包内嵌降级视图:仅 Qoder 快照(目录置空、catalog 记 snapshot),零网络。 */
27
+ export function embeddedSnapshotView(now = new Date()): StatusView {
28
+ const status = buildStatus({ source: "snapshot", models: [], fetchedAt: "embedded" }, now);
29
+ return { status, degraded: true };
30
+ }
31
+
32
+ export function loadStatusView(now = new Date()): StatusView {
33
+ const r: StatusRead = readStatusFile(now);
34
+ switch (r.kind) {
35
+ case "live":
36
+ return { status: r.status, degraded: false };
37
+ case "stale":
38
+ // 陈旧:数据仍可比快照更有信息量时保留窗口结构,但徽标必须转灰;m4 重算边界防倒计时冻结
39
+ return { status: recomputeWindows(r.status, now), degraded: true };
40
+ default:
41
+ // status.json 缺席/损坏降级:与 stale 同规则,用本地 now 重算(内嵌视图虽用 now 构建,
42
+ // 但读取时刻可能已跨过边界,统一按窗口边界重算保持一致)
43
+ return { status: recomputeWindows(embeddedSnapshotView(now).status, now), degraded: true };
44
+ }
45
+ }