dsh-session-guard 4.1.11 → 4.1.16

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.
package/CHANGELOG.ja.md CHANGED
@@ -11,6 +11,35 @@
11
11
  - [Changelog на русском](./CHANGELOG.ru.md)
12
12
  - [Changelog en español](./CHANGELOG.es.md)
13
13
 
14
+ ## 4.1.16 — 2026-10-14(マニフェスト整合/欠落していた 4.1.15 の補記)
15
+
16
+ ### 修正
17
+
18
+ - `dsh.plugin.json` の版数を `package.json` と一致させた。公開済みの 4.1.15 ターボールは
19
+ まだ 4.1.14 と書かれていた(レジストリから実際にダウンロードして確認)。
20
+ - 動作変更なし。
21
+ - テストの構造ピンが退役済みの歯車入口をまだ要求しており、4.1.15 以来スイートが赤だった。
22
+ 不在を断言する側へ修正した。
23
+
24
+ ## 4.1.15 — 2026-10-08(補記:リリース当時この項が書かれていませんでした)
25
+
26
+ ### 削除
27
+
28
+ - シェルレベルの `shell.overlay` シート注入を廃止:右下固定の歯車ボタンと浮層レイヤーを除去
29
+ (`src/client/shell-overlay.tsx` を削除)。設定への入口はホストの設定ページのみを頼る。
30
+
31
+ ## 4.1.14 — 2026-10-08
32
+
33
+ ### 削除
34
+
35
+ ## 4.1.13 — 2026-10-08
36
+
37
+ ### 修正 — ≤0.1.5 でのブリッジ記述子スケルトン(5セルゲートで発覚)
38
+
39
+ - **≤0.1.5 ホストの describe `value` はフィールドスケルトン**(実測:`{enabled:{},…}`、実値は `base`+`user` 層)。旧世代で `descriptor.value` をそのまま消費すると `officialProviders` が `{}` になり `?? []` を素通りして `ListRow` の `values.join` が throw、`dsh-family.tab` スロット全体が殉葬していた。`descriptorPlainValue()` がスケルトンを検出して `{...base, ...user}` を再構築。
40
+ - **カード字段消毒(多層防御)**:スロットは一つの描画エラーで全族タブが死ぬため、`ListRow` 入力を `toList()` で無毒化、`拦截方式` セレクトは非 `error` 値を `hold` にフォールバック。
41
+ - 5セルファームゲートで検証:0.1.0/0.1.5 はブリッジ経由、0.1.7 は configForms 経由でシード値 `321123` の読み返しを確認。
42
+
14
43
  ## 4.0.0 — 2026-09-29
15
44
 
16
45
  ### Changed
package/CHANGELOG.ko.md CHANGED
@@ -11,6 +11,35 @@
11
11
  - [Changelog на русском](./CHANGELOG.ru.md)
12
12
  - [Changelog en español](./CHANGELOG.es.md)
13
13
 
14
+ ## 4.1.16 — 2026-10-14 (매니페스트 정리, 누락된 4.1.15 항목 보충)
15
+
16
+ ### 수정
17
+
18
+ - `dsh.plugin.json` 버전을 `package.json` 과 일치시켰습니다. 게시된 4.1.15 tarball 은
19
+ 여전히 4.1.14 였습니다 (레지스트리에서 직접 내려받아 확인).
20
+ - 동작 변경 없음.
21
+ - 구조 핀이 이미 폐지된 톱니 진입점을 요구해서 4.1.15 이후 테스트가 빨랬습니다.
22
+ 부존재를 단언하는 쪽으로 고쳤습니다.
23
+
24
+ ## 4.1.15 — 2026-10-08 (보충: 배포 당시에 이 항목이 기록되지 않았습니다)
25
+
26
+ ### 삭제
27
+
28
+ - 셸 레벨 `shell.overlay` 슬롯 주입 폐지: 오른쪽 아래 고정 톱니 버튼과 부유 레이어 제거
29
+ (`src/client/shell-overlay.tsx` 삭제). 설정 진입점은 호스트 설정 페이지에만 의존합니다.
30
+
31
+ ## 4.1.14 — 2026-10-08
32
+
33
+ ### 제거
34
+
35
+ ## 4.1.13 — 2026-10-08
36
+
37
+ ### 수정 — ≤0.1.5 브리지 디스크립터 스켈레톤(5셀 게이트 발견)
38
+
39
+ - **≤0.1.5 호스트의 describe `value`는 필드 스켈레톤**(실측: `{enabled:{},…}`, 실값은 `base`+`user` 레이어). 구세대에서 `descriptor.value`를 그대로 쓰면 `officialProviders`가 `{}`가 되어 `?? []`를 통과하고 `ListRow`의 `values.join`이 throw, `dsh-family.tab` 슬롯 전체가 순직.`descriptorPlainValue()`가 스켈레톤을 감지해 `{...base, ...user}`로 재구성.
40
+ - **카드 필드 소독(다층 방어)**: 슬롯은 렌더 오류 하나로 전체 탭이 죽으므로 `ListRow` 입력을 `toList()`로 무독화, `拦截方式` 셀렉트는 non-`error` 값을 `hold`로 폴백.
41
+ - 5셀 팜 게이트 검증: 0.1.0/0.1.5는 브리지 경유, 0.1.7은 configForms 경유로 시드값 `321123` 재판독 확인.
42
+
14
43
  ## 4.0.0 — 2026-09-29
15
44
 
16
45
  ### Changed
package/CHANGELOG.md CHANGED
@@ -11,6 +11,89 @@ All notable changes to `dsh-session-guard` are recorded here. Versions follow se
11
11
  - [Changelog на русском](./CHANGELOG.ru.md)
12
12
  - [Changelog en español](./CHANGELOG.es.md)
13
13
 
14
+ ## 4.1.16 — 2026-10-14 (metadata alignment; backfill the missing 4.1.15 entry)
15
+
16
+ ### Fixed
17
+
18
+ - `dsh.plugin.json` now carries the same version as `package.json`. The published 4.1.15 tarball
19
+ still said 4.1.14 (verified by downloading it from the registry), so the two manifests disagreed in a released artifact.
20
+ - No behaviour change in this release.
21
+ - Test suite repaired: the structural pin in `tests/settings-bridge.test.mjs` still demanded the
22
+ retired gear entry, so the suite had been red since 4.1.15; it now asserts the opposite (absence).
23
+
24
+ ## 4.1.15 — 2026-10-08 (backfilled: this entry was never written at release time)
25
+
26
+ ### Removed
27
+
28
+ - The shell-level `shell.overlay` slot injection: the fixed bottom-right gear button and its
29
+ positioned floating layer are gone (`src/client/shell-overlay.tsx` deleted). The settings entry
30
+ now relies on the host settings page alone.
31
+
32
+ ## 4.1.14 — 2026-10-08
33
+
34
+ ### Removed
35
+
36
+ ## 4.1.13 — 2026-10-08
37
+
38
+ ### Fixed — bridge descriptor skeleton on ≤0.1.5 (five-cell gate catch, 2026-10-08)
39
+
40
+ - **Host describe `value` is a field skeleton on ≤0.1.5** (0.1.0/0.1.5 empirical):
41
+ the old host generations serialize `descriptor.value` as `{enabled:{},…,officialProviders:{}}`
42
+ (one empty object per schema field) while the real merged config lives in
43
+ `descriptor.base` (combined defaults) + `descriptor.user` (user layer).
44
+ `BridgeDocHandle` consumed `value` verbatim, so `officialProviders` arrived as
45
+ `{}` — truthy non-array, invisible to `?? []` — and `ListRow`'s
46
+ `values.join` threw, crashing the whole `dsh-family.tab` slot (guard card +
47
+ IT card + TL nav all died together). `descriptorPlainValue()` now detects the
48
+ skeleton (object whose every own value is an empty plain object — impossible
49
+ for guard's scalar/array config) and rebuilds the effective value as
50
+ `{...base, ...user}` (shallow merge is exact for guard's flat config).
51
+ - **Card-field sanitization (defense in depth)**: any card render error kills
52
+ the entire family tab slot on these hosts, so `ListRow` inputs are now
53
+ sanitized at the call sites (`toList()` — non-arrays become `[]`) and the
54
+ `拦截方式` select falls back to `hold` for any non-`error` value. A malformed
55
+ descriptor may degrade one field, never the tab.
56
+ - Verified on the five-cell farm gate: describe replays on 0.1.0/0.1.5 now
57
+ yield plain merged values; the seeded `stepGateTimeoutMs: 321123` reads back
58
+ through the bridge on ≤0.1.5 and natively (configForms) on 0.1.7.
59
+
60
+ ## 4.1.12 — 2026-10-08
61
+
62
+ ### Added — settings webServer bridge (M4/T20-b): dual-track card data plane
63
+
64
+ - **Top-level card registration refactor (prerequisite)**: the three settings
65
+ faces (`dsh-family.tab`, `plugins.bundle.config`, `settings.plugin.item`,
66
+ `shell.overlay`) were registered inside the `onScope` callback of the dual
67
+ scoped-inject waist — on ≤0.1.5 that callback never fires (settingsScope fiber
68
+ never resolves; 0.1.0/0.1.1 lack both services), so the cards never registered
69
+ and a bridge would have had no consumer. All registrations now run at apply
70
+ top level with a `scopeRef` live reference; the callback degrades to a pure
71
+ data arm (perm-gate `client/index.ts` L166-257 shape).
72
+ - **Server bridge** (`src/settings-bridge.js`): `describe`/`mutate` endpoints at
73
+ `/api/session-guard/settings/*` (POST-only, `{ok,value|error}` envelope,
74
+ redacted describe filtered to the plugin ns, ops forwarded to the host
75
+ settings service with `expectedRevision` optimistic concurrency, 256 KB cap).
76
+ Registration is fail-open (error log, never blocks plugin load).
77
+ - **Client bridge handle** (`src/client/settings-bridge.ts`): `BridgeDocHandle`
78
+ implements the same `SettingsScope` face as the native handles
79
+ (getSnapshot/subscribe/set/unset); describe misses retry within a bounded
80
+ budget (settings service resolving later than card mount) before landing on
81
+ `unavailable`. Card faces read `scopeRef.scope ?? bridgeScope()` — native
82
+ first, bridge fallback (perm-gate T13b dual-track). **Generation split in the
83
+ data arm (five-cell empirical, 2026-10-08 night)**: on 0.1.2/0.1.5 the
84
+ `settingsScope` handle is write-alive but read-dead — `set()` persists to
85
+ settings.yaml while `getSnapshot().value` stays `undefined`, so that arm
86
+ deliberately leaves `scopeRef` empty and the card falls to the bridge (bridge
87
+ mutate lands in the same host service; describe heals the read path).
88
+
89
+ ### Corrected — 4.1.11 publication note
90
+
91
+ - The 4.1.11 entry said "none of 4.1.5-4.1.10 was published to npm"; **4.1.5
92
+ was published** (npm registry confirms `4.1.5` in versions, and `latest`
93
+ pointed at it until this release line). 4.1.6-4.1.10 indeed never shipped.
94
+ The earlier claim came from reading the npmmirror mirror instead of the
95
+ authoritative registry (registry-pin discipline, B2+).
96
+
14
97
  ## 4.1.11 — 2026-10-08
15
98
 
16
99
  ### Fixed — family-section takeover rebuilt (4.1.5 → 4.1.11 arc; none of 4.1.5-4.1.10 was published to npm)
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-session-guard",
3
- "version": "4.1.11",
3
+ "version": "4.1.16",
4
4
  "main": "./src/index.js",
5
5
  "description": "高峰自动会话门:step 级门控 + 周末模式 + 高峰自动暂停 + 官方源二维判定 + 会话级冻结 + 后端自动重试。",
6
6
  "engines": {
package/lib/client.js CHANGED
@@ -429,6 +429,11 @@ window.__ModuleLoader__.load({
429
429
  })]
430
430
  });
431
431
  }
432
+ /** 卡面字段消毒:宿主 describe 形态异常(≤0.1.5 骨架残留等)时槽崩会殉葬全族
433
+ * tab——ListRow 的 values 非 nullish 非数组(如 {})会让 `?? []` 失守,逐卡硬护栏。 */
434
+ function toList(v) {
435
+ return Array.isArray(v) ? v.map(String) : [];
436
+ }
432
437
  /** 插件配置卡片主体:可展开的 <li> + header 按钮 + pill switch 字段体。 */
433
438
  function SessionGuardCard({ scope }) {
434
439
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
@@ -517,7 +522,7 @@ window.__ModuleLoader__.load({
517
522
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListRow, {
518
523
  label: "追加官方 provider id",
519
524
  description: "精确匹配,优先级最高(逗号分隔)",
520
- values: value.officialProviders ?? [],
525
+ values: toList(value.officialProviders),
521
526
  placeholder: "deepseek-official",
522
527
  disabled: readonly,
523
528
  onCommit: (next) => {
@@ -527,7 +532,7 @@ window.__ModuleLoader__.load({
527
532
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListRow, {
528
533
  label: "官方端点名单",
529
534
  description: "baseURL 归一化后的 host(逗号分隔)",
530
- values: value.officialBaseURLs ?? [],
535
+ values: toList(value.officialBaseURLs),
531
536
  placeholder: "api.deepseek.com",
532
537
  disabled: readonly,
533
538
  onCommit: (next) => {
@@ -537,7 +542,7 @@ window.__ModuleLoader__.load({
537
542
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectRow, {
538
543
  label: "拦截方式",
539
544
  description: "挂起等待不报错;或报错并记入延后队列",
540
- value: value.deferredMode ?? "hold",
545
+ value: value.deferredMode === "error" ? "error" : "hold",
541
546
  options: [{
542
547
  value: "hold",
543
548
  label: "挂起等待(退峰自动放行)"
@@ -615,81 +620,220 @@ window.__ModuleLoader__.load({
615
620
  });
616
621
  }
617
622
  //#endregion
618
- //#region src/client/shell-overlay.tsx
623
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/typeof.js
624
+ function _typeof(o) {
625
+ "@babel/helpers - typeof";
626
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
627
+ return typeof o;
628
+ } : function(o) {
629
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
630
+ }, _typeof(o);
631
+ }
632
+ //#endregion
633
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/toPrimitive.js
634
+ function toPrimitive(t, r) {
635
+ if ("object" != _typeof(t) || !t) return t;
636
+ var e = t[Symbol.toPrimitive];
637
+ if (void 0 !== e) {
638
+ var i = e.call(t, r || "default");
639
+ if ("object" != _typeof(i)) return i;
640
+ throw new TypeError("@@toPrimitive must return a primitive value.");
641
+ }
642
+ return ("string" === r ? String : Number)(t);
643
+ }
644
+ //#endregion
645
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/toPropertyKey.js
646
+ function toPropertyKey(t) {
647
+ var i = toPrimitive(t, "string");
648
+ return "symbol" == _typeof(i) ? i : i + "";
649
+ }
650
+ //#endregion
651
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/defineProperty.js
652
+ function _defineProperty(e, r, t) {
653
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
654
+ value: t,
655
+ enumerable: !0,
656
+ configurable: !0,
657
+ writable: !0
658
+ }) : e[r] = t, e;
659
+ }
660
+ //#endregion
661
+ //#region src/client/settings-bridge.ts
619
662
  /**
620
- * dsh-session-guard — 无设置页宿主(0.1.0/0.1.1)的兜底配置入口。
663
+ * BridgeDocHandle — 设置卡双轨数据源的桥轨(M4/T20-b)。
621
664
  *
622
- * 壳级 `shell.overlay` 席位渲染:右下角固定齿轮钮,点开固定定位浮层承载
623
- * SessionGuardCard(page 形态,默认展开)。配置存储全在 host 侧(settings
624
- * 服务),浮层与宿主设置页解耦——壳再老也不影响读写。有设置页的宿主上
625
- * 该齿轮与行配置卡/家族 tab 并存,作为全局快捷入口。
665
+ * ≤0.1.5(settingsScope fiber 永不解 / 0.1.0-0.1.1 无 client 服务)时,
666
+ * 配置卡经本句柄走自家 webServer 桥(describe/mutate)读写宿主 settings 服务。
667
+ * 与原生 SettingsScope(scope-face.ts)同构:getSnapshot/subscribe/set/unset。
668
+ * 读 = POST /api/<ns>/settings/describe;写 = POST …/mutate(ops 转发宿主服务),
669
+ * 成功即用回填描述符刷新快照并通知订阅者(无本地第二份状态——权威值始终在宿主)。
670
+ *
671
+ * 与 perm-gate src/client/bridge-scope.ts(T13b/T20-b 泛化模板)的差异:
672
+ * ① 快照形状对齐本仓 SettingsScope(status: loading/ready/unavailable + mode);
673
+ * ② describe 未就绪(settings-unavailable/ns-not-registered)时有限重试
674
+ * (宿主 settings 服务解析晚于卡挂载的窗口),重试耗尽才落 'unavailable'。
626
675
  */
627
- function ShellSettingsEntry(props) {
628
- const [open, setOpen] = (0, react.useState)(false);
629
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
630
- style: {
631
- position: "fixed",
632
- right: 16,
633
- bottom: 64,
634
- zIndex: 1e4
635
- },
636
- children: [
637
- open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
638
- onClick: () => setOpen(false),
639
- style: {
640
- position: "fixed",
641
- top: 0,
642
- left: 0,
643
- right: 0,
644
- bottom: 0,
645
- background: "rgba(0,0,0,.45)",
646
- zIndex: 1e4
647
- }
648
- }),
649
- open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
650
- style: {
651
- position: "fixed",
652
- top: "6vh",
653
- left: "50%",
654
- transform: "translateX(-50%)",
655
- width: "min(760px, 92vw)",
656
- maxHeight: "84vh",
657
- overflowY: "auto",
658
- zIndex: 10001
659
- },
660
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
661
- type: "button",
662
- onClick: () => setOpen(false),
663
- style: {
664
- position: "sticky",
665
- top: 6,
666
- float: "right",
667
- marginRight: 8,
668
- zIndex: 2
669
- },
670
- children: "✕ 关闭"
671
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionGuardCard, { scope: props.scope })]
672
- }),
673
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
674
- type: "button",
675
- title: "会话守护设置",
676
- onClick: () => setOpen((state) => !state),
677
- style: {
678
- width: 40,
679
- height: 40,
680
- borderRadius: "50%",
681
- border: "1px solid rgba(127,127,127,.4)",
682
- background: "rgba(30,30,32,.85)",
683
- color: "inherit",
684
- cursor: "pointer",
685
- fontSize: 16,
686
- boxShadow: "0 4px 16px rgba(0,0,0,.35)"
687
- },
688
- children: "🛡"
689
- })
690
- ]
691
- });
676
+ /** 与 server 侧 settingsBridgeRoutesFor(src/settings-bridge.js)必须逐字符一致。 */
677
+ function settingsBridgeRoutesFor(ns) {
678
+ return {
679
+ describe: `/api/${ns}/settings/describe`,
680
+ mutate: `/api/${ns}/settings/mutate`
681
+ };
682
+ }
683
+ /**
684
+ * ≤0.1.5 宿主 describe 的 descriptor.value 是「每字段 → {}」骨架(0.1.0/0.1.5
685
+ * 实测:value={enabled:{},…,officialProviders:{}},真值拆在 base(合并默认层)
686
+ * 与 user(用户层))——直接吞 value 会让卡拿到 officialProviders={},
687
+ * `?? []` 不兜底({} 非 nullish)→ ListRow values.join 崩,整族 tab 槽殉葬。
688
+ * 判特征:对象且每个自有值都是空 plain 对象(guard 字段是标量/数组,不会全空)。
689
+ * 命中即以 {...base, ...user} 重组有效值(guard 配置全是顶层标量/数组,浅合并即语义)。
690
+ */
691
+ function isFieldSkeleton(v) {
692
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
693
+ const values = Object.values(v);
694
+ return values.length > 0 && values.every((x) => x !== null && typeof x === "object" && !Array.isArray(x) && Object.keys(x).length === 0);
695
+ }
696
+ function plainObject(v) {
697
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? { ...v } : {};
698
+ }
699
+ /** descriptor → 卡面可用 plain 值:正常代际用 value;骨架代际用 base+user 浅合并。 */
700
+ function descriptorPlainValue(d) {
701
+ if (!isFieldSkeleton(d.value)) return d.value;
702
+ return {
703
+ ...plainObject(d.base),
704
+ ...plainObject(d.user)
705
+ };
692
706
  }
707
+ /** describe 不可就绪码:宿主 settings 未解析/未注册——值得重试的瞬态。 */
708
+ const RETRYABLE_CODES = /* @__PURE__ */ new Set(["settings-unavailable", "ns-not-registered"]);
709
+ const RETRY_DELAY_MS = 1500;
710
+ const RETRY_MAX = 4;
711
+ var BridgeDocHandle = class {
712
+ constructor(ns) {
713
+ _defineProperty(this, "snapshot", void 0);
714
+ _defineProperty(this, "listeners", /* @__PURE__ */ new Set());
715
+ _defineProperty(this, "revision", void 0);
716
+ _defineProperty(this, "retriesLeft", RETRY_MAX);
717
+ _defineProperty(this, "inflight", void 0);
718
+ _defineProperty(this, "ns", void 0);
719
+ _defineProperty(this, "routes", void 0);
720
+ this.ns = ns;
721
+ this.routes = settingsBridgeRoutesFor(ns);
722
+ this.snapshot = {
723
+ status: "loading",
724
+ value: void 0,
725
+ revision: void 0,
726
+ writable: true,
727
+ base: void 0,
728
+ user: void 0,
729
+ mode: "bridge"
730
+ };
731
+ this.refresh();
732
+ }
733
+ getSnapshot() {
734
+ return this.snapshot;
735
+ }
736
+ subscribe(listener) {
737
+ this.listeners.add(listener);
738
+ return () => {
739
+ this.listeners.delete(listener);
740
+ };
741
+ }
742
+ async set(field, value) {
743
+ return this.mutate([{
744
+ op: "set",
745
+ path: [field],
746
+ value
747
+ }]);
748
+ }
749
+ async unset(field) {
750
+ return this.mutate([{
751
+ op: "unset",
752
+ path: [field]
753
+ }]);
754
+ }
755
+ notify() {
756
+ for (const l of [...this.listeners]) l();
757
+ }
758
+ applyDescriptor(d, writable) {
759
+ this.revision = typeof d.revision === "number" ? d.revision : void 0;
760
+ this.snapshot = {
761
+ status: "ready",
762
+ value: descriptorPlainValue(d),
763
+ revision: this.revision,
764
+ writable,
765
+ base: void 0,
766
+ user: void 0,
767
+ mode: "bridge"
768
+ };
769
+ }
770
+ async request(path, body) {
771
+ const response = await fetch(path, {
772
+ method: "POST",
773
+ headers: { "content-type": "application/json" },
774
+ body: JSON.stringify(body ?? {})
775
+ });
776
+ if (!response.ok) throw new Error(`bridge ${path} HTTP ${response.status}`);
777
+ return await response.json();
778
+ }
779
+ /** describe 拉取;瞬态未就绪码在重试预算内延迟重拉,耗尽才落 unavailable。 */
780
+ refresh() {
781
+ if (this.inflight !== void 0) return this.inflight;
782
+ this.inflight = (async () => {
783
+ try {
784
+ const body = await this.request(this.routes.describe);
785
+ if (body.ok && body.value?.descriptor !== void 0) {
786
+ this.retriesLeft = RETRY_MAX;
787
+ this.applyDescriptor(body.value.descriptor, body.value.writable !== false);
788
+ } else if (body.code !== void 0 && RETRYABLE_CODES.has(body.code) && this.retriesLeft > 0) {
789
+ this.retriesLeft -= 1;
790
+ this.inflight = void 0;
791
+ setTimeout(() => {
792
+ this.refresh();
793
+ }, RETRY_DELAY_MS);
794
+ return;
795
+ } else this.snapshot = {
796
+ ...this.snapshot,
797
+ status: "unavailable",
798
+ writable: false
799
+ };
800
+ } catch {
801
+ if (this.retriesLeft > 0) {
802
+ this.retriesLeft -= 1;
803
+ this.inflight = void 0;
804
+ setTimeout(() => {
805
+ this.refresh();
806
+ }, RETRY_DELAY_MS);
807
+ return;
808
+ }
809
+ this.snapshot = {
810
+ ...this.snapshot,
811
+ status: "unavailable",
812
+ writable: false
813
+ };
814
+ }
815
+ this.inflight = void 0;
816
+ this.notify();
817
+ })();
818
+ return this.inflight;
819
+ }
820
+ async mutate(ops) {
821
+ const body = await this.request(this.routes.mutate, {
822
+ ops,
823
+ expectedRevision: this.revision
824
+ });
825
+ if (body.ok && body.value?.descriptor !== void 0) {
826
+ this.applyDescriptor(body.value.descriptor, this.snapshot.writable);
827
+ this.notify();
828
+ return true;
829
+ }
830
+ if (body.code === "settings-conflict") {
831
+ await this.refresh();
832
+ return false;
833
+ }
834
+ throw new Error(`bridge mutate failed: ${body.code ?? "unknown"} ${body.message ?? ""}`);
835
+ }
836
+ };
693
837
  //#endregion
694
838
  //#region src/client/family-section.tsx
695
839
  /**
@@ -764,8 +908,9 @@ window.__ModuleLoader__.load({
764
908
  }
765
909
  /**
766
910
  * 双代持久设置腰(移植 input-traffic registerSettingsFaces):两个 scoped
767
- * 子注入各自产出同形 scope 后回调;0.2.0 两代并存时以 configForms 先到为准
768
- * (mounted 位防双挂载,ui-slots 同 id 仲裁兜底)。
911
+ * 子注入各自产出同形 scope 后回调。M4 P0 重构后本函数**只承担数据臂**——
912
+ * 回调仅写 scopeRef 活引用(0.1.7+ 原生优先,mounted 位防双挂),卡面注册
913
+ * 已全部提到 apply 顶层(≤0.1.5 回调永不触发,注册不能关在这里)。
769
914
  */
770
915
  function registerSettingsFaces(ctx, onScope) {
771
916
  let mounted = false;
@@ -780,20 +925,32 @@ window.__ModuleLoader__.load({
780
925
  onScope(sctx.settingsScope.bind({ namespace: "session-guard" }), "settingsScope");
781
926
  });
782
927
  }
783
- /** 两张配置卡(family tab + bundle 详情页)挂载;scope 由双代腰产出。 */
784
- function mountConfigCards(ctx, scope) {
928
+ /** scope 活引用(perm-gate client/index.ts L166-257 形态):回调填、卡面现读。 */
929
+ const scopeRef = { scope: void 0 };
930
+ /** 桥轨惰性单例:≤0.1.5(settingsScope 死路 / 0.1.0-0.1.1 无服务)的数据源。 */
931
+ let bridgeScopeSingleton;
932
+ function bridgeScope() {
933
+ if (bridgeScopeSingleton === void 0) bridgeScopeSingleton = new BridgeDocHandle("session-guard");
934
+ return bridgeScopeSingleton;
935
+ }
936
+ /** 卡面现取数据源:原生句柄解到即原生,否则桥兜底(perm-gate 同式)。 */
937
+ function currentScope() {
938
+ return scopeRef.scope ?? bridgeScope();
939
+ }
940
+ /** 两张配置卡(family tab + bundle 详情页)挂载;数据经 currentScope() 现取。 */
941
+ function mountConfigCards(ctx) {
785
942
  ctx.slots.inject("dsh-family.tab", () => ctx.slots.register({
786
943
  name: "dsh-family.tab",
787
944
  id: "session-guard",
788
945
  order: 20,
789
946
  label: "会话守护门禁",
790
947
  locale: "session-guard",
791
- inject: () => ({ scope })
948
+ inject: () => ({ scope: currentScope() })
792
949
  }, SessionGuardCard));
793
950
  ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
794
951
  name: "plugins.bundle.config",
795
952
  key: "dsh-session-guard",
796
- inject: () => ({ scope })
953
+ inject: () => ({ scope: currentScope() })
797
954
  }, SessionGuardCard));
798
955
  }
799
956
  function apply(ctx) {
@@ -809,22 +966,9 @@ window.__ModuleLoader__.load({
809
966
  order: 40,
810
967
  locale: "session-guard"
811
968
  }, StatusBadge)));
969
+ safe("config-cards/family-tab", () => mountConfigCards(ctx));
812
970
  registerSettingsFaces(ctx, (scope, generation) => {
813
- safe(`config-cards/${generation}`, () => {
814
- mountConfigCards(ctx, scope);
815
- ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
816
- name: "settings.plugin.item",
817
- id: "session-guard",
818
- key: "session-guard",
819
- order: 60,
820
- locale: "session-guard",
821
- inject: () => ({ scope })
822
- }, SessionGuardCard));
823
- });
824
- safe("shell.overlay", () => ctx.slots.inject("shell.overlay", () => ctx.slots.register({
825
- name: "shell.overlay",
826
- id: "session-guard-settings"
827
- }, () => ShellSettingsEntry({ scope }))));
971
+ if (generation === "configForms") scopeRef.scope = scope;
828
972
  });
829
973
  safe("family-section-election", () => {
830
974
  console.log("[dsh-session-guard] election: registering settings.section inject (apply-time)");