dsh-session-guard 4.1.15 → 4.2.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.
- package/CHANGELOG.ja.md +29 -0
- package/CHANGELOG.ko.md +29 -0
- package/CHANGELOG.md +30 -0
- package/README.en.md +7 -0
- package/README.md +7 -0
- package/dsh.plugin.json +1 -1
- package/lib/client.js +4 -9
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +26 -16
- package/src/holiday-store.js +196 -0
- package/src/holidays.js +159 -0
- package/src/index.js +38 -6
- package/src/settings.js +5 -1
- package/src/time.js +30 -9
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.2.0 — 2026-10-17
|
|
15
|
+
|
|
16
|
+
### 追加
|
|
17
|
+
- 法定祝日対応(src/holidays.js + src/holiday-store.js)。内置 2026 確定スナップショット:39 行 = 33 休日 + 6 振替出勤日(出典 holiday-cn、国務院公告)。厳格 parse(ゼロ埋め日付、isOffDay 真偽、跨年拒否);判定字段のみ sha256 指紋(上流の文言/順序変更は偽差異を出さない)。キャッシュファイル $DSH_HOME/.dsh/session-guard/holidays/<year>.json はロード時に指紋照合、不一致は破棄して内置フォールバック + 再取得。自動更新:起動時 + 每小时 ensureFresh;対象年は北京当年、11/12 月は翌年も(年末公告はリリース無しで反映);jsDelivr 主 / raw.githubusercontent 副、10s タイムアウト;指紋変化時のみ原子的書込;24h 再取得抑制;全链路 fail-open。DSH_SESSION_GUARD_HOLIDAY_FETCH=0 で離线内置固定。
|
|
18
|
+
- 宣言式開关 holidayMode / holidayAutoFetch(どちらも既定 ON)。/session-guard/status に holidayMode + holidays[](年/出典/指紋/取得时刻/件数)を追加。
|
|
19
|
+
|
|
20
|
+
### 変更
|
|
21
|
+
- weekendMode を休息日语义へ昇格:法定祝日は终日オフピーク、振替出勤周末は通常どおりピーク判定、nextOffPeakInstant の先読み 7 -> 14 日で表対応。holidayMap は readCfg 経由で cfg に載り、shouldPause 消费链(scheduler tick / request-guard / step-gate / wiring)はシグネチャ不変で自动生效。
|
|
22
|
+
|
|
23
|
+
### 修正
|
|
24
|
+
- family claim rank 1 -> 2、冻结权重表(唯一真源 _tools/family-rank.mjs)と整合。
|
|
25
|
+
|
|
26
|
+
## 4.1.16 — 2026-10-14(マニフェスト整合/欠落していた 4.1.15 の補記)
|
|
27
|
+
|
|
28
|
+
### 修正
|
|
29
|
+
|
|
30
|
+
- `dsh.plugin.json` の版数を `package.json` と一致させた。公開済みの 4.1.15 ターボールは
|
|
31
|
+
まだ 4.1.14 と書かれていた(レジストリから実際にダウンロードして確認)。
|
|
32
|
+
- 動作変更なし。
|
|
33
|
+
- テストの構造ピンが退役済みの歯車入口をまだ要求しており、4.1.15 以来スイートが赤だった。
|
|
34
|
+
不在を断言する側へ修正した。
|
|
35
|
+
|
|
36
|
+
## 4.1.15 — 2026-10-08(補記:リリース当時この項が書かれていませんでした)
|
|
37
|
+
|
|
38
|
+
### 削除
|
|
39
|
+
|
|
40
|
+
- シェルレベルの `shell.overlay` シート注入を廃止:右下固定の歯車ボタンと浮層レイヤーを除去
|
|
41
|
+
(`src/client/shell-overlay.tsx` を削除)。設定への入口はホストの設定ページのみを頼る。
|
|
42
|
+
|
|
14
43
|
## 4.1.14 — 2026-10-08
|
|
15
44
|
|
|
16
45
|
### 削除
|
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.2.0 — 2026-10-17
|
|
15
|
+
|
|
16
|
+
### 추가
|
|
17
|
+
- 법정 공휴 인식(src/holidays.js + src/holiday-store.js). 내장 2026 확정 스냅샷: 39행 = 33 휴일 + 6 보근 근무일(출처 holiday-cn, 국무원 공고). 엄격한 파싱(제로패딩 날짜, isOffDay 불리언, 연도 초과 거부); 판정 필드만 sha256 지문(상문구/순서 변경은 오탐 없음). 캐시 파일 $DSH_HOME/.dsh/session-guard/holidays/<year>.json 은 로드 시 지문 자체 검증 — 불일치 시 폐기 후 내장 폴백 + 재조회. 자동 갱신: 시작 시 + 매시간 ensureFresh; 관련 연도 = 베이징 현재 연도, 11/12 월은 이듬해 추가(jsDelivr 주, raw.githubusercontent 보조, 10s 타임아웃); 지문 변화 시에만 원자적 tmp+rename 기록; 24h 재조회 억제; 전 구간 fail-open. DSH_SESSION_GUARD_HOLIDAY_FETCH=0 오프라인 고정.
|
|
18
|
+
- 선언식 스위치 holidayMode / holidayAutoFetch(기본 켜짐). /session-guard/status 가 holidayMode + 연도별 holidays[](출처/지문/조회시각/건수) 노출.
|
|
19
|
+
|
|
20
|
+
### 변경
|
|
21
|
+
- weekendMode 를 휴식일 의미로 승격: 법정 공휴는 종일 비봉우리, 보근 주말은 정상 봉우리 판정, nextOffPeakInstant 선탐색 7 -> 14 일, 표 인지. holidayMap 은 readCfg 로 cfg 에 실려 shouldPause 소비 체인(스케줄러/request-guard/step-gate/wiring)이 서명 변경 없이 자동 적용.
|
|
22
|
+
|
|
23
|
+
### 수정
|
|
24
|
+
- family claim rank 1 -> 2, 동결 가중 랭크 표(_tools/family-rank.mjs 단일 진원)와 정렬.
|
|
25
|
+
|
|
26
|
+
## 4.1.16 — 2026-10-14 (매니페스트 정리, 누락된 4.1.15 항목 보충)
|
|
27
|
+
|
|
28
|
+
### 수정
|
|
29
|
+
|
|
30
|
+
- `dsh.plugin.json` 버전을 `package.json` 과 일치시켰습니다. 게시된 4.1.15 tarball 은
|
|
31
|
+
여전히 4.1.14 였습니다 (레지스트리에서 직접 내려받아 확인).
|
|
32
|
+
- 동작 변경 없음.
|
|
33
|
+
- 구조 핀이 이미 폐지된 톱니 진입점을 요구해서 4.1.15 이후 테스트가 빨랬습니다.
|
|
34
|
+
부존재를 단언하는 쪽으로 고쳤습니다.
|
|
35
|
+
|
|
36
|
+
## 4.1.15 — 2026-10-08 (보충: 배포 당시에 이 항목이 기록되지 않았습니다)
|
|
37
|
+
|
|
38
|
+
### 삭제
|
|
39
|
+
|
|
40
|
+
- 셸 레벨 `shell.overlay` 슬롯 주입 폐지: 오른쪽 아래 고정 톱니 버튼과 부유 레이어 제거
|
|
41
|
+
(`src/client/shell-overlay.tsx` 삭제). 설정 진입점은 호스트 설정 페이지에만 의존합니다.
|
|
42
|
+
|
|
14
43
|
## 4.1.14 — 2026-10-08
|
|
15
44
|
|
|
16
45
|
### 제거
|
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,36 @@ 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.2.0 — 2026-10-17
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Statutory-holiday awareness (src/holidays.js + src/holiday-store.js). Builtin 2026 finalized snapshot: 39 rows = 33 rest days + 6 make-up workdays (source: holiday-cn, State Council notices). Strict parse (zero-padded dates, boolean isOffDay, cross-year rows rejected); sha256 fingerprint over the decision fields only — upstream copy/reorder edits cause no false churn. The cache file $DSH_HOME/.dsh/session-guard/holidays/<year>.json self-verifies its fingerprint at load: mismatch discards the cache (builtin fallback) and triggers a re-fetch. Auto refresh: startup check + hourly ensureFresh; relevant years = Beijing current year, Nov/Dec also add next year (the State Council publishes next year in late December, so it lands without a release); jsDelivr primary, raw.githubusercontent fallback, 10 s timeout; refetch only on fingerprint change, then atomic tmp+rename write; 24 h no-refetch budget; consistent hits only bump the check timestamp; fail-open end to end. DSH_SESSION_GUARD_HOLIDAY_FETCH=0 pins offline/builtin mode (tests ride it).
|
|
18
|
+
- Declarative settings switches holidayMode and holidayAutoFetch (both default on; auto-form on 0.1.7+). /session-guard/status now exposes holidayMode + holidays[] per year (source / fingerprint / fetchedAt / entry count).
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- weekendMode upgraded to rest-day semantics: statutory holidays (including ones landing on workdays) are off-peak all day; make-up-workday weekends enter peak gating normally; nextOffPeakInstant lookahead widened 7 -> 14 days and is table-aware. holidayMap rides cfg via readCfg, so shouldPause consumers (scheduler tick / request-guard / step-gate / wiring release timers) pick it up with zero signature churn.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Family claim rank 1 -> 2, aligned with the frozen weighted rank ladder (single source _tools/family-rank.mjs).
|
|
25
|
+
|
|
26
|
+
## 4.1.16 — 2026-10-14 (metadata alignment; backfill the missing 4.1.15 entry)
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `dsh.plugin.json` now carries the same version as `package.json`. The published 4.1.15 tarball
|
|
31
|
+
still said 4.1.14 (verified by downloading it from the registry), so the two manifests disagreed in a released artifact.
|
|
32
|
+
- No behaviour change in this release.
|
|
33
|
+
- Test suite repaired: the structural pin in `tests/settings-bridge.test.mjs` still demanded the
|
|
34
|
+
retired gear entry, so the suite had been red since 4.1.15; it now asserts the opposite (absence).
|
|
35
|
+
|
|
36
|
+
## 4.1.15 — 2026-10-08 (backfilled: this entry was never written at release time)
|
|
37
|
+
|
|
38
|
+
### Removed
|
|
39
|
+
|
|
40
|
+
- The shell-level `shell.overlay` slot injection: the fixed bottom-right gear button and its
|
|
41
|
+
positioned floating layer are gone (`src/client/shell-overlay.tsx` deleted). The settings entry
|
|
42
|
+
now relies on the host settings page alone.
|
|
43
|
+
|
|
14
44
|
## 4.1.14 — 2026-10-08
|
|
15
45
|
|
|
16
46
|
### Removed
|
package/README.en.md
CHANGED
|
@@ -344,6 +344,13 @@ npm test # node --test tests/*.test.mjs (timezone/weekend/state-machine/sessio
|
|
|
344
344
|
| `src/index.js` | Host apply (settings/routes/tick/provide service/retry + request guard wiring) |
|
|
345
345
|
| `src/client/` | Browser half (status badge + settings card + four-language dictionaries) |
|
|
346
346
|
|
|
347
|
+
|
|
348
|
+
## Statutory holidays and make-up workdays (4.2.0+)
|
|
349
|
+
|
|
350
|
+
weekendMode now means rest-day aware: statutory holidays (including ones landing on workdays) are off-peak all day; make-up-workday weekends enter the peak windows normally. The data source is the holiday-cn finalized snapshot — builtin 2026 with all 39 rows (33 rest days + 6 make-up workdays, State Council notices) — plus startup check and hourly refresh: jsDelivr primary with raw.githubusercontent fallback, 10 s timeout; the Beijing current year is fetched, and Nov/Dec also track next year (late-December notices land without a plugin release). Cache lives at $DSH_HOME/.dsh/session-guard/holidays/<year>.json, self-verified by a sha256 fingerprint over the decision fields only (upstream copy edits cause no false churn); mismatch discards the cache and falls back to builtin. Only fingerprint changes write (atomic tmp+rename); 24 h refetch budget; fail-open across the entire chain.
|
|
351
|
+
|
|
352
|
+
Settings: holidayMode (rest-day awareness, default on) and holidayAutoFetch (network refresh, default on); pin offline mode with DSH_SESSION_GUARD_HOLIDAY_FETCH=0. /session-guard/status exposes year / source / fingerprint / fetchedAt / entry count.
|
|
353
|
+
|
|
347
354
|
## License
|
|
348
355
|
|
|
349
356
|
MIT — see [LICENSE](LICENSE).
|
package/README.md
CHANGED
|
@@ -368,6 +368,13 @@ npm test # node --test tests/*.test.mjs(时区/周末/状态机/会话门/
|
|
|
368
368
|
| `src/index.js` | host apply(设置/路由/tick/提供服务/重试接线/请求守卫) |
|
|
369
369
|
| `src/client/` | 浏览器 half(**暂停会话按钮** + 状态徽标 + 设置卡片) |
|
|
370
370
|
|
|
371
|
+
|
|
372
|
+
## 法定节假日与调休(4.2.0+)
|
|
373
|
+
|
|
374
|
+
weekendMode 升级为休息日语义:法定假日(含落在工作日的)全天非峰;调休补班的周末照常走入峰窗口。数据源为 holiday-cn 定稿快照——内置 2026 全年 39 条(33 放假日 + 6 补班日,国务院公告),启动核对 + 每小时自动刷新:jsDelivr 主源、raw.githubusercontent 备源,10s 超时;按北京当年抓取,11/12 月追加次年(年底公告无需发版即自动进表);缓存落 $DSH_HOME/.dsh/session-guard/holidays/<year>.json,加载时以 sha256 指纹(只覆盖判定字段,上游改文案不引起伪变化)自校验,不符即弃用回退内置并重抓;仅指纹变化才原子写盘(tmp+rename),24 小时内不重抓;全链路 fail-open。
|
|
375
|
+
|
|
376
|
+
设置项:holidayMode(休息日识别,默认开)、holidayAutoFetch(联网自动刷新,默认开);离线可用环境变量 DSH_SESSION_GUARD_HOLIDAY_FETCH=0 钉死内置档。/session-guard/status 透出年份/来源/指纹/抓取时间/条数。
|
|
377
|
+
|
|
371
378
|
## License
|
|
372
379
|
|
|
373
380
|
MIT — 见 [LICENSE](LICENSE)。
|
package/dsh.plugin.json
CHANGED
package/lib/client.js
CHANGED
|
@@ -890,8 +890,9 @@ window.__ModuleLoader__.load({
|
|
|
890
890
|
const FAMILY_SECTION_ID = "dsh-family";
|
|
891
891
|
/** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
|
|
892
892
|
const FAMILY_CHILD_KEY = "dsh-family.tab";
|
|
893
|
-
/**
|
|
894
|
-
|
|
893
|
+
/** 让位宽限(由分级派生):TL rank 0 在 200ms 落账,本插件 rank 1 到 1100ms 才查账本——
|
|
894
|
+
* 落账时刻由各家 rank 决定(不再依赖 inject 注册顺序里谁先 fire),此延迟后足以分辨「TL 在场」。 */
|
|
895
|
+
const FAMILY_YIELD_MS = 2e3;
|
|
895
896
|
/**
|
|
896
897
|
* 客户端所需服务:slots(状态徽标)+ locale。持久设置走双代腰
|
|
897
898
|
* (registerSettingsFaces):configForms 为 0.1.7+ 独有,顶层注入会在
|
|
@@ -1000,13 +1001,7 @@ window.__ModuleLoader__.load({
|
|
|
1000
1001
|
}, GuardFamilySection);
|
|
1001
1002
|
console.log("[dsh-session-guard] family takeover registered (children declared)");
|
|
1002
1003
|
} catch (error) {
|
|
1003
|
-
console.log(`[dsh-session-guard] children
|
|
1004
|
-
try {
|
|
1005
|
-
ctx.slots.register(base, GuardFamilySection);
|
|
1006
|
-
console.log("[dsh-session-guard] family takeover registered (no children)");
|
|
1007
|
-
} catch (secondary) {
|
|
1008
|
-
console.log(`[dsh-session-guard] family takeover abandoned: ${secondary instanceof Error ? secondary.message : secondary}`);
|
|
1009
|
-
}
|
|
1004
|
+
console.log(`[dsh-session-guard] children claim lost — withdrawing (${error instanceof Error ? error.message : error})`);
|
|
1010
1005
|
}
|
|
1011
1006
|
}, FAMILY_YIELD_MS);
|
|
1012
1007
|
});
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["POLL_MS","useState","useState","useState","useSyncExternalStore","useState","createElement"],"sources":["../src/client/badge-text.ts","../src/client/styles.ts","../src/client/status-badge.tsx","../src/client/pause-button-text.ts","../src/client/pause-button.tsx","../src/client/settings-card.tsx","../src/client/settings-bridge.ts","../src/client/family-section.tsx","../src/client/index.ts"],"sourcesContent":["/**\r\n * dsh-session-guard — 状态徽标文案(纯函数,零依赖,可单测)。\r\n *\r\n * 分离出来的原因:徽标语义(高峰期「只拦官方」vs「全部暂停」)需要单测,\r\n * 而组件文件是 TSX(含 React 运行时)。这里只有字符串逻辑。\r\n */\r\n\r\n/** /session-guard/status 返回的全局阶段(组件与测试共用)。 */\r\nexport interface Status {\r\n phase: 'peak' | 'off-peak' | 'weekend'\r\n enabled: boolean\r\n weekendMode: boolean\r\n /** 官方源二维判定是否开启(开启时高峰期只拦官方源)。 */\r\n providerGuard?: boolean\r\n /** 当前被挂起的请求数(hold 模式)。 */\r\n held?: number\r\n /** error 模式记下的延后会话数。 */\r\n deferred?: number\r\n /** 当前被 step 门挂起的会话数(v0.2.0)。 */\r\n stepHeld?: number\r\n timezone: string\r\n}\r\n\r\n/** 阶段文案。 */\r\nexport const PHASE_LABELS = {\r\n peak: '高峰',\r\n 'off-peak': '谷时',\r\n weekend: '周末',\r\n} as const\r\n\r\n/** 高峰期的徽标文案:二维判定开启时区分「只拦官方」与「全部暂停」。 */\r\nexport function peakLabel(status: Pick<Status, 'phase' | 'providerGuard'>): string {\r\n if (status.phase !== 'peak') return PHASE_LABELS[status.phase]\r\n return status.providerGuard === true ? '高峰·拦官方' : '高峰·全部暂停'\r\n}\r\n\r\n/** 悬浮说明:把判定口径与当前挂起/延后数量写清楚。 */\r\nexport function badgeTitle(status: Status): string {\r\n const base = `${PHASE_LABELS[status.phase]} · ${status.timezone}${status.weekendMode ? ' · 周末模式' : ''}`\r\n if (status.phase !== 'peak') return base\r\n const mode = status.providerGuard === true ? '仅拦截 DeepSeek 官方源' : '全部会话暂停(未启用二维判定)'\r\n const held = status.held ?? 0\r\n const deferred = status.deferred ?? 0\r\n const stepHeld = status.stepHeld ?? 0\r\n const extra = held > 0 || deferred > 0 || stepHeld > 0\r\n ? ` · 挂起 ${held} · 延后 ${deferred} · step 挂起 ${stepHeld}`\r\n : ''\r\n return `${base} · ${mode}${extra}`\r\n}\r\n","/**\n * dsh-session-guard — 客户端样式(与 composer 右侧 input-traffic 冻结按钮同一视觉语言)。\n *\n * 为什么注入 `<style>` 而不是 CSS Modules:本插件的 tsdown 配置没有 CSS Modules 管线\n * (input-traffic 有),而 settings-card 已经用 `<style data-plugin-css>` 的既有约定。\n * 这里只做一件事:把「暂停会话 / 继续会话」按钮与状态徽标对齐到同一行的其它控件\n * (高度 24px、圆角 6px、12px 字号、同样的 border / hover / pressed 令牌)。\n */\n\n/** 与 `dsh-input-traffic/src/client/freeze-button.module.css` 对齐的控件外观。 */\nexport const CLIENT_CSS = `\n.sg-pause{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 8px;border:1px solid var(--dsw-alias-border-l3,rgba(0,0,0,.12));border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1;cursor:pointer;white-space:nowrap;flex:none}\n.sg-pause:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.04))}\n.sg-pause:disabled{opacity:.55;cursor:default}\n.sg-pause[aria-pressed='true']{border-color:var(--dsw-alias-state-warning-primary,#d97706);color:var(--dsw-alias-state-warning-primary,#d97706);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary,#d97706) 8%,transparent)}\n.sg-status{display:inline-flex;align-items:center;height:24px;padding:0 8px;border:1px solid var(--dsw-alias-border-l3,rgba(0,0,0,.12));border-radius:6px;font-size:12px;line-height:1;color:var(--dsw-alias-label-secondary,#6b7280);white-space:nowrap;flex:none}\n.sg-status.sg-peak{border-color:var(--dsw-alias-state-warning-primary,#d97706);color:var(--dsw-alias-state-warning-primary,#d97706)}\n.sg-status.sg-weekend{border-color:var(--dsw-alias-state-success-primary,#30a46c);color:var(--dsw-alias-state-success-primary,#30a46c)}\n`\n\n/** 注入一次(幂等;无 document 时静默跳过)。 */\nexport function injectClientCss(): void {\n if (typeof document === 'undefined') return\n if (document.querySelector('style[data-plugin-css=\"session-guard-client\"]') !== null) return\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-session-guard'\n tag.dataset.pluginCss = 'session-guard-client'\n tag.textContent = CLIENT_CSS\n document.head.appendChild(tag)\n}\n","/**\r\n * dsh-session-guard — 状态徽标(纯展示,fail-open)。\r\n *\r\n * 轮询 host 的 /session-guard/status(全局当前阶段),显示 高峰/谷时/周末;\r\n * 高峰期按二维判定区分「只拦官方」与「全部暂停」(文案见 badge-text.ts)。\r\n * 仅展示,不做任何队列/会话动作;冻结按钮由 input-traffic 经桥接管(D6/D8)。\r\n *\r\n * 自适应退避(universal-rc):视口宽度不足(< PEAK_BADGE_MIN_VIEWPORT,窄窗/\r\n * 分屏)或新空会话(sessionId 未落盘为 undefined)→ 整体退避隐藏。\r\n */\r\nimport { useEffect, useState } from 'react'\r\nimport { badgeTitle, peakLabel, type Status } from './badge-text'\r\nimport { injectClientCss } from './styles'\r\n\r\n/** slot 运行时注入的会话级 props(sessionId 由 dsh 的 SessionStandardProps 提供)。 */\r\nexport interface StatusBadgeProps {\r\n sessionId?: string\r\n}\r\n\r\nconst POLL_MS = 15_000\r\n/** 视口宽度退避阈值:低于此值隐藏徽标(窄窗/分屏下输入区放不下)。 */\r\nconst PEAK_BADGE_MIN_VIEWPORT = 960\r\n\r\n/** 状态徽标:轮询全局阶段,显示 高峰/谷时/周末(enabled 关闭或请求失败时静默隐藏)。 */\r\nexport function StatusBadge({ sessionId }: StatusBadgeProps) {\r\n const [status, setStatus] = useState<Status | null>(null)\r\n const [viewportNarrow, setViewportNarrow] = useState(\r\n () => typeof window !== 'undefined' && window.innerWidth < PEAK_BADGE_MIN_VIEWPORT,\r\n )\r\n\r\n injectClientCss()\r\n\r\n useEffect(() => {\r\n if (!sessionId) return\r\n let cancelled = false\r\n const poll = async () => {\r\n try {\r\n const res = await fetch('/session-guard/status')\r\n const body = await res.json().catch(() => null)\r\n if (!cancelled && body?.ok && body.status) setStatus(body.status)\r\n } catch {\r\n // fail-open:路由/网络不可达 → 静默,徽标隐藏。\r\n }\r\n }\r\n void poll()\r\n const timer = setInterval(poll, POLL_MS)\r\n return () => {\r\n cancelled = true\r\n clearInterval(timer)\r\n }\r\n }, [sessionId])\r\n\r\n useEffect(() => {\r\n const onResize = () => setViewportNarrow(window.innerWidth < PEAK_BADGE_MIN_VIEWPORT)\r\n window.addEventListener('resize', onResize)\r\n return () => window.removeEventListener('resize', onResize)\r\n }, [])\r\n\r\n // 视口宽度不足:退避隐藏。新空会话隐藏需会话空态信号(UseChat 契约),\r\n // 留待后续(sessionId 在新会话同样有值,不能作判定)。\r\n if (viewportNarrow) return null\r\n if (!status || !status.enabled) return null\r\n\r\n const cls = status.phase === 'peak' ? 'sg-peak' : status.phase === 'weekend' ? 'sg-weekend' : 'sg-off'\r\n return (\r\n <span\r\n className={`sg-status ${cls}`}\r\n title={badgeTitle(status)}\r\n data-sg-phase={status.phase}\r\n data-sg-provider-guard={status.providerGuard === true ? 'on' : 'off'}\r\n data-sg-step-held={String(status.stepHeld ?? 0)}\r\n >\r\n {peakLabel(status)}\r\n </span>\r\n )\r\n}\r\n","/**\n * dsh-session-guard — 暂停/继续按钮文案与状态投影(纯函数,零依赖,可单测)。\n *\n * 交互(v0.2.0 修订):\n * - 未暂停 → 「暂停会话」(**可点**:点击 = 在下一次 step 边界手动暂停该会话);\n * - 已暂停(step 门已拉 / 已请求但未到边界)→ 「继续会话」(点击 = 放行并本峰不再拦)。\n * 按钮不置灰:step 暂停虽然会自动发生,但按钮同时是**手动暂停**入口。\n */\n\n/** 按钮状态。 */\nexport interface PauseButtonState {\n /** 是否处于暂停态(step 门已挂起,或已请求暂停但还没到边界) */\n paused: boolean\n /** 挂起起始时间戳(毫秒);未知为 null */\n heldSince?: number | null\n /** 暂停是否来自用户手动请求(而非高峰自动拉门) */\n manual?: boolean\n}\n\n/** 按钮文案。 */\nexport function pauseButtonLabel(state: PauseButtonState): string {\n return state.paused === true ? '继续会话' : '暂停会话'\n}\n\n/** 悬浮说明:写清当前状态与点击后的语义。 */\nexport function pauseButtonTitle(state: PauseButtonState): string {\n if (state.paused !== true) {\n return '暂停会话:在下一次 step 的模型请求前暂停(高峰 + 官方源时会自动暂停)'\n }\n const since = typeof state.heldSince === 'number' && Number.isFinite(state.heldSince) ? new Date(state.heldSince) : null\n const at = since === null ? null : `${String(since.getHours()).padStart(2, '0')}:${String(since.getMinutes()).padStart(2, '0')}`\n const why = state.manual === true ? '手动暂停' : '高峰自动暂停'\n const base = at === null ? `已暂停(${why})` : `已暂停(${why},自 ${at})`\n return `${base} · 点击继续:放行当前 step,且本高峰内不再拦该会话`\n}\n\n/** 把 `/session-guard/state` 的响应体投影为按钮状态(形状异常 → 未暂停,fail-open)。 */\nexport function readPauseState(body: unknown): PauseButtonState {\n const b = (body ?? {}) as {\n ok?: unknown\n paused?: { step?: unknown; manual?: unknown }\n stepGate?: { since?: unknown; manual?: unknown }\n state?: { pausedStep?: unknown; stepHeldSince?: unknown; stepManual?: unknown }\n }\n if (b.ok !== true) return { paused: false, heldSince: null, manual: false }\n const manual = b.paused?.manual === true || b.stepGate?.manual === true || b.state?.stepManual === true\n const paused = b.paused?.step === true || b.state?.pausedStep === true || manual\n const raw = b.stepGate?.since ?? b.state?.stepHeldSince ?? null\n const heldSince = typeof raw === 'number' && Number.isFinite(raw) ? raw : null\n return { paused, heldSince, manual }\n}\n\n/** 把 SSE `/session-guard/events` 的 `state` 快照投影为按钮状态。 */\nexport function readStepSnapshot(snapshot: unknown): PauseButtonState {\n const s = (snapshot ?? {}) as { held?: unknown; since?: unknown; manual?: unknown }\n const manual = s.manual === true\n const paused = s.held === true || manual\n const heldSince = typeof s.since === 'number' && Number.isFinite(s.since) ? s.since : null\n return { paused, heldSince, manual }\n}\n","/**\n * dsh-session-guard — 「暂停会话 / 继续会话」按钮(`conversation.input.right`,order 20)。\n *\n * 交互(v0.2.0 修订):\n * - 未暂停 → 「暂停会话」,**可点**:点击 POST `{action:'stepPause'}`,在**下一次 step 边界**暂停\n * (不打断当前 step;step 1 也拦,不受峰谷 / provider 限制);\n * - 已暂停(高峰自动拉门 **或** 手动请求已登记)→ 「继续会话」,点击 POST `{action:'stepResume'}`;\n * - 状态更新双通道:① SSE `/session-guard/events?session=<id>` 即时推送;② 10s 轮询兜底\n * (SSE 不可用 / 断线时仍能收敛)。全部 fail-open,绝不抛错。\n *\n * 与 input-traffic 的「❄ 冻结追加 / 恢复追加」并列(order 30 在其右侧),互不取代:\n * 本按钮控 step 门,冻结按钮控回合级冻结 + 队列摘除。\n */\nimport { useEffect, useState } from 'react'\nimport {\n pauseButtonLabel,\n pauseButtonTitle,\n readPauseState,\n readStepSnapshot,\n type PauseButtonState,\n} from './pause-button-text'\nimport { injectClientCss } from './styles'\n\n/** slot 运行时注入的会话级 props。 */\nexport interface PauseButtonProps {\n sessionId?: string\n}\n\n/** SSE 不可用时的兜底轮询间隔(正常路径由事件驱动,几乎不触发)。 */\nconst POLL_MS = 10_000\n\nconst IDLE: PauseButtonState = { paused: false, heldSince: null, manual: false }\n\n/** 暂停 / 继续会话按钮。 */\nexport function PauseButton({ sessionId }: PauseButtonProps) {\n const [state, setState] = useState<PauseButtonState>(IDLE)\n const [busy, setBusy] = useState(false)\n\n injectClientCss()\n\n useEffect(() => {\n if (sessionId === undefined || sessionId === '') return\n let cancelled = false\n\n const poll = async (): Promise<void> => {\n try {\n const res = await fetch(`/session-guard/state?session=${encodeURIComponent(sessionId)}`)\n const body = await res.json().catch(() => null)\n if (cancelled) return\n setState(readPauseState(body))\n } catch {\n // fail-open:路由不存在 / 网络失败 → 保持上一次状态。\n }\n }\n void poll()\n const timer = setInterval(() => {\n void poll()\n }, POLL_MS)\n\n let source: EventSource | undefined\n try {\n source = new EventSource(`/session-guard/events?session=${encodeURIComponent(sessionId)}`)\n source.onmessage = (ev: MessageEvent<string>) => {\n try {\n const msg = JSON.parse(ev.data) as { type?: string; state?: unknown }\n if (cancelled || msg?.type !== 'step') return\n setState(readStepSnapshot(msg.state))\n } catch {\n /* 非法负载:忽略,等下一次推送或轮询 */\n }\n }\n // onerror:EventSource 会自行重连;轮询同时兜底。\n source.onerror = () => undefined\n } catch {\n // 浏览器不支持 EventSource → 只靠轮询。\n }\n\n return () => {\n cancelled = true\n clearInterval(timer)\n try {\n source?.close()\n } catch {\n /* ignore */\n }\n }\n }, [sessionId])\n\n if (sessionId === undefined || sessionId === '') return null\n\n const paused = state.paused === true\n const onClick = async (): Promise<void> => {\n if (busy) return\n setBusy(true)\n try {\n const action = paused ? 'stepResume' : 'stepPause'\n const res = await fetch('/session-guard/rpc', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sessionId, action }),\n })\n const body = await res.json().catch(() => null)\n if (body?.ok === true) {\n // 立即乐观更新;SSE / 轮询随后校正。\n setState(paused ? IDLE : { paused: true, heldSince: null, manual: true })\n }\n } catch {\n // fail-open:下一轮轮询会纠正显示。\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <button\n type=\"button\"\n className=\"sg-pause\"\n disabled={busy}\n aria-pressed={paused || undefined}\n title={pauseButtonTitle(state)}\n data-sg-step-paused={paused ? 'on' : 'off'}\n onClick={() => {\n void onClick()\n }}\n >\n {pauseButtonLabel(state)}\n </button>\n )\n}\n","/**\n * dsh-session-guard — 插件配置卡片(dsh-family.tab 共用设置节,抽屉默认展开)。\n *\n * 对齐 dsh-thinking-levels / dsh-tidychat 的卡片语法:一个可展开的 `<li>`,\n * header 按钮(插件名 + 描述 + chevron)切换字段体;开关为 pill switch\n * (track + thumb),不是复选框对勾。\n *\n * 经 `configForms.get('session-guard')` 绑定本插件 entry 的配置\n * (0.1.7 configForms 缝);每次变更立即经 scope 提交(无 staged form)。\n * 仅依赖 react;CSS 经 `<style data-plugin-css>` 注入一次,控件为原生 HTML,\n * 客户端 bundle 无 value-import @deepseek-ai/* 平台包(类型导入被构建擦除)。\n */\nimport { useEffect, useState, useSyncExternalStore } from 'react'\nimport type { JSX } from 'react'\nimport type { SettingsScope } from './scope-face.ts'\n\n/** 命名空间值形状(与 host 的 DEFAULT_SETTINGS 对应)。 */\nexport interface SessionGuardConfig {\n enabled: boolean\n offPeakAutoResume: boolean\n weekendMode: boolean\n queueFallback: boolean\n retryEnabled: boolean\n timezone: string\n pauseMode: 'safe' | 'force'\n pauseReason: 'wait' | 'stop'\n // ── step 级门控(v0.2.0)──\n stepLevelPause: boolean\n stepGateTimeoutMs: number\n // ── 官方 provider 二维判定(高峰 × 目标源)──\n providerGuard: boolean\n officialProviders: string[]\n officialBaseURLs: string[]\n deferredResume: boolean\n deferredResumeText: string\n deferredMode: 'hold' | 'error'\n deferredMaxHoldMs: number\n guardSubagents: boolean\n}\n\n/** 注入给卡片的面:绑定到 session-guard 命名空间的 settings scope。 */\nexport interface SettingsCardInjected {\n scope: SettingsScope<SessionGuardConfig>\n}\n\nexport type SettingsCardProps = SettingsCardInjected\n\n/** 卡片样式,注入一次(保持 bundle CSS-free)。 */\nconst CARD_CSS = `\n.sgCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}\n.sgCard:hover{border-color:var(--dsw-alias-label-dimmed)}\n.sgCard-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}\n.sgCardHeader{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:transparent;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}\n.sgCardHeadtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}\n.sgCardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}\n.sgCardDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}\n.sgCardChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}\n.sgCardChevron-open{transform:rotate(180deg)}\n.sgCardBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:4px 0 12px}\n.sgRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}\n.sgRow:last-child{border-bottom:0}\n.sgRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}\n.sgRowText-wide{padding-right:0}\n.sgTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}\n.sgDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}\n.sgSwitch{position:relative;width:40px;height:22px;flex:none}\n.sgSwitch>input{position:absolute;inset:0;width:100%;height:100%;opacity:0;margin:0;cursor:pointer}\n.sgSwitch>input:disabled{cursor:not-allowed}\n.sgSwitchTrack{position:absolute;inset:0;border-radius:22px;background:var(--dsw-alias-interactive-bg-hover);transition:background .16s}\n.sgSwitch>input:checked+.sgSwitchTrack{background:var(--dsw-alias-button-primary-fill)}\n.sgSwitchThumb{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .16s}\n.sgSwitch>input:checked~.sgSwitchThumb{transform:translateX(18px)}\n.sgInput{appearance:none;min-width:0;width:100%;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;margin-top:8px}\n.sgInput:disabled{opacity:.6;cursor:not-allowed}\n.sgSelect{appearance:none;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;min-width:160px;flex:none}\n.sgSelect:disabled{opacity:.6;cursor:not-allowed}\n.sgHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:6px 0 0}\n.sgReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 0}\n`\n\n/** 注入一次卡片样式。 */\nfunction injectCss(): void {\n if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=\"session-guard-card\"]') === null) {\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-session-guard'\n tag.dataset.pluginCss = 'session-guard-card'\n tag.textContent = CARD_CSS\n document.head.appendChild(tag)\n }\n}\n\n/** 一行 pill switch(滑块开关,绑定 scope)。 */\nfunction SwitchRow(props: {\n label: string\n description?: string\n checked: boolean\n disabled: boolean\n onChange: (_next: boolean) => void\n}): JSX.Element {\n const { label, description, checked, disabled, onChange } = props\n return (\n <div className=\"sgRow\">\n <div className=\"sgRowText\">\n <div className=\"sgTitle\">{label}</div>\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\n </div>\n <label className=\"sgSwitch\">\n <input\n type=\"checkbox\"\n checked={checked}\n disabled={disabled}\n onChange={(e) => onChange(e.currentTarget.checked)}\n />\n <span className=\"sgSwitchTrack\" />\n <span className=\"sgSwitchThumb\" />\n </label>\n </div>\n )\n}\n\n/** 一行文本输入(失焦提交;外部值变化时同步草稿)。 */\nfunction TextRow(props: {\n label: string\n description?: string\n value: string\n placeholder?: string\n disabled: boolean\n onCommit: (_next: string) => void\n}): JSX.Element {\n const { label, description, value, placeholder, disabled, onCommit } = props\n const [draft, setDraft] = useState(value)\n useEffect(() => {\n setDraft(value)\n }, [value])\n return (\n <div className=\"sgRow\">\n <div className=\"sgRowText sgRowText-wide\">\n <div className=\"sgTitle\">{label}</div>\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\n <input\n className=\"sgInput\"\n type=\"text\"\n value={draft}\n placeholder={placeholder}\n disabled={disabled}\n onChange={(e) => setDraft(e.currentTarget.value)}\n onBlur={() => onCommit(draft)}\n />\n </div>\n </div>\n )\n}\n\n/** 一行「逗号/换行分隔」的字符串列表(失焦提交为数组)。 */\nfunction ListRow(props: {\n label: string\n description?: string\n values: string[]\n placeholder?: string\n disabled: boolean\n onCommit: (_next: string[]) => void\n}): JSX.Element {\n const { label, description, values, placeholder, disabled, onCommit } = props\n const joined = values.join(', ')\n const [draft, setDraft] = useState(joined)\n useEffect(() => {\n setDraft(joined)\n }, [joined])\n return (\n <div className=\"sgRow\">\n <div className=\"sgRowText sgRowText-wide\">\n <div className=\"sgTitle\">{label}</div>\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\n <input\n className=\"sgInput\"\n type=\"text\"\n value={draft}\n placeholder={placeholder}\n disabled={disabled}\n onChange={(e) => setDraft(e.currentTarget.value)}\n onBlur={() => {\n const next = draft\n .split(/[,\\n]/)\n .map((s) => s.trim())\n .filter((s) => s !== '')\n onCommit(next)\n }}\n />\n </div>\n </div>\n )\n}\n\n/** 一行下拉选择。 */\nfunction SelectRow<T extends string>(props: {\n label: string\n description?: string\n value: T\n options: readonly { value: T; label: string }[]\n disabled: boolean\n onChange: (_next: T) => void\n}): JSX.Element {\n const { label, description, value, options, disabled, onChange } = props\n return (\n <div className=\"sgRow\">\n <div className=\"sgRowText\">\n <div className=\"sgTitle\">{label}</div>\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\n </div>\n <select\n className=\"sgSelect\"\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.currentTarget.value as T)}\n >\n {options.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n </div>\n )\n}\n\n/** 卡面字段消毒:宿主 describe 形态异常(≤0.1.5 骨架残留等)时槽崩会殉葬全族\n * tab——ListRow 的 values 非 nullish 非数组(如 {})会让 `?? []` 失守,逐卡硬护栏。 */\nfunction toList(v: unknown): string[] {\n return Array.isArray(v) ? v.map(String) : []\n}\n\n/** 插件配置卡片主体:可展开的 <li> + header 按钮 + pill switch 字段体。 */\nexport function SessionGuardCard({ scope }: SettingsCardProps): JSX.Element {\n const snapshot = useSyncExternalStore(\n (listener) => scope.subscribe(listener),\n () => scope.getSnapshot(),\n )\n const unavailable = snapshot.status === 'unavailable'\n const readonly = unavailable || !snapshot.writable\n const value = (snapshot.value ?? {}) as Partial<SessionGuardConfig>\n const [open, setOpen] = useState(true)\n\n injectCss()\n\n const toggle = (field: keyof SessionGuardConfig, next: boolean) => { void scope.set(field, next) }\n return (\n <li className={'sgCard' + (open ? ' sgCard-open' : '')}>\n <button\n type=\"button\"\n className=\"sgCardHeader\"\n aria-expanded={open}\n onClick={() => setOpen(!open)}\n >\n <span className=\"sgCardHeadtext\">\n <span className=\"sgCardName\">会话守护门禁</span>\n <span className=\"sgCardDesc\">高峰自动暂停运行会话,周末模式无视峰谷畅快跑</span>\n </span>\n <svg\n className={'sgCardChevron' + (open ? ' sgCardChevron-open' : '')}\n viewBox=\"0 0 14 14\"\n width={14}\n height={14}\n fill=\"none\"\n aria-hidden=\"true\"\n >\n <path d=\"M3.5 5.5L7 9l3.5-3.5\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </button>\n {open && (\n <div className=\"sgCardBody\">\n {unavailable ? (\n <p style={{ margin: '0', padding: '12px 0', fontSize: '13px', color: 'var(--dsw-alias-label-tertiary)' }}>\n 设置命名空间不可用:请确认 dsh-session-guard 已装配进此 profile。\n </p>\n ) : (\n <>\n <SwitchRow\n label=\"高峰自动暂停冻结会话\"\n description=\"高峰时段自动暂停运行会话\"\n checked={value.enabled ?? true}\n disabled={readonly}\n onChange={(next) => toggle('enabled', next)}\n />\n <SwitchRow\n label=\"step 级门控\"\n description=\"高峰在下一个 step 的模型请求前拉门(省 token 更彻底);关闭则回退为回合级暂停\"\n checked={value.stepLevelPause ?? true}\n disabled={readonly}\n onChange={(next) => toggle('stepLevelPause', next)}\n />\n <TextRow\n label=\"step 门控超时(毫秒)\"\n description=\"到期释放 step 门并升级为回合级暂停(防死锁);0 或非法值用默认 300000\"\n value={String(value.stepGateTimeoutMs ?? 300000)}\n placeholder=\"300000\"\n disabled={readonly}\n onCommit={(next) => {\n const n = Number(String(next).trim())\n void scope.set('stepGateTimeoutMs', Number.isFinite(n) && n > 0 ? n : 300000)\n }}\n />\n <SwitchRow\n label=\"官方源二维判定\"\n description=\"高峰期只拦 DeepSeek 官方源;本地/第三方 provider 照常跑\"\n checked={value.providerGuard ?? true}\n disabled={readonly}\n onChange={(next) => toggle('providerGuard', next)}\n />\n <ListRow\n label=\"追加官方 provider id\"\n description=\"精确匹配,优先级最高(逗号分隔)\"\n values={toList(value.officialProviders)}\n placeholder=\"deepseek-official\"\n disabled={readonly}\n onCommit={(next) => { void scope.set('officialProviders', next) }}\n />\n <ListRow\n label=\"官方端点名单\"\n description=\"baseURL 归一化后的 host(逗号分隔)\"\n values={toList(value.officialBaseURLs)}\n placeholder=\"api.deepseek.com\"\n disabled={readonly}\n onCommit={(next) => { void scope.set('officialBaseURLs', next) }}\n />\n <SelectRow<'hold' | 'error'>\n label=\"拦截方式\"\n description=\"挂起等待不报错;或报错并记入延后队列\"\n value={value.deferredMode === 'error' ? 'error' : 'hold'}\n options={[\n { value: 'hold', label: '挂起等待(退峰自动放行)' },\n { value: 'error', label: '报错并延后(退峰续跑)' },\n ]}\n disabled={readonly}\n onChange={(next) => { void scope.set('deferredMode', next) }}\n />\n <SwitchRow\n label=\"退峰自动继续\"\n description=\"关闭后延后的请求/会话不自动续跑,需手动 /resume\"\n checked={value.deferredResume ?? true}\n disabled={readonly}\n onChange={(next) => toggle('deferredResume', next)}\n />\n <TextRow\n label=\"退峰续跑文案\"\n description=\"error 模式退峰时发送的消息内容\"\n value={value.deferredResumeText ?? ''}\n placeholder=\"继续(高峰已过,自动继续)\"\n disabled={readonly}\n onCommit={(next) => { void scope.set('deferredResumeText', next) }}\n />\n <SwitchRow\n label=\"纳入子代理请求\"\n description=\"子代理请求同样计费,默认一并拦截\"\n checked={value.guardSubagents ?? true}\n disabled={readonly}\n onChange={(next) => toggle('guardSubagents', next)}\n />\n <SwitchRow\n label=\"低谷自动恢复\"\n description=\"低峰时段自动恢复被暂停的会话\"\n checked={value.offPeakAutoResume ?? true}\n disabled={readonly}\n onChange={(next) => toggle('offPeakAutoResume', next)}\n />\n <SwitchRow\n label=\"周末模式\"\n description=\"识别周末,无视峰谷畅快跑\"\n checked={value.weekendMode ?? true}\n disabled={readonly}\n onChange={(next) => toggle('weekendMode', next)}\n />\n <SwitchRow\n label=\"回退锁队列\"\n description=\"无会话门时锁等待队列\"\n checked={value.queueFallback ?? true}\n disabled={readonly}\n onChange={(next) => toggle('queueFallback', next)}\n />\n <SwitchRow\n label=\"自动重试\"\n description=\"后端重试,默认关(保守)\"\n checked={value.retryEnabled ?? false}\n disabled={readonly}\n onChange={(next) => toggle('retryEnabled', next)}\n />\n <p className=\"sgHint\">\n 判定口径:显式 id 名单 → baseURL 端点 → catalog 默认端点 → 内置 id。\n 排查误判访问 /session-guard/provider?provider=<id> 看 matchedBy。\n </p>\n {!snapshot.writable && <p className=\"sgReadonly\">当前只读,无法修改。</p>}\n </>\n )}\n </div>\n )}\n </li>\n )\n}\n","/**\n * BridgeDocHandle — 设置卡双轨数据源的桥轨(M4/T20-b)。\n *\n * ≤0.1.5(settingsScope fiber 永不解 / 0.1.0-0.1.1 无 client 服务)时,\n * 配置卡经本句柄走自家 webServer 桥(describe/mutate)读写宿主 settings 服务。\n * 与原生 SettingsScope(scope-face.ts)同构:getSnapshot/subscribe/set/unset。\n * 读 = POST /api/<ns>/settings/describe;写 = POST …/mutate(ops 转发宿主服务),\n * 成功即用回填描述符刷新快照并通知订阅者(无本地第二份状态——权威值始终在宿主)。\n *\n * 与 perm-gate src/client/bridge-scope.ts(T13b/T20-b 泛化模板)的差异:\n * ① 快照形状对齐本仓 SettingsScope(status: loading/ready/unavailable + mode);\n * ② describe 未就绪(settings-unavailable/ns-not-registered)时有限重试\n * (宿主 settings 服务解析晚于卡挂载的窗口),重试耗尽才落 'unavailable'。\n */\n\n/** 与 server 侧 settingsBridgeRoutesFor(src/settings-bridge.js)必须逐字符一致。 */\nfunction settingsBridgeRoutesFor(ns: string): { describe: string; mutate: string } {\n return { describe: `/api/${ns}/settings/describe`, mutate: `/api/${ns}/settings/mutate` }\n}\n\ninterface Snapshot<T> {\n status: 'loading' | 'ready' | 'unavailable'\n value: T | undefined\n revision: number | undefined\n writable: boolean\n base: unknown\n user: unknown\n mode: 'bridge'\n}\n\ninterface Descriptor {\n value?: unknown\n revision?: number\n [k: string]: unknown\n}\n\ninterface BridgeBody {\n ok?: boolean\n code?: string\n message?: string\n value?: { descriptor?: Descriptor; writable?: boolean }\n}\n\ninterface MutateOp {\n op: 'set' | 'unset'\n path: string[]\n value?: unknown\n}\n\n/**\n * ≤0.1.5 宿主 describe 的 descriptor.value 是「每字段 → {}」骨架(0.1.0/0.1.5\n * 实测:value={enabled:{},…,officialProviders:{}},真值拆在 base(合并默认层)\n * 与 user(用户层))——直接吞 value 会让卡拿到 officialProviders={},\n * `?? []` 不兜底({} 非 nullish)→ ListRow values.join 崩,整族 tab 槽殉葬。\n * 判特征:对象且每个自有值都是空 plain 对象(guard 字段是标量/数组,不会全空)。\n * 命中即以 {...base, ...user} 重组有效值(guard 配置全是顶层标量/数组,浅合并即语义)。\n */\nfunction isFieldSkeleton(v: unknown): v is Record<string, Record<string, never>> {\n if (v === null || typeof v !== 'object' || Array.isArray(v)) return false\n const values = Object.values(v as Record<string, unknown>)\n return values.length > 0\n && values.every((x) => x !== null && typeof x === 'object' && !Array.isArray(x) && Object.keys(x as object).length === 0)\n}\n\nfunction plainObject(v: unknown): Record<string, unknown> {\n return v !== null && typeof v === 'object' && !Array.isArray(v) ? { ...(v as Record<string, unknown>) } : {}\n}\n\n/** descriptor → 卡面可用 plain 值:正常代际用 value;骨架代际用 base+user 浅合并。 */\nfunction descriptorPlainValue(d: Descriptor): unknown {\n if (!isFieldSkeleton(d.value)) return d.value\n return { ...plainObject(d.base), ...plainObject(d.user) }\n}\n\n/** describe 不可就绪码:宿主 settings 未解析/未注册——值得重试的瞬态。 */\nconst RETRYABLE_CODES = new Set(['settings-unavailable', 'ns-not-registered'])\nconst RETRY_DELAY_MS = 1500\nconst RETRY_MAX = 4\n\nexport class BridgeDocHandle<T> {\n private snapshot: Snapshot<T>\n private listeners = new Set<() => void>()\n private revision: number | undefined\n private retriesLeft = RETRY_MAX\n private inflight: Promise<void> | undefined\n private readonly ns: string\n private readonly routes: { describe: string; mutate: string }\n\n constructor(ns: string) {\n this.ns = ns\n this.routes = settingsBridgeRoutesFor(ns)\n this.snapshot = {\n status: 'loading',\n value: undefined,\n revision: undefined,\n writable: true,\n base: undefined,\n user: undefined,\n mode: 'bridge',\n }\n void this.refresh()\n }\n\n getSnapshot(): Snapshot<T> {\n return this.snapshot\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n async set(field: string, value: unknown): Promise<boolean> {\n return this.mutate([{ op: 'set', path: [field], value }])\n }\n\n async unset(field: string): Promise<boolean> {\n return this.mutate([{ op: 'unset', path: [field] }])\n }\n\n private notify(): void {\n for (const l of [...this.listeners]) l()\n }\n\n private applyDescriptor(d: Descriptor, writable: boolean): void {\n this.revision = typeof d.revision === 'number' ? d.revision : undefined\n this.snapshot = {\n status: 'ready',\n value: descriptorPlainValue(d) as T | undefined,\n revision: this.revision,\n writable,\n base: undefined,\n user: undefined,\n mode: 'bridge',\n }\n }\n\n private async request(path: string, body?: unknown): Promise<BridgeBody> {\n const response = await fetch(path, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body ?? {}),\n })\n if (!response.ok) throw new Error(`bridge ${path} HTTP ${response.status}`)\n return (await response.json()) as BridgeBody\n }\n\n /** describe 拉取;瞬态未就绪码在重试预算内延迟重拉,耗尽才落 unavailable。 */\n refresh(): Promise<void> {\n if (this.inflight !== undefined) return this.inflight\n this.inflight = (async () => {\n try {\n const body = await this.request(this.routes.describe)\n if (body.ok && body.value?.descriptor !== undefined) {\n this.retriesLeft = RETRY_MAX\n this.applyDescriptor(body.value.descriptor, body.value.writable !== false)\n } else if (body.code !== undefined && RETRYABLE_CODES.has(body.code) && this.retriesLeft > 0) {\n this.retriesLeft -= 1\n this.inflight = undefined\n setTimeout(() => {\n void this.refresh()\n }, RETRY_DELAY_MS)\n return\n } else {\n this.snapshot = { ...this.snapshot, status: 'unavailable', writable: false }\n }\n } catch {\n // 网络异常:按瞬态处理,预算内重试\n if (this.retriesLeft > 0) {\n this.retriesLeft -= 1\n this.inflight = undefined\n setTimeout(() => {\n void this.refresh()\n }, RETRY_DELAY_MS)\n return\n }\n this.snapshot = { ...this.snapshot, status: 'unavailable', writable: false }\n }\n // 重试路径已在上面清过 inflight(setTimeout 后由 refresh() 重建);\n // 终态路径在此清挂起标记并广播。重试等待期 notify 无害(快照未变)。\n this.inflight = undefined\n this.notify()\n })()\n return this.inflight\n }\n\n private async mutate(ops: MutateOp[]): Promise<boolean> {\n const body = await this.request(this.routes.mutate, { ops, expectedRevision: this.revision })\n if (body.ok && body.value?.descriptor !== undefined) {\n this.applyDescriptor(body.value.descriptor, this.snapshot.writable)\n this.notify()\n return true\n }\n if (body.code === 'settings-conflict') {\n await this.refresh() // 乐观并发失败:拉权威值,让下次写入带新 revision\n return false\n }\n throw new Error(`bridge mutate failed: ${body.code ?? 'unknown'} ${body.message ?? ''}`)\n }\n}\n","/**\r\n * dsh-session-guard — 家族节接管组件(fallback host section)。\r\n *\r\n * 仅当宿主 dsh-thinking-levels 缺席、且本插件在接管选举中胜出时,由\r\n * index.ts 注册为顶级 `settings.section`(id 固定 `dsh-family`,节名仍为\r\n * 「起子插件设置」)。组件是纯通用渲染:把 `dsh-family.tab` 账本里的每张\r\n * 贡献卡(含本插件自己的,因为接管时本插件也声明了该子席位)按 order\r\n * 依次 renderSlot —— 卡片本身是默认展开的抽屉,整节即一页抽屉。\r\n *\r\n * 与 thinking-levels 的 FamilySettingsSection 的差别仅在不需要 thinking-\r\n * levels 自己的卡(宿主不在了,它的卡也不在):渲染循环没有 own-tab 特例。\r\n */\r\nimport { useEffect, useState } from 'react'\r\nimport type { JSX } from 'react'\r\n\r\n/** 与 FamilySectionInjected.hooks.tabs 同形的账本投影(结构化镜像)。 */\r\ntype HostObservable<T> = {\r\n getSnapshot(): T\r\n subscribe(listener: () => void): () => void\r\n}\r\n\r\n/** 账本条目(id/order/label,label 已解析为字符串)。 */\r\nexport interface FamilyTabEntry {\r\n id: string\r\n order: number\r\n label: string\r\n}\r\n\r\n/** 组件 props:hooks 舱(宿主 bindInjectHooks 把 inject 面的 `hooks` 键剥掉\r\n * 转成 `useTabs`——0.1.0 起各代皆然,故 useTabs 是正路、raw hooks 只是\r\n * 无转换环境的兜底)+ ≤0.1.5 壳缺 renderSlot 时的直接挂载面。 */\r\nexport interface GuardFamilySectionProps {\r\n renderSlot?: (key: 'dsh-family.tab', owner?: object, opts?: { only?: string; fallback?: JSX.Element | null }) => JSX.Element | null\r\n /** 宿主转换后的 hooks 舱(`inject.hooks.tabs` → `useTabs`)。 */\r\n useTabs?: <S>(selector: (value: readonly FamilyTabEntry[]) => S) => S\r\n /** 未被宿主转换时的原始账本投影(防御位,实测各代都会转换走 useTabs)。 */\r\n hooks?: { tabs: HostObservable<readonly FamilyTabEntry[]> }\r\n /** 直接挂载面(≤0.1.5 壳无 renderSlot 时):注册侧从账本原始条目\r\n * (entries 自带 component + inject)现挂激活卡。失败一律 null。 */\r\n renderContributor?: (id: string) => JSX.Element | null\r\n}\r\n\r\nconst NO_TABS: readonly FamilyTabEntry[] = []\r\n\r\nexport function GuardFamilySection(props: GuardFamilySectionProps): JSX.Element {\r\n const raw = props.hooks?.tabs\r\n // 账本读取:useTabs 舱优先;raw 投影走 React 订阅。face 选择每代固定,\r\n // 两种路径的 hook 数不同但同实例内次序恒定。\r\n const [polled, setPolled] = useState<readonly FamilyTabEntry[]>(() => raw?.getSnapshot() ?? NO_TABS)\r\n useEffect(() => {\r\n if (props.useTabs !== undefined || raw === undefined) return\r\n return raw.subscribe(() => setPolled(raw.getSnapshot()))\r\n })\r\n const tabs = props.useTabs !== undefined ? props.useTabs(value => value) : polled\r\n return (\r\n <div style={{ display: 'grid', gap: '12px' }}>\r\n {tabs.map(row => (\r\n <div key={row.id}>\r\n {props.renderSlot\r\n ? props.renderSlot('dsh-family.tab', {}, { only: row.id, fallback: null })\r\n : props.renderContributor?.(row.id) ?? null}\r\n </div>\r\n ))}\r\n </div>\r\n )\r\n}\r\n","/**\r\n * dsh-session-guard — 浏览器 half。\r\n *\r\n * 职责(全部 fail-open,D8):\r\n * - 在 composer 输入区右侧注册一个**纯展示**状态徽标(高峰/谷时/周末),轮询\r\n * /session-guard/status;\r\n * - **不做**冻结/会话动作——冻结按钮由 input-traffic 接管并经 /session-guard/rpc\r\n * 桥接 host 会话门;本插件客户端不注册任何按钮,避免与 input-traffic 冲突。\r\n * - 0.1.7:旧的插件设置卡已随席位删除 —— 设置表单由 host\r\n * 侧 Config 的 `.volatile()` 字段自动生成。\r\n * - 跨版本(universal-rc):持久设置经 configForms(0.1.7+)/settingsScope(≤0.1.5)\r\n * 双代腰产出同形 scope;无设置页宿主(0.1.0/0.1.1)经 shell.overlay 浮窗\r\n * 齿轮提供配置入口(配置存储全在 host 侧,浮层与宿主设置页解耦)。\r\n *\r\n * 构建:tsdown → lib/client.js(__ModuleLoader__.load 注册,与 input-traffic 同构)。\r\n */\r\nimport { createElement } from 'react'\r\nimport type { JSX } from 'react'\r\nimport { StatusBadge } from './status-badge'\r\nimport { PauseButton } from './pause-button'\r\nimport { SessionGuardCard } from './settings-card'\r\nimport { BridgeDocHandle } from './settings-bridge.ts'\r\nimport { GuardFamilySection, type FamilyTabEntry } from './family-section'\r\nimport type { SessionGuardConfig } from './settings-card'\r\nimport type { SettingsScope } from './scope-face'\r\n\r\n// 起子插件族共用设置节:宿主 dsh-thinking-levels 注册顶级 `settings.section`\r\n// (id `dsh-family`)并声明 `dsh-family.tab` 子席位,本插件贡献卡片。宿主缺席\r\n// 时本插件按下载量排名接管该节(见 apply 内的选举逻辑),节名仍为\r\n// 「起子插件设置」;宿主在场时本插件只贡献卡片。\r\n// 类型导入(构建时擦除,bundle 无 value-import 平台包):让下方 `declare\r\n// module` 增补能解析到 @deepseek-ai/dsh-client-ui-slots 的 SlotMap。\r\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\r\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\r\n interface SlotMap {\r\n 'dsh-family.tab': { kind: 'list'; scope: 'root' }\r\n /**\r\n * 插件页配置卡(官方 ui-plugin-manager 契约,key = 包名):DSH 0.2.0 的\r\n * Plugins 页不自动渲染 volatile 配置表单,bundle 详情页只渲染注册在本\r\n * keyed 席位下的卡片;owner props 由页面给(`{ view: 'page', form }`),\r\n * 本卡片改经 inject 工厂拿自己的 scope(与 dsh-family.tab 注册同源)。\r\n */\r\n 'plugins.bundle.config': { kind: 'keyed'; scope: 'root'; owner: Record<string, unknown> }\r\n }\r\n}\r\n\r\n/** 家族节固定 id(与 thinking-levels 的注册严格一致)。 */\r\nconst FAMILY_SECTION_ID = 'dsh-family'\r\n/** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */\r\nconst FAMILY_CHILD_KEY = 'dsh-family.tab'\r\n/** 让位宽限:声明批内 TL 的注册微秒级落账,此延迟后查账本足以分辨「TL 在场」。 */\r\nconst FAMILY_YIELD_MS = 500\r\n\r\n/**\r\n * 客户端所需服务:slots(状态徽标)+ locale。持久设置走双代腰\r\n * (registerSettingsFaces):configForms 为 0.1.7+ 独有,顶层注入会在\r\n * 0.1.0/0.1.1(无此服务)上让整段 apply 静默死。\r\n */\r\nexport const inject = ['slots', 'locale']\r\n\r\n/** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */\r\ninterface SlotsFace {\r\n inject: (_name: string, _fn: () => unknown) => () => void\r\n register: (_options: Record<string, unknown>, _component: unknown) => () => void\r\n entries: (_name: string) => Array<{\r\n options: { id?: string; order?: number; label?: unknown }\r\n component?: unknown\r\n inject?: () => unknown\r\n }>\r\n getVersion: (_name: string) => number\r\n subscribe: (_name: string, _listener: () => void) => () => void\r\n}\r\ninterface ConfigFormsFactoryFace {\r\n get: <T>(_entryId: string) => SettingsScope<T>\r\n}\r\ninterface SettingsScopeFactoryFace {\r\n bind: <T>(_opts: { namespace: string }) => SettingsScope<T>\r\n}\r\ninterface ClientCtx {\r\n slots: SlotsFace\r\n inject: (\r\n _names: string[],\r\n _fn: (sctx: { configForms?: ConfigFormsFactoryFace; settingsScope?: SettingsScopeFactoryFace }) => void,\r\n ) => void\r\n}\r\n\r\n/** 单面注册护栏:某条宿主线上未知槽名/缺服务的同步异常不得带走其它面。 */\r\nfunction safe(tag: string, fn: () => void): void {\r\n try {\r\n fn()\r\n } catch (error) {\r\n console.warn(`dsh-session-guard: client surface '${tag}' failed:`, error)\r\n }\r\n}\r\n\r\n/**\r\n * 双代持久设置腰(移植 input-traffic registerSettingsFaces):两个 scoped\r\n * 子注入各自产出同形 scope 后回调。M4 P0 重构后本函数**只承担数据臂**——\r\n * 回调仅写 scopeRef 活引用(0.1.7+ 原生优先,mounted 位防双挂),卡面注册\r\n * 已全部提到 apply 顶层(≤0.1.5 回调永不触发,注册不能关在这里)。\r\n */\r\nfunction registerSettingsFaces(\r\n ctx: ClientCtx,\r\n onScope: (scope: SettingsScope<SessionGuardConfig>, generation: 'configForms' | 'settingsScope') => void,\r\n): void {\r\n let mounted = false\r\n ctx.inject(['configForms'], (sctx) => {\r\n if (mounted) return\r\n mounted = true\r\n onScope(sctx.configForms!.get<SessionGuardConfig>('session-guard'), 'configForms')\r\n })\r\n ctx.inject(['settingsScope'], (sctx) => {\r\n if (mounted) return\r\n mounted = true\r\n onScope(sctx.settingsScope!.bind<SessionGuardConfig>({ namespace: 'session-guard' }), 'settingsScope')\r\n })\r\n}\r\n\r\n/** scope 活引用(perm-gate client/index.ts L166-257 形态):回调填、卡面现读。 */\r\nconst scopeRef: { scope: SettingsScope<SessionGuardConfig> | undefined } = { scope: undefined }\r\n\r\n/** 桥轨惰性单例:≤0.1.5(settingsScope 死路 / 0.1.0-0.1.1 无服务)的数据源。 */\r\nlet bridgeScopeSingleton: BridgeDocHandle<SessionGuardConfig> | undefined\r\nfunction bridgeScope(): BridgeDocHandle<SessionGuardConfig> {\r\n if (bridgeScopeSingleton === undefined) bridgeScopeSingleton = new BridgeDocHandle<SessionGuardConfig>('session-guard')\r\n return bridgeScopeSingleton\r\n}\r\n\r\n/** 卡面现取数据源:原生句柄解到即原生,否则桥兜底(perm-gate 同式)。 */\r\nfunction currentScope(): SettingsScope<SessionGuardConfig> {\r\n return scopeRef.scope ?? bridgeScope()\r\n}\r\n\r\n/** 两张配置卡(family tab + bundle 详情页)挂载;数据经 currentScope() 现取。 */\r\nfunction mountConfigCards(ctx: ClientCtx): void {\r\n // 插件族共用 tab 贡献卡:读写本插件 entry(`session-guard`)的持久配置。\r\n ctx.slots.inject('dsh-family.tab', () => ctx.slots.register({\r\n name: 'dsh-family.tab',\r\n id: 'session-guard',\r\n order: 20,\r\n label: '会话守护门禁',\r\n locale: 'session-guard',\r\n inject: () => ({ scope: currentScope() }),\r\n }, SessionGuardCard))\r\n\r\n // 插件页配置卡(DSH 0.2.0):Plugins 页不自动渲染 volatile 配置表单,须注册\r\n // `plugins.bundle.config` 席位(key = 包名),配置卡才渲染在 bundle 详情页。\r\n // 卡片组件与 props(scope)与上方 dsh-family.tab 注册完全同源。\r\n ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({\r\n name: 'plugins.bundle.config',\r\n key: 'dsh-session-guard',\r\n inject: () => ({ scope: currentScope() }),\r\n }, SessionGuardCard))\r\n}\r\n\r\nexport function apply(ctx: ClientCtx) {\r\n // 暂停会话按钮(v0.2.0):step 级门控的状态显示 + 手动解除。\r\n // order 20 < input-traffic 冻结按钮的 30 → 排在左侧,两者并列互不取代(ADR-001/002)。\r\n safe('conversation.input.right/pause', () => ctx.slots.inject('conversation.input.right', () => ctx.slots.register({\r\n name: 'conversation.input.right',\r\n id: 'session-guard-pause',\r\n order: 20,\r\n locale: 'session-guard',\r\n }, PauseButton)))\r\n\r\n // 状态徽标:输入区右侧,纯展示。\r\n safe('conversation.input.right/status', () => ctx.slots.inject('conversation.input.right', () => ctx.slots.register({\r\n name: 'conversation.input.right',\r\n id: 'session-guard-status',\r\n order: 40,\r\n locale: 'session-guard',\r\n }, StatusBadge)))\r\n\r\n // ── 配置卡三面(M4 P0 重构:注册提到 apply 顶层)────────────────────\r\n // 旧形态把注册关在 onScope 回调里——≤0.1.5 settingsScope 死路/0.1.0-0.1.1\r\n // 无服务 → 回调永不触发 → 三面永不注册、桥无消费者(perm-gate client/\r\n // index.ts L160-165 注释点名的反例)。现注册无条件执行,数据经 currentScope()\r\n // 现取:原生句柄解到即原生,否则桥兜底。\r\n safe('config-cards/family-tab', () => mountConfigCards(ctx))\r\n // 老设置页(≤0.1.5)插件列表配置卡(settings.plugin.item)已于 2026-10-08\r\n // 撤除(family-insection 第二轮):与会话守护门禁家族 tab 重复——家族 tab\r\n // 是唯一设置面(TL/free-search/browser-cdp/preset-manager 同批撤各自 item 卡)。\r\n\r\n // 双代腰 = 纯数据臂,**按代分流**(2026-10-08 深夜五格实测裁决,findings「实测裁决」节):\r\n // - configForms(0.1.7+):句柄写读皆活 → 入 scopeRef(原生优先,桥自动让位)。\r\n // - settingsScope(0.1.2/0.1.5):句柄**写活读死**——set() 落盘持久化,但\r\n // getSnapshot().value 恒 undefined(卡渲染默认值,磁盘是真值)。留空\r\n // scopeRef → currentScope() 落桥:桥 mutate 同样落宿主服务(写行为等价),\r\n // describe 读到持久化真值(读路径痊愈)。0.1.0/0.1.1 无 client 服务,\r\n // 同样落桥。\r\n registerSettingsFaces(ctx, (scope, generation) => {\r\n if (generation === 'configForms') scopeRef.scope = scope\r\n })\r\n\r\n // 家族节接管(2026-10 对齐 TL route A + bindInjectHooks 实证):让位选举。\r\n // 2026-10-08 T20-a 实证修正:≤0.1.7 壳的设置导航对同 id 异 priority 的遮蔽位\r\n // 也各列一行——「TL(p0) 恒头位、本插件 p10 恒阴影不占导航」假设不成立,双注册\r\n // = 双「起子插件设置」导航行(0.1.0~0.1.7 五格全现;0.1.0 摘除 guard 实验:\r\n // 单行归位、TL 节独座满血)。改为延迟让位:声明批内 TL 同批落账(工厂 fire\r\n // 顺序=inject 注册顺序,本插件的 inject 先注册先 fire,TL 同批紧随),延迟\r\n // FAMILY_YIELD_MS 再查账本——已有 dsh-family(TL 头位落座)→ 让位退场;仍空\r\n // (TL 缺席/落账失败)→ 接管。inject 仍必须 apply 期注册——slots 核只在「声明\r\n // 事件」触发工厂,宽限期后注册的 inject 等不到下一次声明(0.1.0 实机日志定案:\r\n // 4.1.7/4.1.8 的 grace 后注册工厂零执行);register 本就是指令式调用,移入\r\n // 定时器不改变机制。子席位声明带冲突兜底:TL 先声明 → 去 children 重试(接管\r\n // 者不重复声明);同 id 同 priority 竞争 → 双抛,放弃。register 走本工厂触发\r\n // 管道(裸注册的 entry.inject 在 ≤0.1.5 壳上不解,4.1.5「半补位」真相)。\r\n safe('family-section-election', () => {\r\n console.log('[dsh-session-guard] election: registering settings.section inject (apply-time)')\r\n ctx.slots.inject('settings.section', () => {\r\n console.log('[dsh-session-guard] election: factory fired (settings.section declared)')\r\n setTimeout(() => {\r\n if (ctx.slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) {\r\n console.log('[dsh-session-guard] election: dsh-family already seated — stand down')\r\n return\r\n }\r\n const base = {\r\n name: 'settings.section',\r\n id: FAMILY_SECTION_ID,\r\n order: 40,\r\n priority: 10,\r\n label: () => '起子插件设置',\r\n inject: () => ({\r\n hooks: makeGuardFamilyTabsHooks(ctx.slots),\r\n renderContributor,\r\n }),\r\n }\r\n try {\r\n ctx.slots.register({\r\n ...base,\r\n children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },\r\n }, GuardFamilySection)\r\n console.log('[dsh-session-guard] family takeover registered (children declared)')\r\n } catch (error) {\r\n console.log(`[dsh-session-guard] children decl conflict (${error instanceof Error ? error.message : error}); registering without children`)\r\n try {\r\n ctx.slots.register(base, GuardFamilySection)\r\n console.log('[dsh-session-guard] family takeover registered (no children)')\r\n } catch (secondary) {\r\n console.log(`[dsh-session-guard] family takeover abandoned: ${secondary instanceof Error ? secondary.message : secondary}`)\r\n }\r\n }\r\n }, FAMILY_YIELD_MS)\r\n })\r\n })\r\n}\r\n\r\n/** 直接挂载面(≤0.1.5 壳无 renderSlot 的降级):从账本原始条目现挂激活卡。\r\n * entries 每代都带 component + inject(ui-slots SlotCore.entries 原始记录);\r\n * 任何失败(条目消失/抛错 inject)降级为 null,不炸接管节。 */\r\nfunction renderContributor(id: string): JSX.Element | null {\r\n try {\r\n const entry = ctx.slots.entries(FAMILY_CHILD_KEY).find(e => e.options.id === id)\r\n if (entry === undefined || typeof entry.component !== 'function') return null\r\n const injected = typeof entry.inject === 'function' ? entry.inject() : undefined\r\n return createElement(\r\n entry.component as (props: Record<string, unknown>) => JSX.Element | null,\r\n (injected ?? {}) as Record<string, unknown>,\r\n )\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\n/** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形)。 */\r\nfunction makeGuardFamilyTabsHooks(slots: SlotsFace): {\r\n getSnapshot: () => readonly FamilyTabEntry[]\r\n subscribe: (listener: () => void) => () => void\r\n} {\r\n let version = -1\r\n let tabs: readonly FamilyTabEntry[] = []\r\n return {\r\n getSnapshot: () => {\r\n const next = slots.getVersion(FAMILY_CHILD_KEY)\r\n if (next !== version) {\r\n version = next\r\n tabs = slots.entries(FAMILY_CHILD_KEY)\r\n .map(entry => ({\r\n id: entry.options.id ?? '',\r\n order: entry.options.order ?? 0,\r\n label: typeof entry.options.label === 'function'\r\n ? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()\r\n : String(entry.options.label ?? entry.options.id ?? ''),\r\n }))\r\n .sort((a, b) => a.order - b.order)\r\n }\r\n return tabs\r\n },\r\n subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;EAwBA,MAAa,eAAe;GAC1B,MAAM;GACN,YAAY;GACZ,SAAS;EACX;;EAGA,SAAgB,UAAU,QAAyD;GACjF,IAAI,OAAO,UAAU,QAAQ,OAAO,aAAa,OAAO;GACxD,OAAO,OAAO,kBAAkB,OAAO,WAAW;EACpD;;EAGA,SAAgB,WAAW,QAAwB;GACjD,MAAM,OAAO,GAAG,aAAa,OAAO,OAAO,KAAK,OAAO,WAAW,OAAO,cAAc,YAAY;GACnG,IAAI,OAAO,UAAU,QAAQ,OAAO;GACpC,MAAM,OAAO,OAAO,kBAAkB,OAAO,qBAAqB;GAClE,MAAM,OAAO,OAAO,QAAQ;GAC5B,MAAM,WAAW,OAAO,YAAY;GACpC,MAAM,WAAW,OAAO,YAAY;GAIpC,OAAO,GAAG,KAAK,KAAK,OAHN,OAAO,KAAK,WAAW,KAAK,WAAW,IACjD,SAAS,KAAK,QAAQ,SAAS,aAAa,aAC5C;EAEN;;;;;;;;;;;;ECtCA,MAAa,aAAa;;;;;;;;;;EAW1B,SAAgB,kBAAwB;GACtC,IAAI,OAAO,aAAa,aAAa;GACrC,IAAI,SAAS,cAAc,iDAA+C,MAAM,MAAM;GACtF,MAAM,MAAM,SAAS,cAAc,OAAO;GAC1C,IAAI,QAAQ,SAAS;GACrB,IAAI,QAAQ,YAAY;GACxB,IAAI,cAAc;GAClB,SAAS,KAAK,YAAY,GAAG;EAC/B;;;;;;;;;;;;;ECVA,MAAMA,YAAU;;EAEhB,MAAM,0BAA0B;;EAGhC,SAAgB,YAAY,EAAE,aAA+B;GAC3D,MAAM,CAAC,QAAQ,cAAA,GAAaC,MAAAA,SAAAA,CAAwB,IAAI;GACxD,MAAM,CAAC,gBAAgB,sBAAA,GAAqBA,MAAAA,SAAAA,OACpC,OAAO,WAAW,eAAe,OAAO,aAAa,uBAC7D;GAEA,gBAAgB;GAEhB,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,CAAC,WAAW;IAChB,IAAI,YAAY;IAChB,MAAM,OAAO,YAAY;KACvB,IAAI;MAEF,MAAM,OAAO,OAAM,MADD,MAAM,uBAAuB,EAAA,CACxB,KAAK,CAAC,CAAC,YAAY,IAAI;MAC9C,IAAI,CAAC,aAAa,MAAM,MAAM,KAAK,QAAQ,UAAU,KAAK,MAAM;KAClE,QAAQ,CAER;IACF;IACA,KAAU;IACV,MAAM,QAAQ,YAAY,MAAMD,SAAO;IACvC,aAAa;KACX,YAAY;KACZ,cAAc,KAAK;IACrB;GACF,GAAG,CAAC,SAAS,CAAC;GAEd,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,MAAM,iBAAiB,kBAAkB,OAAO,aAAa,uBAAuB;IACpF,OAAO,iBAAiB,UAAU,QAAQ;IAC1C,aAAa,OAAO,oBAAoB,UAAU,QAAQ;GAC5D,GAAG,CAAC,CAAC;GAIL,IAAI,gBAAgB,OAAO;GAC3B,IAAI,CAAC,UAAU,CAAC,OAAO,SAAS,OAAO;GAEvC,MAAM,MAAM,OAAO,UAAU,SAAS,YAAY,OAAO,UAAU,YAAY,eAAe;GAC9F,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;IACE,WAAW,aAAa;IACxB,OAAO,WAAW,MAAM;IACxB,iBAAe,OAAO;IACtB,0BAAwB,OAAO,kBAAkB,OAAO,OAAO;IAC/D,qBAAmB,OAAO,OAAO,YAAY,CAAC;IAE7C,UAAA,UAAU,MAAM;GACb,CAAA;EAEV;;;;ECvDA,SAAgB,iBAAiB,OAAiC;GAChE,OAAO,MAAM,WAAW,OAAO,SAAS;EAC1C;;EAGA,SAAgB,iBAAiB,OAAiC;GAChE,IAAI,MAAM,WAAW,MACnB,OAAO;GAET,MAAM,QAAQ,OAAO,MAAM,cAAc,YAAY,OAAO,SAAS,MAAM,SAAS,IAAI,IAAI,KAAK,MAAM,SAAS,IAAI;GACpH,MAAM,KAAK,UAAU,OAAO,OAAO,GAAG,OAAO,MAAM,SAAS,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,GAAG,OAAO,MAAM,WAAW,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;GAC7H,MAAM,MAAM,MAAM,WAAW,OAAO,SAAS;GAE7C,OAAO,GADM,OAAO,OAAO,OAAO,IAAI,KAAK,OAAO,IAAI,KAAK,GAAG,GAC/C;EACjB;;EAGA,SAAgB,eAAe,MAAiC;GAC9D,MAAM,IAAK,QAAQ,CAAC;GAMpB,IAAI,EAAE,OAAO,MAAM,OAAO;IAAE,QAAQ;IAAO,WAAW;IAAM,QAAQ;GAAM;GAC1E,MAAM,SAAS,EAAE,QAAQ,WAAW,QAAQ,EAAE,UAAU,WAAW,QAAQ,EAAE,OAAO,eAAe;GACnG,MAAM,SAAS,EAAE,QAAQ,SAAS,QAAQ,EAAE,OAAO,eAAe,QAAQ;GAC1E,MAAM,MAAM,EAAE,UAAU,SAAS,EAAE,OAAO,iBAAiB;GAE3D,OAAO;IAAE;IAAQ,WADC,OAAO,QAAQ,YAAY,OAAO,SAAS,GAAG,IAAI,MAAM;IAC9C;GAAO;EACrC;;EAGA,SAAgB,iBAAiB,UAAqC;GACpE,MAAM,IAAK,YAAY,CAAC;GACxB,MAAM,SAAS,EAAE,WAAW;GAG5B,OAAO;IAAE,QAFM,EAAE,SAAS,QAAQ;IAEjB,WADC,OAAO,EAAE,UAAU,YAAY,OAAO,SAAS,EAAE,KAAK,IAAI,EAAE,QAAQ;IAC1D;GAAO;EACrC;;;;;;;;;;;;;;;;;EC9BA,MAAM,UAAU;EAEhB,MAAM,OAAyB;GAAE,QAAQ;GAAO,WAAW;GAAM,QAAQ;EAAM;;EAG/E,SAAgB,YAAY,EAAE,aAA+B;GAC3D,MAAM,CAAC,OAAO,aAAA,GAAYE,MAAAA,SAAAA,CAA2B,IAAI;GACzD,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,KAAK;GAEtC,gBAAgB;GAEhB,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,cAAc,KAAA,KAAa,cAAc,IAAI;IACjD,IAAI,YAAY;IAEhB,MAAM,OAAO,YAA2B;KACtC,IAAI;MAEF,MAAM,OAAO,OAAM,MADD,MAAM,gCAAgC,mBAAmB,SAAS,GAAG,EAAA,CAChE,KAAK,CAAC,CAAC,YAAY,IAAI;MAC9C,IAAI,WAAW;MACf,SAAS,eAAe,IAAI,CAAC;KAC/B,QAAQ,CAER;IACF;IACA,KAAU;IACV,MAAM,QAAQ,kBAAkB;KAC9B,KAAU;IACZ,GAAG,OAAO;IAEV,IAAI;IACJ,IAAI;KACF,SAAS,IAAI,YAAY,iCAAiC,mBAAmB,SAAS,GAAG;KACzF,OAAO,aAAa,OAA6B;MAC/C,IAAI;OACF,MAAM,MAAM,KAAK,MAAM,GAAG,IAAI;OAC9B,IAAI,aAAa,KAAK,SAAS,QAAQ;OACvC,SAAS,iBAAiB,IAAI,KAAK,CAAC;MACtC,QAAQ,CAER;KACF;KAEA,OAAO,gBAAgB,KAAA;IACzB,QAAQ,CAER;IAEA,aAAa;KACX,YAAY;KACZ,cAAc,KAAK;KACnB,IAAI;MACF,QAAQ,MAAM;KAChB,QAAQ,CAER;IACF;GACF,GAAG,CAAC,SAAS,CAAC;GAEd,IAAI,cAAc,KAAA,KAAa,cAAc,IAAI,OAAO;GAExD,MAAM,SAAS,MAAM,WAAW;GAChC,MAAM,UAAU,YAA2B;IACzC,IAAI,MAAM;IACV,QAAQ,IAAI;IACZ,IAAI;KAQF,KAAI,OADe,MALD,MAAM,sBAAsB;MAC5C,QAAQ;MACR,SAAS,EAAE,gBAAgB,mBAAmB;MAC9C,MAAM,KAAK,UAAU;OAAE;OAAW,QAJrB,SAAS,eAAe;MAII,CAAC;KAC5C,CAAC,EAAA,CACsB,KAAK,CAAC,CAAC,YAAY,IAAI,EAAA,EACpC,OAAO,MAEf,SAAS,SAAS,OAAO;MAAE,QAAQ;MAAM,WAAW;MAAM,QAAQ;KAAK,CAAC;IAE5E,QAAQ,CAER,UAAU;KACR,QAAQ,KAAK;IACf;GACF;GAEA,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;IACE,MAAK;IACL,WAAU;IACV,UAAU;IACV,gBAAc,UAAU,KAAA;IACxB,OAAO,iBAAiB,KAAK;IAC7B,uBAAqB,SAAS,OAAO;IACrC,eAAe;KACb,QAAa;IACf;IAEC,UAAA,iBAAiB,KAAK;GACjB,CAAA;EAEZ;;;;;;;;;;;;;;;;EChFA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiCjB,SAAS,YAAkB;GACzB,IAAI,OAAO,aAAa,eAAe,SAAS,cAAc,+CAA6C,MAAM,MAAM;IACrH,MAAM,MAAM,SAAS,cAAc,OAAO;IAC1C,IAAI,QAAQ,SAAS;IACrB,IAAI,QAAQ,YAAY;IACxB,IAAI,cAAc;IAClB,SAAS,KAAK,YAAY,GAAG;GAC/B;EACF;;EAGA,SAAS,UAAU,OAMH;GACd,MAAM,EAAE,OAAO,aAAa,SAAS,UAAU,aAAa;GAC5D,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAU;MAAW,UAAA;KAAW,CAAA,GACpC,gBAAgB,KAAA,KAAa,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAU;MAAU,UAAA;KAAiB,CAAA,CACrE;IACL,CAAA,GAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,SAAD;KAAO,WAAU;KAAjB,UAAA;MACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,MAAK;OACI;OACC;OACV,WAAW,MAAM,SAAS,EAAE,cAAc,OAAO;MAClD,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAM,WAAU,gBAAiB,CAAA;MACjC,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAM,WAAU,gBAAiB,CAAA;KAC5B;IACJ,CAAA,CAAA;;EAET;;EAGA,SAAS,QAAQ,OAOD;GACd,MAAM,EAAE,OAAO,aAAa,OAAO,aAAa,UAAU,aAAa;GACvE,MAAM,CAAC,OAAO,aAAA,GAAYC,MAAAA,SAAAA,CAAS,KAAK;GACxC,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,SAAS,KAAK;GAChB,GAAG,CAAC,KAAK,CAAC;GACV,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;IAAK,WAAU;IACb,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,WAAU;OAAW,UAAA;MAAW,CAAA;MACpC,gBAAgB,KAAA,KAAa,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,WAAU;OAAU,UAAA;MAAiB,CAAA;MACxE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,WAAU;OACV,MAAK;OACL,OAAO;OACM;OACH;OACV,WAAW,MAAM,SAAS,EAAE,cAAc,KAAK;OAC/C,cAAc,SAAS,KAAK;MAC7B,CAAA;KACE;;GACF,CAAA;EAET;;EAGA,SAAS,QAAQ,OAOD;GACd,MAAM,EAAE,OAAO,aAAa,QAAQ,aAAa,UAAU,aAAa;GACxE,MAAM,SAAS,OAAO,KAAK,IAAI;GAC/B,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAS,MAAM;GACzC,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,SAAS,MAAM;GACjB,GAAG,CAAC,MAAM,CAAC;GACX,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;IAAK,WAAU;IACb,UAAA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,WAAU;OAAW,UAAA;MAAW,CAAA;MACpC,gBAAgB,KAAA,KAAa,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,WAAU;OAAU,UAAA;MAAiB,CAAA;MACxE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,WAAU;OACV,MAAK;OACL,OAAO;OACM;OACH;OACV,WAAW,MAAM,SAAS,EAAE,cAAc,KAAK;OAC/C,cAAc;QACZ,MAAM,OAAO,MACV,MAAM,OAAO,CAAC,CACd,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CACpB,QAAQ,MAAM,MAAM,EAAE;QACzB,SAAS,IAAI;OACf;MACD,CAAA;KACE;;GACF,CAAA;EAET;;EAGA,SAAS,UAA4B,OAOrB;GACd,MAAM,EAAE,OAAO,aAAa,OAAO,SAAS,UAAU,aAAa;GACnE,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAU;MAAW,UAAA;KAAW,CAAA,GACpC,gBAAgB,KAAA,KAAa,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MAAK,WAAU;MAAU,UAAA;KAAiB,CAAA,CACrE;IACL,CAAA,GAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;KACE,WAAU;KACH;KACG;KACV,WAAW,MAAM,SAAS,EAAE,cAAc,KAAU;KAEnD,UAAA,QAAQ,KAAK,MACZ,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MAAsB,OAAO,EAAE;MAC5B,UAAA,EAAE;KACG,GAFK,EAAE,KAEP,CACT;IACK,CAAA,CACL;;EAET;;;EAIA,SAAS,OAAO,GAAsB;GACpC,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,IAAI,MAAM,IAAI,CAAC;EAC7C;;EAGA,SAAgB,iBAAiB,EAAE,SAAyC;GAC1E,MAAM,YAAA,GAAWC,MAAAA,qBAAAA,EACd,aAAa,MAAM,UAAU,QAAQ,SAChC,MAAM,YAAY,CAC1B;GACA,MAAM,cAAc,SAAS,WAAW;GACxC,MAAM,WAAW,eAAe,CAAC,SAAS;GAC1C,MAAM,QAAS,SAAS,SAAS,CAAC;GAClC,MAAM,CAAC,MAAM,YAAA,GAAWD,MAAAA,SAAAA,CAAS,IAAI;GAErC,UAAU;GAEV,MAAM,UAAU,OAAiC,SAAkB;IAAE,MAAW,IAAI,OAAO,IAAI;GAAE;GACjG,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,MAAD;IAAI,WAAW,YAAY,OAAO,iBAAiB;IAAnD,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;KACE,MAAK;KACL,WAAU;KACV,iBAAe;KACf,eAAe,QAAQ,CAAC,IAAI;KAJ9B,UAAA,CAME,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;MAAM,WAAU;MAAhB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;OAAM,WAAU;OAAa,UAAA;MAAY,CAAA,GACzC,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;OAAM,WAAU;OAAa,UAAA;MAA4B,CAAA,CACrD;KACN,CAAA,GAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;MACE,WAAW,mBAAmB,OAAO,wBAAwB;MAC7D,SAAQ;MACR,OAAO;MACP,QAAQ;MACR,MAAK;MACL,eAAY;MAEZ,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;OAAM,GAAE;OAAuB,QAAO;OAAe,aAAa;OAAK,eAAc;OAAQ,gBAAe;MAAS,CAAA;KAClH,CAAA,CACC;IACP,CAAA,GAAA,QACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;KAAK,WAAU;KACZ,UAAA,cACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;MAAG,OAAO;OAAE,QAAQ;OAAK,SAAS;OAAU,UAAU;OAAQ,OAAO;MAAkC;MAAG,UAAA;KAEvG,CAAA,IAEH,iBAAA,GAAA,kBAAA,KAAA,CAAA,kBAAA,UAAA,EAAA,UAAA;MACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OACE,OAAM;OACN,aAAY;OACZ,SAAS,MAAM,WAAW;OAC1B,UAAU;OACV,WAAW,SAAS,OAAO,WAAW,IAAI;MAC3C,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OACE,OAAM;OACN,aAAY;OACZ,SAAS,MAAM,kBAAkB;OACjC,UAAU;OACV,WAAW,SAAS,OAAO,kBAAkB,IAAI;MAClD,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,OAAM;OACN,aAAY;OACZ,OAAO,OAAO,MAAM,qBAAqB,GAAM;OAC/C,aAAY;OACZ,UAAU;OACV,WAAW,SAAS;QAClB,MAAM,IAAI,OAAO,OAAO,IAAI,CAAC,CAAC,KAAK,CAAC;QACpC,MAAW,IAAI,qBAAqB,OAAO,SAAS,CAAC,KAAK,IAAI,IAAI,IAAI,GAAM;OAC9E;MACD,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OACE,OAAM;OACN,aAAY;OACZ,SAAS,MAAM,iBAAiB;OAChC,UAAU;OACV,WAAW,SAAS,OAAO,iBAAiB,IAAI;MACjD,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,OAAM;OACN,aAAY;OACZ,QAAQ,OAAO,MAAM,iBAAiB;OACtC,aAAY;OACZ,UAAU;OACV,WAAW,SAAS;QAAE,MAAW,IAAI,qBAAqB,IAAI;OAAE;MACjE,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,OAAM;OACN,aAAY;OACZ,QAAQ,OAAO,MAAM,gBAAgB;OACrC,aAAY;OACZ,UAAU;OACV,WAAW,SAAS;QAAE,MAAW,IAAI,oBAAoB,IAAI;OAAE;MAChE,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OACE,OAAM;OACN,aAAY;OACZ,OAAO,MAAM,iBAAiB,UAAU,UAAU;OAClD,SAAS,CACP;QAAE,OAAO;QAAQ,OAAO;OAAe,GACvC;QAAE,OAAO;QAAS,OAAO;OAAc,CACzC;OACA,UAAU;OACV,WAAW,SAAS;QAAE,MAAW,IAAI,gBAAgB,IAAI;OAAE;MAC5D,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OACE,OAAM;OACN,aAAY;OACZ,SAAS,MAAM,kBAAkB;OACjC,UAAU;OACV,WAAW,SAAS,OAAO,kBAAkB,IAAI;MAClD,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;OACE,OAAM;OACN,aAAY;OACZ,OAAO,MAAM,sBAAsB;OACnC,aAAY;OACZ,UAAU;OACV,WAAW,SAAS;QAAE,MAAW,IAAI,sBAAsB,IAAI;OAAE;MAClE,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OACE,OAAM;OACN,aAAY;OACZ,SAAS,MAAM,kBAAkB;OACjC,UAAU;OACV,WAAW,SAAS,OAAO,kBAAkB,IAAI;MAClD,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OACE,OAAM;OACN,aAAY;OACZ,SAAS,MAAM,qBAAqB;OACpC,UAAU;OACV,WAAW,SAAS,OAAO,qBAAqB,IAAI;MACrD,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OACE,OAAM;OACN,aAAY;OACZ,SAAS,MAAM,eAAe;OAC9B,UAAU;OACV,WAAW,SAAS,OAAO,eAAe,IAAI;MAC/C,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OACE,OAAM;OACN,aAAY;OACZ,SAAS,MAAM,iBAAiB;OAChC,UAAU;OACV,WAAW,SAAS,OAAO,iBAAiB,IAAI;MACjD,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;OACE,OAAM;OACN,aAAY;OACZ,SAAS,MAAM,gBAAgB;OAC/B,UAAU;OACV,WAAW,SAAS,OAAO,gBAAgB,IAAI;MAChD,CAAA;MACD,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;OAAG,WAAU;OAAS,UAAA;MAGnB,CAAA;MACF,CAAC,SAAS,YAAY,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;OAAG,WAAU;OAAa,UAAA;MAAa,CAAA;KAC9D,EAAA,CAAA;IAED,CAAA,CAEL;;EAER;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC5XA,SAAS,wBAAwB,IAAkD;GACjF,OAAO;IAAE,UAAU,QAAQ,GAAG;IAAqB,QAAQ,QAAQ,GAAG;GAAkB;EAC1F;;;;;;;;;EAuCA,SAAS,gBAAgB,GAAwD;GAC/E,IAAI,MAAM,QAAQ,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,GAAG,OAAO;GACpE,MAAM,SAAS,OAAO,OAAO,CAA4B;GACzD,OAAO,OAAO,SAAS,KAClB,OAAO,OAAO,MAAM,MAAM,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC,KAAK,OAAO,KAAK,CAAW,CAAC,CAAC,WAAW,CAAC;EAC5H;EAEA,SAAS,YAAY,GAAqC;GACxD,OAAO,MAAM,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,GAAI,EAA8B,IAAI,CAAC;EAC7G;;EAGA,SAAS,qBAAqB,GAAwB;GACpD,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,OAAO,EAAE;GACxC,OAAO;IAAE,GAAG,YAAY,EAAE,IAAI;IAAG,GAAG,YAAY,EAAE,IAAI;GAAE;EAC1D;;EAGA,MAAM,kCAAkB,IAAI,IAAI,CAAC,wBAAwB,mBAAmB,CAAC;EAC7E,MAAM,iBAAiB;EACvB,MAAM,YAAY;EAElB,IAAa,kBAAb,MAAgC;GAS9B,YAAY,IAAY;IARhB,gBAAA,MAAA,YAAA,KAAA,CAAA;IACA,gBAAA,MAAA,6BAAY,IAAI,IAAgB,CAAA;IAChC,gBAAA,MAAA,YAAA,KAAA,CAAA;IACA,gBAAA,MAAA,eAAc,SAAA;IACd,gBAAA,MAAA,YAAA,KAAA,CAAA;IACS,gBAAA,MAAA,MAAA,KAAA,CAAA;IACA,gBAAA,MAAA,UAAA,KAAA,CAAA;IAGf,KAAK,KAAK;IACV,KAAK,SAAS,wBAAwB,EAAE;IACxC,KAAK,WAAW;KACd,QAAQ;KACR,OAAO,KAAA;KACP,UAAU,KAAA;KACV,UAAU;KACV,MAAM,KAAA;KACN,MAAM,KAAA;KACN,MAAM;IACR;IACA,KAAU,QAAQ;GACpB;GAEA,cAA2B;IACzB,OAAO,KAAK;GACd;GAEA,UAAU,UAAkC;IAC1C,KAAK,UAAU,IAAI,QAAQ;IAC3B,aAAa;KACX,KAAK,UAAU,OAAO,QAAQ;IAChC;GACF;GAEA,MAAM,IAAI,OAAe,OAAkC;IACzD,OAAO,KAAK,OAAO,CAAC;KAAE,IAAI;KAAO,MAAM,CAAC,KAAK;KAAG;IAAM,CAAC,CAAC;GAC1D;GAEA,MAAM,MAAM,OAAiC;IAC3C,OAAO,KAAK,OAAO,CAAC;KAAE,IAAI;KAAS,MAAM,CAAC,KAAK;IAAE,CAAC,CAAC;GACrD;GAEA,SAAuB;IACrB,KAAK,MAAM,KAAK,CAAC,GAAG,KAAK,SAAS,GAAG,EAAE;GACzC;GAEA,gBAAwB,GAAe,UAAyB;IAC9D,KAAK,WAAW,OAAO,EAAE,aAAa,WAAW,EAAE,WAAW,KAAA;IAC9D,KAAK,WAAW;KACd,QAAQ;KACR,OAAO,qBAAqB,CAAC;KAC7B,UAAU,KAAK;KACf;KACA,MAAM,KAAA;KACN,MAAM,KAAA;KACN,MAAM;IACR;GACF;GAEA,MAAc,QAAQ,MAAc,MAAqC;IACvE,MAAM,WAAW,MAAM,MAAM,MAAM;KACjC,QAAQ;KACR,SAAS,EAAE,gBAAgB,mBAAmB;KAC9C,MAAM,KAAK,UAAU,QAAQ,CAAC,CAAC;IACjC,CAAC;IACD,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,UAAU,KAAK,QAAQ,SAAS,QAAQ;IAC1E,OAAQ,MAAM,SAAS,KAAK;GAC9B;;GAGA,UAAyB;IACvB,IAAI,KAAK,aAAa,KAAA,GAAW,OAAO,KAAK;IAC7C,KAAK,YAAY,YAAY;KAC3B,IAAI;MACF,MAAM,OAAO,MAAM,KAAK,QAAQ,KAAK,OAAO,QAAQ;MACpD,IAAI,KAAK,MAAM,KAAK,OAAO,eAAe,KAAA,GAAW;OACnD,KAAK,cAAc;OACnB,KAAK,gBAAgB,KAAK,MAAM,YAAY,KAAK,MAAM,aAAa,KAAK;MAC3E,OAAO,IAAI,KAAK,SAAS,KAAA,KAAa,gBAAgB,IAAI,KAAK,IAAI,KAAK,KAAK,cAAc,GAAG;OAC5F,KAAK,eAAe;OACpB,KAAK,WAAW,KAAA;OAChB,iBAAiB;QACf,KAAU,QAAQ;OACpB,GAAG,cAAc;OACjB;MACF,OACE,KAAK,WAAW;OAAE,GAAG,KAAK;OAAU,QAAQ;OAAe,UAAU;MAAM;KAE/E,QAAQ;MAEN,IAAI,KAAK,cAAc,GAAG;OACxB,KAAK,eAAe;OACpB,KAAK,WAAW,KAAA;OAChB,iBAAiB;QACf,KAAU,QAAQ;OACpB,GAAG,cAAc;OACjB;MACF;MACA,KAAK,WAAW;OAAE,GAAG,KAAK;OAAU,QAAQ;OAAe,UAAU;MAAM;KAC7E;KAGA,KAAK,WAAW,KAAA;KAChB,KAAK,OAAO;IACd,EAAA,CAAG;IACH,OAAO,KAAK;GACd;GAEA,MAAc,OAAO,KAAmC;IACtD,MAAM,OAAO,MAAM,KAAK,QAAQ,KAAK,OAAO,QAAQ;KAAE;KAAK,kBAAkB,KAAK;IAAS,CAAC;IAC5F,IAAI,KAAK,MAAM,KAAK,OAAO,eAAe,KAAA,GAAW;KACnD,KAAK,gBAAgB,KAAK,MAAM,YAAY,KAAK,SAAS,QAAQ;KAClE,KAAK,OAAO;KACZ,OAAO;IACT;IACA,IAAI,KAAK,SAAS,qBAAqB;KACrC,MAAM,KAAK,QAAQ;KACnB,OAAO;IACT;IACA,MAAM,IAAI,MAAM,yBAAyB,KAAK,QAAQ,UAAU,GAAG,KAAK,WAAW,IAAI;GACzF;EACF;;;;;;;;;;;;;;;EC/JA,MAAM,UAAqC,CAAC;EAE5C,SAAgB,mBAAmB,OAA6C;GAC9E,MAAM,MAAM,MAAM,OAAO;GAGzB,MAAM,CAAC,QAAQ,cAAA,GAAaE,MAAAA,SAAAA,OAA0C,KAAK,YAAY,KAAK,OAAO;GACnG,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,MAAM,YAAY,KAAA,KAAa,QAAQ,KAAA,GAAW;IACtD,OAAO,IAAI,gBAAgB,UAAU,IAAI,YAAY,CAAC,CAAC;GACzD,CAAC;GACD,MAAM,OAAO,MAAM,YAAY,KAAA,IAAY,MAAM,SAAQ,UAAS,KAAK,IAAI;GAC3E,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;IAAK,OAAO;KAAE,SAAS;KAAQ,KAAK;IAAO;IACxC,UAAA,KAAK,KAAI,QACR,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD,EAAA,UACG,MAAM,aACH,MAAM,WAAW,kBAAkB,CAAC,GAAG;KAAE,MAAM,IAAI;KAAI,UAAU;IAAK,CAAC,IACvE,MAAM,oBAAoB,IAAI,EAAE,KAAK,KACtC,GAJK,IAAI,EAIT,CACN;GACE,CAAA;EAET;;;;;;;;;;;;;;;;;;;;EClBA,MAAM,oBAAoB;;EAE1B,MAAM,mBAAmB;;EAEzB,MAAM,kBAAkB;;;;;;EAOxB,MAAa,SAAS,CAAC,SAAS,QAAQ;;EA6BxC,SAAS,KAAK,KAAa,IAAsB;GAC/C,IAAI;IACF,GAAG;GACL,SAAS,OAAO;IACd,QAAQ,KAAK,sCAAsC,IAAI,YAAY,KAAK;GAC1E;EACF;;;;;;;EAQA,SAAS,sBACP,KACA,SACM;GACN,IAAI,UAAU;GACd,IAAI,OAAO,CAAC,aAAa,IAAI,SAAS;IACpC,IAAI,SAAS;IACb,UAAU;IACV,QAAQ,KAAK,YAAa,IAAwB,eAAe,GAAG,aAAa;GACnF,CAAC;GACD,IAAI,OAAO,CAAC,eAAe,IAAI,SAAS;IACtC,IAAI,SAAS;IACb,UAAU;IACV,QAAQ,KAAK,cAAe,KAAyB,EAAE,WAAW,gBAAgB,CAAC,GAAG,eAAe;GACvG,CAAC;EACH;;EAGA,MAAM,WAAqE,EAAE,OAAO,KAAA,EAAU;;EAG9F,IAAI;EACJ,SAAS,cAAmD;GAC1D,IAAI,yBAAyB,KAAA,GAAW,uBAAuB,IAAI,gBAAoC,eAAe;GACtH,OAAO;EACT;;EAGA,SAAS,eAAkD;GACzD,OAAO,SAAS,SAAS,YAAY;EACvC;;EAGA,SAAS,iBAAiB,KAAsB;GAE9C,IAAI,MAAM,OAAO,wBAAwB,IAAI,MAAM,SAAS;IAC1D,MAAM;IACN,IAAI;IACJ,OAAO;IACP,OAAO;IACP,QAAQ;IACR,eAAe,EAAE,OAAO,aAAa,EAAE;GACzC,GAAG,gBAAgB,CAAC;GAKpB,IAAI,MAAM,OAAO,+BAA+B,IAAI,MAAM,SAAS;IACjE,MAAM;IACN,KAAK;IACL,eAAe,EAAE,OAAO,aAAa,EAAE;GACzC,GAAG,gBAAgB,CAAC;EACtB;EAEA,SAAgB,MAAM,KAAgB;GAGpC,KAAK,wCAAwC,IAAI,MAAM,OAAO,kCAAkC,IAAI,MAAM,SAAS;IACjH,MAAM;IACN,IAAI;IACJ,OAAO;IACP,QAAQ;GACV,GAAG,WAAW,CAAC,CAAC;GAGhB,KAAK,yCAAyC,IAAI,MAAM,OAAO,kCAAkC,IAAI,MAAM,SAAS;IAClH,MAAM;IACN,IAAI;IACJ,OAAO;IACP,QAAQ;GACV,GAAG,WAAW,CAAC,CAAC;GAOhB,KAAK,iCAAiC,iBAAiB,GAAG,CAAC;GAY3D,sBAAsB,MAAM,OAAO,eAAe;IAChD,IAAI,eAAe,eAAe,SAAS,QAAQ;GACrD,CAAC;GAeD,KAAK,iCAAiC;IACpC,QAAQ,IAAI,gFAAgF;IAC5F,IAAI,MAAM,OAAO,0BAA0B;KACzC,QAAQ,IAAI,yEAAyE;KACrF,iBAAiB;MACf,IAAI,IAAI,MAAM,QAAQ,kBAAkB,CAAC,CAAC,MAAK,MAAK,EAAE,QAAQ,OAAO,iBAAiB,GAAG;OACvF,QAAQ,IAAI,sEAAsE;OAClF;MACF;MACA,MAAM,OAAO;OACX,MAAM;OACN,IAAI;OACJ,OAAO;OACP,UAAU;OACV,aAAa;OACb,eAAe;QACb,OAAO,yBAAyB,IAAI,KAAK;QACzC;OACF;MACF;MACA,IAAI;OACF,IAAI,MAAM,SAAS;QACjB,GAAG;QACH,UAAU,GAAG,mBAAmB;SAAE,MAAM;SAAQ,OAAO;QAAO,EAAE;OAClE,GAAG,kBAAkB;OACrB,QAAQ,IAAI,oEAAoE;MAClF,SAAS,OAAO;OACd,QAAQ,IAAI,+CAA+C,iBAAiB,QAAQ,MAAM,UAAU,MAAM,gCAAgC;OAC1I,IAAI;QACF,IAAI,MAAM,SAAS,MAAM,kBAAkB;QAC3C,QAAQ,IAAI,8DAA8D;OAC5E,SAAS,WAAW;QAClB,QAAQ,IAAI,kDAAkD,qBAAqB,QAAQ,UAAU,UAAU,WAAW;OAC5H;MACF;KACF,GAAG,eAAe;IACpB,CAAC;GACH,CAAC;EACH;;;;EAKA,SAAS,kBAAkB,IAAgC;GACzD,IAAI;IACF,MAAM,QAAQ,IAAI,MAAM,QAAQ,gBAAgB,CAAC,CAAC,MAAK,MAAK,EAAE,QAAQ,OAAO,EAAE;IAC/E,IAAI,UAAU,KAAA,KAAa,OAAO,MAAM,cAAc,YAAY,OAAO;IACzE,MAAM,WAAW,OAAO,MAAM,WAAW,aAAa,MAAM,OAAO,IAAI,KAAA;IACvE,QAAA,GAAOC,MAAAA,cAAAA,CACL,MAAM,WACL,YAAY,CAAC,CAChB;GACF,QAAQ;IACN,OAAO;GACT;EACF;;EAGA,SAAS,yBAAyB,OAGhC;GACA,IAAI,UAAU;GACd,IAAI,OAAkC,CAAC;GACvC,OAAO;IACL,mBAAmB;KACjB,MAAM,OAAO,MAAM,WAAW,gBAAgB;KAC9C,IAAI,SAAS,SAAS;MACpB,UAAU;MACV,OAAO,MAAM,QAAQ,gBAAgB,CAAC,CACnC,KAAI,WAAU;OACb,IAAI,MAAM,QAAQ,MAAM;OACxB,OAAO,MAAM,QAAQ,SAAS;OAC9B,OAAO,OAAO,MAAM,QAAQ,UAAU,oBAC3B;QAAE,IAAI;SAAE,OAAO,OAAQ,MAAM,QAAQ,MAAwB,KAAK,MAAM,QAAQ,MAAM,EAAE;QAAE,QAAQ;SAAE,OAAO,OAAO,MAAM,QAAQ,MAAM,EAAE;QAAE;OAAE,EAAA,CAAG,IACtJ,OAAO,MAAM,QAAQ,SAAS,MAAM,QAAQ,MAAM,EAAE;MAC1D,EAAE,CAAC,CACF,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;KACrC;KACA,OAAO;IACT;IACA,YAAY,aAAyB,MAAM,UAAU,kBAAkB,QAAQ;GACjF;EACF"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["POLL_MS","useState","useState","useState","useSyncExternalStore","useState","createElement"],"sources":["../src/client/badge-text.ts","../src/client/styles.ts","../src/client/status-badge.tsx","../src/client/pause-button-text.ts","../src/client/pause-button.tsx","../src/client/settings-card.tsx","../src/client/settings-bridge.ts","../src/client/family-section.tsx","../src/client/index.ts"],"sourcesContent":["/**\r\n * dsh-session-guard — 状态徽标文案(纯函数,零依赖,可单测)。\r\n *\r\n * 分离出来的原因:徽标语义(高峰期「只拦官方」vs「全部暂停」)需要单测,\r\n * 而组件文件是 TSX(含 React 运行时)。这里只有字符串逻辑。\r\n */\r\n\r\n/** /session-guard/status 返回的全局阶段(组件与测试共用)。 */\r\nexport interface Status {\r\n phase: 'peak' | 'off-peak' | 'weekend'\r\n enabled: boolean\r\n weekendMode: boolean\r\n /** 官方源二维判定是否开启(开启时高峰期只拦官方源)。 */\r\n providerGuard?: boolean\r\n /** 当前被挂起的请求数(hold 模式)。 */\r\n held?: number\r\n /** error 模式记下的延后会话数。 */\r\n deferred?: number\r\n /** 当前被 step 门挂起的会话数(v0.2.0)。 */\r\n stepHeld?: number\r\n timezone: string\r\n}\r\n\r\n/** 阶段文案。 */\r\nexport const PHASE_LABELS = {\r\n peak: '高峰',\r\n 'off-peak': '谷时',\r\n weekend: '周末',\r\n} as const\r\n\r\n/** 高峰期的徽标文案:二维判定开启时区分「只拦官方」与「全部暂停」。 */\r\nexport function peakLabel(status: Pick<Status, 'phase' | 'providerGuard'>): string {\r\n if (status.phase !== 'peak') return PHASE_LABELS[status.phase]\r\n return status.providerGuard === true ? '高峰·拦官方' : '高峰·全部暂停'\r\n}\r\n\r\n/** 悬浮说明:把判定口径与当前挂起/延后数量写清楚。 */\r\nexport function badgeTitle(status: Status): string {\r\n const base = `${PHASE_LABELS[status.phase]} · ${status.timezone}${status.weekendMode ? ' · 周末模式' : ''}`\r\n if (status.phase !== 'peak') return base\r\n const mode = status.providerGuard === true ? '仅拦截 DeepSeek 官方源' : '全部会话暂停(未启用二维判定)'\r\n const held = status.held ?? 0\r\n const deferred = status.deferred ?? 0\r\n const stepHeld = status.stepHeld ?? 0\r\n const extra = held > 0 || deferred > 0 || stepHeld > 0\r\n ? ` · 挂起 ${held} · 延后 ${deferred} · step 挂起 ${stepHeld}`\r\n : ''\r\n return `${base} · ${mode}${extra}`\r\n}\r\n","/**\n * dsh-session-guard — 客户端样式(与 composer 右侧 input-traffic 冻结按钮同一视觉语言)。\n *\n * 为什么注入 `<style>` 而不是 CSS Modules:本插件的 tsdown 配置没有 CSS Modules 管线\n * (input-traffic 有),而 settings-card 已经用 `<style data-plugin-css>` 的既有约定。\n * 这里只做一件事:把「暂停会话 / 继续会话」按钮与状态徽标对齐到同一行的其它控件\n * (高度 24px、圆角 6px、12px 字号、同样的 border / hover / pressed 令牌)。\n */\n\n/** 与 `dsh-input-traffic/src/client/freeze-button.module.css` 对齐的控件外观。 */\nexport const CLIENT_CSS = `\n.sg-pause{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 8px;border:1px solid var(--dsw-alias-border-l3,rgba(0,0,0,.12));border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:1;cursor:pointer;white-space:nowrap;flex:none}\n.sg-pause:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.04))}\n.sg-pause:disabled{opacity:.55;cursor:default}\n.sg-pause[aria-pressed='true']{border-color:var(--dsw-alias-state-warning-primary,#d97706);color:var(--dsw-alias-state-warning-primary,#d97706);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary,#d97706) 8%,transparent)}\n.sg-status{display:inline-flex;align-items:center;height:24px;padding:0 8px;border:1px solid var(--dsw-alias-border-l3,rgba(0,0,0,.12));border-radius:6px;font-size:12px;line-height:1;color:var(--dsw-alias-label-secondary,#6b7280);white-space:nowrap;flex:none}\n.sg-status.sg-peak{border-color:var(--dsw-alias-state-warning-primary,#d97706);color:var(--dsw-alias-state-warning-primary,#d97706)}\n.sg-status.sg-weekend{border-color:var(--dsw-alias-state-success-primary,#30a46c);color:var(--dsw-alias-state-success-primary,#30a46c)}\n`\n\n/** 注入一次(幂等;无 document 时静默跳过)。 */\nexport function injectClientCss(): void {\n if (typeof document === 'undefined') return\n if (document.querySelector('style[data-plugin-css=\"session-guard-client\"]') !== null) return\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-session-guard'\n tag.dataset.pluginCss = 'session-guard-client'\n tag.textContent = CLIENT_CSS\n document.head.appendChild(tag)\n}\n","/**\r\n * dsh-session-guard — 状态徽标(纯展示,fail-open)。\r\n *\r\n * 轮询 host 的 /session-guard/status(全局当前阶段),显示 高峰/谷时/周末;\r\n * 高峰期按二维判定区分「只拦官方」与「全部暂停」(文案见 badge-text.ts)。\r\n * 仅展示,不做任何队列/会话动作;冻结按钮由 input-traffic 经桥接管(D6/D8)。\r\n *\r\n * 自适应退避(universal-rc):视口宽度不足(< PEAK_BADGE_MIN_VIEWPORT,窄窗/\r\n * 分屏)或新空会话(sessionId 未落盘为 undefined)→ 整体退避隐藏。\r\n */\r\nimport { useEffect, useState } from 'react'\r\nimport { badgeTitle, peakLabel, type Status } from './badge-text'\r\nimport { injectClientCss } from './styles'\r\n\r\n/** slot 运行时注入的会话级 props(sessionId 由 dsh 的 SessionStandardProps 提供)。 */\r\nexport interface StatusBadgeProps {\r\n sessionId?: string\r\n}\r\n\r\nconst POLL_MS = 15_000\r\n/** 视口宽度退避阈值:低于此值隐藏徽标(窄窗/分屏下输入区放不下)。 */\r\nconst PEAK_BADGE_MIN_VIEWPORT = 960\r\n\r\n/** 状态徽标:轮询全局阶段,显示 高峰/谷时/周末(enabled 关闭或请求失败时静默隐藏)。 */\r\nexport function StatusBadge({ sessionId }: StatusBadgeProps) {\r\n const [status, setStatus] = useState<Status | null>(null)\r\n const [viewportNarrow, setViewportNarrow] = useState(\r\n () => typeof window !== 'undefined' && window.innerWidth < PEAK_BADGE_MIN_VIEWPORT,\r\n )\r\n\r\n injectClientCss()\r\n\r\n useEffect(() => {\r\n if (!sessionId) return\r\n let cancelled = false\r\n const poll = async () => {\r\n try {\r\n const res = await fetch('/session-guard/status')\r\n const body = await res.json().catch(() => null)\r\n if (!cancelled && body?.ok && body.status) setStatus(body.status)\r\n } catch {\r\n // fail-open:路由/网络不可达 → 静默,徽标隐藏。\r\n }\r\n }\r\n void poll()\r\n const timer = setInterval(poll, POLL_MS)\r\n return () => {\r\n cancelled = true\r\n clearInterval(timer)\r\n }\r\n }, [sessionId])\r\n\r\n useEffect(() => {\r\n const onResize = () => setViewportNarrow(window.innerWidth < PEAK_BADGE_MIN_VIEWPORT)\r\n window.addEventListener('resize', onResize)\r\n return () => window.removeEventListener('resize', onResize)\r\n }, [])\r\n\r\n // 视口宽度不足:退避隐藏。新空会话隐藏需会话空态信号(UseChat 契约),\r\n // 留待后续(sessionId 在新会话同样有值,不能作判定)。\r\n if (viewportNarrow) return null\r\n if (!status || !status.enabled) return null\r\n\r\n const cls = status.phase === 'peak' ? 'sg-peak' : status.phase === 'weekend' ? 'sg-weekend' : 'sg-off'\r\n return (\r\n <span\r\n className={`sg-status ${cls}`}\r\n title={badgeTitle(status)}\r\n data-sg-phase={status.phase}\r\n data-sg-provider-guard={status.providerGuard === true ? 'on' : 'off'}\r\n data-sg-step-held={String(status.stepHeld ?? 0)}\r\n >\r\n {peakLabel(status)}\r\n </span>\r\n )\r\n}\r\n","/**\n * dsh-session-guard — 暂停/继续按钮文案与状态投影(纯函数,零依赖,可单测)。\n *\n * 交互(v0.2.0 修订):\n * - 未暂停 → 「暂停会话」(**可点**:点击 = 在下一次 step 边界手动暂停该会话);\n * - 已暂停(step 门已拉 / 已请求但未到边界)→ 「继续会话」(点击 = 放行并本峰不再拦)。\n * 按钮不置灰:step 暂停虽然会自动发生,但按钮同时是**手动暂停**入口。\n */\n\n/** 按钮状态。 */\nexport interface PauseButtonState {\n /** 是否处于暂停态(step 门已挂起,或已请求暂停但还没到边界) */\n paused: boolean\n /** 挂起起始时间戳(毫秒);未知为 null */\n heldSince?: number | null\n /** 暂停是否来自用户手动请求(而非高峰自动拉门) */\n manual?: boolean\n}\n\n/** 按钮文案。 */\nexport function pauseButtonLabel(state: PauseButtonState): string {\n return state.paused === true ? '继续会话' : '暂停会话'\n}\n\n/** 悬浮说明:写清当前状态与点击后的语义。 */\nexport function pauseButtonTitle(state: PauseButtonState): string {\n if (state.paused !== true) {\n return '暂停会话:在下一次 step 的模型请求前暂停(高峰 + 官方源时会自动暂停)'\n }\n const since = typeof state.heldSince === 'number' && Number.isFinite(state.heldSince) ? new Date(state.heldSince) : null\n const at = since === null ? null : `${String(since.getHours()).padStart(2, '0')}:${String(since.getMinutes()).padStart(2, '0')}`\n const why = state.manual === true ? '手动暂停' : '高峰自动暂停'\n const base = at === null ? `已暂停(${why})` : `已暂停(${why},自 ${at})`\n return `${base} · 点击继续:放行当前 step,且本高峰内不再拦该会话`\n}\n\n/** 把 `/session-guard/state` 的响应体投影为按钮状态(形状异常 → 未暂停,fail-open)。 */\nexport function readPauseState(body: unknown): PauseButtonState {\n const b = (body ?? {}) as {\n ok?: unknown\n paused?: { step?: unknown; manual?: unknown }\n stepGate?: { since?: unknown; manual?: unknown }\n state?: { pausedStep?: unknown; stepHeldSince?: unknown; stepManual?: unknown }\n }\n if (b.ok !== true) return { paused: false, heldSince: null, manual: false }\n const manual = b.paused?.manual === true || b.stepGate?.manual === true || b.state?.stepManual === true\n const paused = b.paused?.step === true || b.state?.pausedStep === true || manual\n const raw = b.stepGate?.since ?? b.state?.stepHeldSince ?? null\n const heldSince = typeof raw === 'number' && Number.isFinite(raw) ? raw : null\n return { paused, heldSince, manual }\n}\n\n/** 把 SSE `/session-guard/events` 的 `state` 快照投影为按钮状态。 */\nexport function readStepSnapshot(snapshot: unknown): PauseButtonState {\n const s = (snapshot ?? {}) as { held?: unknown; since?: unknown; manual?: unknown }\n const manual = s.manual === true\n const paused = s.held === true || manual\n const heldSince = typeof s.since === 'number' && Number.isFinite(s.since) ? s.since : null\n return { paused, heldSince, manual }\n}\n","/**\n * dsh-session-guard — 「暂停会话 / 继续会话」按钮(`conversation.input.right`,order 20)。\n *\n * 交互(v0.2.0 修订):\n * - 未暂停 → 「暂停会话」,**可点**:点击 POST `{action:'stepPause'}`,在**下一次 step 边界**暂停\n * (不打断当前 step;step 1 也拦,不受峰谷 / provider 限制);\n * - 已暂停(高峰自动拉门 **或** 手动请求已登记)→ 「继续会话」,点击 POST `{action:'stepResume'}`;\n * - 状态更新双通道:① SSE `/session-guard/events?session=<id>` 即时推送;② 10s 轮询兜底\n * (SSE 不可用 / 断线时仍能收敛)。全部 fail-open,绝不抛错。\n *\n * 与 input-traffic 的「❄ 冻结追加 / 恢复追加」并列(order 30 在其右侧),互不取代:\n * 本按钮控 step 门,冻结按钮控回合级冻结 + 队列摘除。\n */\nimport { useEffect, useState } from 'react'\nimport {\n pauseButtonLabel,\n pauseButtonTitle,\n readPauseState,\n readStepSnapshot,\n type PauseButtonState,\n} from './pause-button-text'\nimport { injectClientCss } from './styles'\n\n/** slot 运行时注入的会话级 props。 */\nexport interface PauseButtonProps {\n sessionId?: string\n}\n\n/** SSE 不可用时的兜底轮询间隔(正常路径由事件驱动,几乎不触发)。 */\nconst POLL_MS = 10_000\n\nconst IDLE: PauseButtonState = { paused: false, heldSince: null, manual: false }\n\n/** 暂停 / 继续会话按钮。 */\nexport function PauseButton({ sessionId }: PauseButtonProps) {\n const [state, setState] = useState<PauseButtonState>(IDLE)\n const [busy, setBusy] = useState(false)\n\n injectClientCss()\n\n useEffect(() => {\n if (sessionId === undefined || sessionId === '') return\n let cancelled = false\n\n const poll = async (): Promise<void> => {\n try {\n const res = await fetch(`/session-guard/state?session=${encodeURIComponent(sessionId)}`)\n const body = await res.json().catch(() => null)\n if (cancelled) return\n setState(readPauseState(body))\n } catch {\n // fail-open:路由不存在 / 网络失败 → 保持上一次状态。\n }\n }\n void poll()\n const timer = setInterval(() => {\n void poll()\n }, POLL_MS)\n\n let source: EventSource | undefined\n try {\n source = new EventSource(`/session-guard/events?session=${encodeURIComponent(sessionId)}`)\n source.onmessage = (ev: MessageEvent<string>) => {\n try {\n const msg = JSON.parse(ev.data) as { type?: string; state?: unknown }\n if (cancelled || msg?.type !== 'step') return\n setState(readStepSnapshot(msg.state))\n } catch {\n /* 非法负载:忽略,等下一次推送或轮询 */\n }\n }\n // onerror:EventSource 会自行重连;轮询同时兜底。\n source.onerror = () => undefined\n } catch {\n // 浏览器不支持 EventSource → 只靠轮询。\n }\n\n return () => {\n cancelled = true\n clearInterval(timer)\n try {\n source?.close()\n } catch {\n /* ignore */\n }\n }\n }, [sessionId])\n\n if (sessionId === undefined || sessionId === '') return null\n\n const paused = state.paused === true\n const onClick = async (): Promise<void> => {\n if (busy) return\n setBusy(true)\n try {\n const action = paused ? 'stepResume' : 'stepPause'\n const res = await fetch('/session-guard/rpc', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sessionId, action }),\n })\n const body = await res.json().catch(() => null)\n if (body?.ok === true) {\n // 立即乐观更新;SSE / 轮询随后校正。\n setState(paused ? IDLE : { paused: true, heldSince: null, manual: true })\n }\n } catch {\n // fail-open:下一轮轮询会纠正显示。\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <button\n type=\"button\"\n className=\"sg-pause\"\n disabled={busy}\n aria-pressed={paused || undefined}\n title={pauseButtonTitle(state)}\n data-sg-step-paused={paused ? 'on' : 'off'}\n onClick={() => {\n void onClick()\n }}\n >\n {pauseButtonLabel(state)}\n </button>\n )\n}\n","/**\n * dsh-session-guard — 插件配置卡片(dsh-family.tab 共用设置节,抽屉默认展开)。\n *\n * 对齐 dsh-thinking-levels / dsh-tidychat 的卡片语法:一个可展开的 `<li>`,\n * header 按钮(插件名 + 描述 + chevron)切换字段体;开关为 pill switch\n * (track + thumb),不是复选框对勾。\n *\n * 经 `configForms.get('session-guard')` 绑定本插件 entry 的配置\n * (0.1.7 configForms 缝);每次变更立即经 scope 提交(无 staged form)。\n * 仅依赖 react;CSS 经 `<style data-plugin-css>` 注入一次,控件为原生 HTML,\n * 客户端 bundle 无 value-import @deepseek-ai/* 平台包(类型导入被构建擦除)。\n */\nimport { useEffect, useState, useSyncExternalStore } from 'react'\nimport type { JSX } from 'react'\nimport type { SettingsScope } from './scope-face.ts'\n\n/** 命名空间值形状(与 host 的 DEFAULT_SETTINGS 对应)。 */\nexport interface SessionGuardConfig {\n enabled: boolean\n offPeakAutoResume: boolean\n weekendMode: boolean\n queueFallback: boolean\n retryEnabled: boolean\n timezone: string\n pauseMode: 'safe' | 'force'\n pauseReason: 'wait' | 'stop'\n // ── step 级门控(v0.2.0)──\n stepLevelPause: boolean\n stepGateTimeoutMs: number\n // ── 官方 provider 二维判定(高峰 × 目标源)──\n providerGuard: boolean\n officialProviders: string[]\n officialBaseURLs: string[]\n deferredResume: boolean\n deferredResumeText: string\n deferredMode: 'hold' | 'error'\n deferredMaxHoldMs: number\n guardSubagents: boolean\n}\n\n/** 注入给卡片的面:绑定到 session-guard 命名空间的 settings scope。 */\nexport interface SettingsCardInjected {\n scope: SettingsScope<SessionGuardConfig>\n}\n\nexport type SettingsCardProps = SettingsCardInjected\n\n/** 卡片样式,注入一次(保持 bundle CSS-free)。 */\nconst CARD_CSS = `\n.sgCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}\n.sgCard:hover{border-color:var(--dsw-alias-label-dimmed)}\n.sgCard-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}\n.sgCardHeader{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:transparent;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}\n.sgCardHeadtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}\n.sgCardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}\n.sgCardDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}\n.sgCardChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}\n.sgCardChevron-open{transform:rotate(180deg)}\n.sgCardBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:4px 0 12px}\n.sgRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}\n.sgRow:last-child{border-bottom:0}\n.sgRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}\n.sgRowText-wide{padding-right:0}\n.sgTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}\n.sgDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}\n.sgSwitch{position:relative;width:40px;height:22px;flex:none}\n.sgSwitch>input{position:absolute;inset:0;width:100%;height:100%;opacity:0;margin:0;cursor:pointer}\n.sgSwitch>input:disabled{cursor:not-allowed}\n.sgSwitchTrack{position:absolute;inset:0;border-radius:22px;background:var(--dsw-alias-interactive-bg-hover);transition:background .16s}\n.sgSwitch>input:checked+.sgSwitchTrack{background:var(--dsw-alias-button-primary-fill)}\n.sgSwitchThumb{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .16s}\n.sgSwitch>input:checked~.sgSwitchThumb{transform:translateX(18px)}\n.sgInput{appearance:none;min-width:0;width:100%;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;margin-top:8px}\n.sgInput:disabled{opacity:.6;cursor:not-allowed}\n.sgSelect{appearance:none;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;min-width:160px;flex:none}\n.sgSelect:disabled{opacity:.6;cursor:not-allowed}\n.sgHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:6px 0 0}\n.sgReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 0}\n`\n\n/** 注入一次卡片样式。 */\nfunction injectCss(): void {\n if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=\"session-guard-card\"]') === null) {\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-session-guard'\n tag.dataset.pluginCss = 'session-guard-card'\n tag.textContent = CARD_CSS\n document.head.appendChild(tag)\n }\n}\n\n/** 一行 pill switch(滑块开关,绑定 scope)。 */\nfunction SwitchRow(props: {\n label: string\n description?: string\n checked: boolean\n disabled: boolean\n onChange: (_next: boolean) => void\n}): JSX.Element {\n const { label, description, checked, disabled, onChange } = props\n return (\n <div className=\"sgRow\">\n <div className=\"sgRowText\">\n <div className=\"sgTitle\">{label}</div>\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\n </div>\n <label className=\"sgSwitch\">\n <input\n type=\"checkbox\"\n checked={checked}\n disabled={disabled}\n onChange={(e) => onChange(e.currentTarget.checked)}\n />\n <span className=\"sgSwitchTrack\" />\n <span className=\"sgSwitchThumb\" />\n </label>\n </div>\n )\n}\n\n/** 一行文本输入(失焦提交;外部值变化时同步草稿)。 */\nfunction TextRow(props: {\n label: string\n description?: string\n value: string\n placeholder?: string\n disabled: boolean\n onCommit: (_next: string) => void\n}): JSX.Element {\n const { label, description, value, placeholder, disabled, onCommit } = props\n const [draft, setDraft] = useState(value)\n useEffect(() => {\n setDraft(value)\n }, [value])\n return (\n <div className=\"sgRow\">\n <div className=\"sgRowText sgRowText-wide\">\n <div className=\"sgTitle\">{label}</div>\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\n <input\n className=\"sgInput\"\n type=\"text\"\n value={draft}\n placeholder={placeholder}\n disabled={disabled}\n onChange={(e) => setDraft(e.currentTarget.value)}\n onBlur={() => onCommit(draft)}\n />\n </div>\n </div>\n )\n}\n\n/** 一行「逗号/换行分隔」的字符串列表(失焦提交为数组)。 */\nfunction ListRow(props: {\n label: string\n description?: string\n values: string[]\n placeholder?: string\n disabled: boolean\n onCommit: (_next: string[]) => void\n}): JSX.Element {\n const { label, description, values, placeholder, disabled, onCommit } = props\n const joined = values.join(', ')\n const [draft, setDraft] = useState(joined)\n useEffect(() => {\n setDraft(joined)\n }, [joined])\n return (\n <div className=\"sgRow\">\n <div className=\"sgRowText sgRowText-wide\">\n <div className=\"sgTitle\">{label}</div>\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\n <input\n className=\"sgInput\"\n type=\"text\"\n value={draft}\n placeholder={placeholder}\n disabled={disabled}\n onChange={(e) => setDraft(e.currentTarget.value)}\n onBlur={() => {\n const next = draft\n .split(/[,\\n]/)\n .map((s) => s.trim())\n .filter((s) => s !== '')\n onCommit(next)\n }}\n />\n </div>\n </div>\n )\n}\n\n/** 一行下拉选择。 */\nfunction SelectRow<T extends string>(props: {\n label: string\n description?: string\n value: T\n options: readonly { value: T; label: string }[]\n disabled: boolean\n onChange: (_next: T) => void\n}): JSX.Element {\n const { label, description, value, options, disabled, onChange } = props\n return (\n <div className=\"sgRow\">\n <div className=\"sgRowText\">\n <div className=\"sgTitle\">{label}</div>\n {description !== undefined && <div className=\"sgDesc\">{description}</div>}\n </div>\n <select\n className=\"sgSelect\"\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.currentTarget.value as T)}\n >\n {options.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n </div>\n )\n}\n\n/** 卡面字段消毒:宿主 describe 形态异常(≤0.1.5 骨架残留等)时槽崩会殉葬全族\n * tab——ListRow 的 values 非 nullish 非数组(如 {})会让 `?? []` 失守,逐卡硬护栏。 */\nfunction toList(v: unknown): string[] {\n return Array.isArray(v) ? v.map(String) : []\n}\n\n/** 插件配置卡片主体:可展开的 <li> + header 按钮 + pill switch 字段体。 */\nexport function SessionGuardCard({ scope }: SettingsCardProps): JSX.Element {\n const snapshot = useSyncExternalStore(\n (listener) => scope.subscribe(listener),\n () => scope.getSnapshot(),\n )\n const unavailable = snapshot.status === 'unavailable'\n const readonly = unavailable || !snapshot.writable\n const value = (snapshot.value ?? {}) as Partial<SessionGuardConfig>\n const [open, setOpen] = useState(true)\n\n injectCss()\n\n const toggle = (field: keyof SessionGuardConfig, next: boolean) => { void scope.set(field, next) }\n return (\n <li className={'sgCard' + (open ? ' sgCard-open' : '')}>\n <button\n type=\"button\"\n className=\"sgCardHeader\"\n aria-expanded={open}\n onClick={() => setOpen(!open)}\n >\n <span className=\"sgCardHeadtext\">\n <span className=\"sgCardName\">会话守护门禁</span>\n <span className=\"sgCardDesc\">高峰自动暂停运行会话,周末模式无视峰谷畅快跑</span>\n </span>\n <svg\n className={'sgCardChevron' + (open ? ' sgCardChevron-open' : '')}\n viewBox=\"0 0 14 14\"\n width={14}\n height={14}\n fill=\"none\"\n aria-hidden=\"true\"\n >\n <path d=\"M3.5 5.5L7 9l3.5-3.5\" stroke=\"currentColor\" strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </button>\n {open && (\n <div className=\"sgCardBody\">\n {unavailable ? (\n <p style={{ margin: '0', padding: '12px 0', fontSize: '13px', color: 'var(--dsw-alias-label-tertiary)' }}>\n 设置命名空间不可用:请确认 dsh-session-guard 已装配进此 profile。\n </p>\n ) : (\n <>\n <SwitchRow\n label=\"高峰自动暂停冻结会话\"\n description=\"高峰时段自动暂停运行会话\"\n checked={value.enabled ?? true}\n disabled={readonly}\n onChange={(next) => toggle('enabled', next)}\n />\n <SwitchRow\n label=\"step 级门控\"\n description=\"高峰在下一个 step 的模型请求前拉门(省 token 更彻底);关闭则回退为回合级暂停\"\n checked={value.stepLevelPause ?? true}\n disabled={readonly}\n onChange={(next) => toggle('stepLevelPause', next)}\n />\n <TextRow\n label=\"step 门控超时(毫秒)\"\n description=\"到期释放 step 门并升级为回合级暂停(防死锁);0 或非法值用默认 300000\"\n value={String(value.stepGateTimeoutMs ?? 300000)}\n placeholder=\"300000\"\n disabled={readonly}\n onCommit={(next) => {\n const n = Number(String(next).trim())\n void scope.set('stepGateTimeoutMs', Number.isFinite(n) && n > 0 ? n : 300000)\n }}\n />\n <SwitchRow\n label=\"官方源二维判定\"\n description=\"高峰期只拦 DeepSeek 官方源;本地/第三方 provider 照常跑\"\n checked={value.providerGuard ?? true}\n disabled={readonly}\n onChange={(next) => toggle('providerGuard', next)}\n />\n <ListRow\n label=\"追加官方 provider id\"\n description=\"精确匹配,优先级最高(逗号分隔)\"\n values={toList(value.officialProviders)}\n placeholder=\"deepseek-official\"\n disabled={readonly}\n onCommit={(next) => { void scope.set('officialProviders', next) }}\n />\n <ListRow\n label=\"官方端点名单\"\n description=\"baseURL 归一化后的 host(逗号分隔)\"\n values={toList(value.officialBaseURLs)}\n placeholder=\"api.deepseek.com\"\n disabled={readonly}\n onCommit={(next) => { void scope.set('officialBaseURLs', next) }}\n />\n <SelectRow<'hold' | 'error'>\n label=\"拦截方式\"\n description=\"挂起等待不报错;或报错并记入延后队列\"\n value={value.deferredMode === 'error' ? 'error' : 'hold'}\n options={[\n { value: 'hold', label: '挂起等待(退峰自动放行)' },\n { value: 'error', label: '报错并延后(退峰续跑)' },\n ]}\n disabled={readonly}\n onChange={(next) => { void scope.set('deferredMode', next) }}\n />\n <SwitchRow\n label=\"退峰自动继续\"\n description=\"关闭后延后的请求/会话不自动续跑,需手动 /resume\"\n checked={value.deferredResume ?? true}\n disabled={readonly}\n onChange={(next) => toggle('deferredResume', next)}\n />\n <TextRow\n label=\"退峰续跑文案\"\n description=\"error 模式退峰时发送的消息内容\"\n value={value.deferredResumeText ?? ''}\n placeholder=\"继续(高峰已过,自动继续)\"\n disabled={readonly}\n onCommit={(next) => { void scope.set('deferredResumeText', next) }}\n />\n <SwitchRow\n label=\"纳入子代理请求\"\n description=\"子代理请求同样计费,默认一并拦截\"\n checked={value.guardSubagents ?? true}\n disabled={readonly}\n onChange={(next) => toggle('guardSubagents', next)}\n />\n <SwitchRow\n label=\"低谷自动恢复\"\n description=\"低峰时段自动恢复被暂停的会话\"\n checked={value.offPeakAutoResume ?? true}\n disabled={readonly}\n onChange={(next) => toggle('offPeakAutoResume', next)}\n />\n <SwitchRow\n label=\"周末模式\"\n description=\"识别周末,无视峰谷畅快跑\"\n checked={value.weekendMode ?? true}\n disabled={readonly}\n onChange={(next) => toggle('weekendMode', next)}\n />\n <SwitchRow\n label=\"回退锁队列\"\n description=\"无会话门时锁等待队列\"\n checked={value.queueFallback ?? true}\n disabled={readonly}\n onChange={(next) => toggle('queueFallback', next)}\n />\n <SwitchRow\n label=\"自动重试\"\n description=\"后端重试,默认关(保守)\"\n checked={value.retryEnabled ?? false}\n disabled={readonly}\n onChange={(next) => toggle('retryEnabled', next)}\n />\n <p className=\"sgHint\">\n 判定口径:显式 id 名单 → baseURL 端点 → catalog 默认端点 → 内置 id。\n 排查误判访问 /session-guard/provider?provider=<id> 看 matchedBy。\n </p>\n {!snapshot.writable && <p className=\"sgReadonly\">当前只读,无法修改。</p>}\n </>\n )}\n </div>\n )}\n </li>\n )\n}\n","/**\n * BridgeDocHandle — 设置卡双轨数据源的桥轨(M4/T20-b)。\n *\n * ≤0.1.5(settingsScope fiber 永不解 / 0.1.0-0.1.1 无 client 服务)时,\n * 配置卡经本句柄走自家 webServer 桥(describe/mutate)读写宿主 settings 服务。\n * 与原生 SettingsScope(scope-face.ts)同构:getSnapshot/subscribe/set/unset。\n * 读 = POST /api/<ns>/settings/describe;写 = POST …/mutate(ops 转发宿主服务),\n * 成功即用回填描述符刷新快照并通知订阅者(无本地第二份状态——权威值始终在宿主)。\n *\n * 与 perm-gate src/client/bridge-scope.ts(T13b/T20-b 泛化模板)的差异:\n * ① 快照形状对齐本仓 SettingsScope(status: loading/ready/unavailable + mode);\n * ② describe 未就绪(settings-unavailable/ns-not-registered)时有限重试\n * (宿主 settings 服务解析晚于卡挂载的窗口),重试耗尽才落 'unavailable'。\n */\n\n/** 与 server 侧 settingsBridgeRoutesFor(src/settings-bridge.js)必须逐字符一致。 */\nfunction settingsBridgeRoutesFor(ns: string): { describe: string; mutate: string } {\n return { describe: `/api/${ns}/settings/describe`, mutate: `/api/${ns}/settings/mutate` }\n}\n\ninterface Snapshot<T> {\n status: 'loading' | 'ready' | 'unavailable'\n value: T | undefined\n revision: number | undefined\n writable: boolean\n base: unknown\n user: unknown\n mode: 'bridge'\n}\n\ninterface Descriptor {\n value?: unknown\n revision?: number\n [k: string]: unknown\n}\n\ninterface BridgeBody {\n ok?: boolean\n code?: string\n message?: string\n value?: { descriptor?: Descriptor; writable?: boolean }\n}\n\ninterface MutateOp {\n op: 'set' | 'unset'\n path: string[]\n value?: unknown\n}\n\n/**\n * ≤0.1.5 宿主 describe 的 descriptor.value 是「每字段 → {}」骨架(0.1.0/0.1.5\n * 实测:value={enabled:{},…,officialProviders:{}},真值拆在 base(合并默认层)\n * 与 user(用户层))——直接吞 value 会让卡拿到 officialProviders={},\n * `?? []` 不兜底({} 非 nullish)→ ListRow values.join 崩,整族 tab 槽殉葬。\n * 判特征:对象且每个自有值都是空 plain 对象(guard 字段是标量/数组,不会全空)。\n * 命中即以 {...base, ...user} 重组有效值(guard 配置全是顶层标量/数组,浅合并即语义)。\n */\nfunction isFieldSkeleton(v: unknown): v is Record<string, Record<string, never>> {\n if (v === null || typeof v !== 'object' || Array.isArray(v)) return false\n const values = Object.values(v as Record<string, unknown>)\n return values.length > 0\n && values.every((x) => x !== null && typeof x === 'object' && !Array.isArray(x) && Object.keys(x as object).length === 0)\n}\n\nfunction plainObject(v: unknown): Record<string, unknown> {\n return v !== null && typeof v === 'object' && !Array.isArray(v) ? { ...(v as Record<string, unknown>) } : {}\n}\n\n/** descriptor → 卡面可用 plain 值:正常代际用 value;骨架代际用 base+user 浅合并。 */\nfunction descriptorPlainValue(d: Descriptor): unknown {\n if (!isFieldSkeleton(d.value)) return d.value\n return { ...plainObject(d.base), ...plainObject(d.user) }\n}\n\n/** describe 不可就绪码:宿主 settings 未解析/未注册——值得重试的瞬态。 */\nconst RETRYABLE_CODES = new Set(['settings-unavailable', 'ns-not-registered'])\nconst RETRY_DELAY_MS = 1500\nconst RETRY_MAX = 4\n\nexport class BridgeDocHandle<T> {\n private snapshot: Snapshot<T>\n private listeners = new Set<() => void>()\n private revision: number | undefined\n private retriesLeft = RETRY_MAX\n private inflight: Promise<void> | undefined\n private readonly ns: string\n private readonly routes: { describe: string; mutate: string }\n\n constructor(ns: string) {\n this.ns = ns\n this.routes = settingsBridgeRoutesFor(ns)\n this.snapshot = {\n status: 'loading',\n value: undefined,\n revision: undefined,\n writable: true,\n base: undefined,\n user: undefined,\n mode: 'bridge',\n }\n void this.refresh()\n }\n\n getSnapshot(): Snapshot<T> {\n return this.snapshot\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n async set(field: string, value: unknown): Promise<boolean> {\n return this.mutate([{ op: 'set', path: [field], value }])\n }\n\n async unset(field: string): Promise<boolean> {\n return this.mutate([{ op: 'unset', path: [field] }])\n }\n\n private notify(): void {\n for (const l of [...this.listeners]) l()\n }\n\n private applyDescriptor(d: Descriptor, writable: boolean): void {\n this.revision = typeof d.revision === 'number' ? d.revision : undefined\n this.snapshot = {\n status: 'ready',\n value: descriptorPlainValue(d) as T | undefined,\n revision: this.revision,\n writable,\n base: undefined,\n user: undefined,\n mode: 'bridge',\n }\n }\n\n private async request(path: string, body?: unknown): Promise<BridgeBody> {\n const response = await fetch(path, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body ?? {}),\n })\n if (!response.ok) throw new Error(`bridge ${path} HTTP ${response.status}`)\n return (await response.json()) as BridgeBody\n }\n\n /** describe 拉取;瞬态未就绪码在重试预算内延迟重拉,耗尽才落 unavailable。 */\n refresh(): Promise<void> {\n if (this.inflight !== undefined) return this.inflight\n this.inflight = (async () => {\n try {\n const body = await this.request(this.routes.describe)\n if (body.ok && body.value?.descriptor !== undefined) {\n this.retriesLeft = RETRY_MAX\n this.applyDescriptor(body.value.descriptor, body.value.writable !== false)\n } else if (body.code !== undefined && RETRYABLE_CODES.has(body.code) && this.retriesLeft > 0) {\n this.retriesLeft -= 1\n this.inflight = undefined\n setTimeout(() => {\n void this.refresh()\n }, RETRY_DELAY_MS)\n return\n } else {\n this.snapshot = { ...this.snapshot, status: 'unavailable', writable: false }\n }\n } catch {\n // 网络异常:按瞬态处理,预算内重试\n if (this.retriesLeft > 0) {\n this.retriesLeft -= 1\n this.inflight = undefined\n setTimeout(() => {\n void this.refresh()\n }, RETRY_DELAY_MS)\n return\n }\n this.snapshot = { ...this.snapshot, status: 'unavailable', writable: false }\n }\n // 重试路径已在上面清过 inflight(setTimeout 后由 refresh() 重建);\n // 终态路径在此清挂起标记并广播。重试等待期 notify 无害(快照未变)。\n this.inflight = undefined\n this.notify()\n })()\n return this.inflight\n }\n\n private async mutate(ops: MutateOp[]): Promise<boolean> {\n const body = await this.request(this.routes.mutate, { ops, expectedRevision: this.revision })\n if (body.ok && body.value?.descriptor !== undefined) {\n this.applyDescriptor(body.value.descriptor, this.snapshot.writable)\n this.notify()\n return true\n }\n if (body.code === 'settings-conflict') {\n await this.refresh() // 乐观并发失败:拉权威值,让下次写入带新 revision\n return false\n }\n throw new Error(`bridge mutate failed: ${body.code ?? 'unknown'} ${body.message ?? ''}`)\n }\n}\n","/**\r\n * dsh-session-guard — 家族节接管组件(fallback host section)。\r\n *\r\n * 仅当宿主 dsh-thinking-levels 缺席、且本插件在接管选举中胜出时,由\r\n * index.ts 注册为顶级 `settings.section`(id 固定 `dsh-family`,节名仍为\r\n * 「起子插件设置」)。组件是纯通用渲染:把 `dsh-family.tab` 账本里的每张\r\n * 贡献卡(含本插件自己的,因为接管时本插件也声明了该子席位)按 order\r\n * 依次 renderSlot —— 卡片本身是默认展开的抽屉,整节即一页抽屉。\r\n *\r\n * 与 thinking-levels 的 FamilySettingsSection 的差别仅在不需要 thinking-\r\n * levels 自己的卡(宿主不在了,它的卡也不在):渲染循环没有 own-tab 特例。\r\n */\r\nimport { useEffect, useState } from 'react'\r\nimport type { JSX } from 'react'\r\n\r\n/** 与 FamilySectionInjected.hooks.tabs 同形的账本投影(结构化镜像)。 */\r\ntype HostObservable<T> = {\r\n getSnapshot(): T\r\n subscribe(listener: () => void): () => void\r\n}\r\n\r\n/** 账本条目(id/order/label,label 已解析为字符串)。 */\r\nexport interface FamilyTabEntry {\r\n id: string\r\n order: number\r\n label: string\r\n}\r\n\r\n/** 组件 props:hooks 舱(宿主 bindInjectHooks 把 inject 面的 `hooks` 键剥掉\r\n * 转成 `useTabs`——0.1.0 起各代皆然,故 useTabs 是正路、raw hooks 只是\r\n * 无转换环境的兜底)+ ≤0.1.5 壳缺 renderSlot 时的直接挂载面。 */\r\nexport interface GuardFamilySectionProps {\r\n renderSlot?: (key: 'dsh-family.tab', owner?: object, opts?: { only?: string; fallback?: JSX.Element | null }) => JSX.Element | null\r\n /** 宿主转换后的 hooks 舱(`inject.hooks.tabs` → `useTabs`)。 */\r\n useTabs?: <S>(selector: (value: readonly FamilyTabEntry[]) => S) => S\r\n /** 未被宿主转换时的原始账本投影(防御位,实测各代都会转换走 useTabs)。 */\r\n hooks?: { tabs: HostObservable<readonly FamilyTabEntry[]> }\r\n /** 直接挂载面(≤0.1.5 壳无 renderSlot 时):注册侧从账本原始条目\r\n * (entries 自带 component + inject)现挂激活卡。失败一律 null。 */\r\n renderContributor?: (id: string) => JSX.Element | null\r\n}\r\n\r\nconst NO_TABS: readonly FamilyTabEntry[] = []\r\n\r\nexport function GuardFamilySection(props: GuardFamilySectionProps): JSX.Element {\r\n const raw = props.hooks?.tabs\r\n // 账本读取:useTabs 舱优先;raw 投影走 React 订阅。face 选择每代固定,\r\n // 两种路径的 hook 数不同但同实例内次序恒定。\r\n const [polled, setPolled] = useState<readonly FamilyTabEntry[]>(() => raw?.getSnapshot() ?? NO_TABS)\r\n useEffect(() => {\r\n if (props.useTabs !== undefined || raw === undefined) return\r\n return raw.subscribe(() => setPolled(raw.getSnapshot()))\r\n })\r\n const tabs = props.useTabs !== undefined ? props.useTabs(value => value) : polled\r\n return (\r\n <div style={{ display: 'grid', gap: '12px' }}>\r\n {tabs.map(row => (\r\n <div key={row.id}>\r\n {props.renderSlot\r\n ? props.renderSlot('dsh-family.tab', {}, { only: row.id, fallback: null })\r\n : props.renderContributor?.(row.id) ?? null}\r\n </div>\r\n ))}\r\n </div>\r\n )\r\n}\r\n","/**\r\n * dsh-session-guard — 浏览器 half。\r\n *\r\n * 职责(全部 fail-open,D8):\r\n * - 在 composer 输入区右侧注册一个**纯展示**状态徽标(高峰/谷时/周末),轮询\r\n * /session-guard/status;\r\n * - **不做**冻结/会话动作——冻结按钮由 input-traffic 接管并经 /session-guard/rpc\r\n * 桥接 host 会话门;本插件客户端不注册任何按钮,避免与 input-traffic 冲突。\r\n * - 0.1.7:旧的插件设置卡已随席位删除 —— 设置表单由 host\r\n * 侧 Config 的 `.volatile()` 字段自动生成。\r\n * - 跨版本(universal-rc):持久设置经 configForms(0.1.7+)/settingsScope(≤0.1.5)\r\n * 双代腰产出同形 scope;无设置页宿主(0.1.0/0.1.1)经 shell.overlay 浮窗\r\n * 齿轮提供配置入口(配置存储全在 host 侧,浮层与宿主设置页解耦)。\r\n *\r\n * 构建:tsdown → lib/client.js(__ModuleLoader__.load 注册,与 input-traffic 同构)。\r\n */\r\nimport { createElement } from 'react'\r\nimport type { JSX } from 'react'\r\nimport { StatusBadge } from './status-badge'\r\nimport { PauseButton } from './pause-button'\r\nimport { SessionGuardCard } from './settings-card'\r\nimport { BridgeDocHandle } from './settings-bridge.ts'\r\nimport { GuardFamilySection, type FamilyTabEntry } from './family-section'\r\nimport type { SessionGuardConfig } from './settings-card'\r\nimport type { SettingsScope } from './scope-face'\r\n\r\n// 起子插件族共用设置节:宿主 dsh-thinking-levels 注册顶级 `settings.section`\r\n// (id `dsh-family`)并声明 `dsh-family.tab` 子席位,本插件贡献卡片。宿主缺席\r\n// 时本插件按下载量排名接管该节(见 apply 内的选举逻辑),节名仍为\r\n// 「起子插件设置」;宿主在场时本插件只贡献卡片。\r\n// 类型导入(构建时擦除,bundle 无 value-import 平台包):让下方 `declare\r\n// module` 增补能解析到 @deepseek-ai/dsh-client-ui-slots 的 SlotMap。\r\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\r\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\r\n interface SlotMap {\r\n 'dsh-family.tab': { kind: 'list'; scope: 'root' }\r\n /**\r\n * 插件页配置卡(官方 ui-plugin-manager 契约,key = 包名):DSH 0.2.0 的\r\n * Plugins 页不自动渲染 volatile 配置表单,bundle 详情页只渲染注册在本\r\n * keyed 席位下的卡片;owner props 由页面给(`{ view: 'page', form }`),\r\n * 本卡片改经 inject 工厂拿自己的 scope(与 dsh-family.tab 注册同源)。\r\n */\r\n 'plugins.bundle.config': { kind: 'keyed'; scope: 'root'; owner: Record<string, unknown> }\r\n }\r\n}\r\n\r\n/** 家族节固定 id(与 thinking-levels 的注册严格一致)。 */\r\nconst FAMILY_SECTION_ID = 'dsh-family'\r\n/** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */\r\nconst FAMILY_CHILD_KEY = 'dsh-family.tab'\r\n/** 家族容器接管分级——唯一真源是 `mine-dsh-plugins/_tools/family-rank.mjs` 里那张\r\n * **外部维护、已冻结(2026-10-14)的权重表**(规则:thinking-levels 钉 rank 0,其余按\r\n * git 首提交日期「越新越低」;不再由 star/下载量派生——同量级插件之间那个分数没有信号,\r\n * 实测 steward/mcp 只差 0.0003)。不得在注释/代码里另行推数。rank 越小越优先,\r\n * 延迟 = BASE + RANK * STEP。本插件按冻结表为 rank 2(次于 thinking-levels / input-traffic)。\r\n * 旧值 500ms 是拍的:它只够等 TL 落账,但排在它后面的 steward/mcp 无法据此错开。 */\r\nconst FAMILY_CLAIM_RANK = 2\r\nconst FAMILY_CLAIM_BASE_MS = 200\r\nconst FAMILY_CLAIM_STEP_MS = 900\r\n/** 让位宽限(由分级派生):TL rank 0 在 200ms 落账,本插件 rank 1 到 1100ms 才查账本——\r\n * 落账时刻由各家 rank 决定(不再依赖 inject 注册顺序里谁先 fire),此延迟后足以分辨「TL 在场」。 */\r\nconst FAMILY_YIELD_MS = FAMILY_CLAIM_BASE_MS + FAMILY_CLAIM_RANK * FAMILY_CLAIM_STEP_MS\r\n\r\n/**\r\n * 客户端所需服务:slots(状态徽标)+ locale。持久设置走双代腰\r\n * (registerSettingsFaces):configForms 为 0.1.7+ 独有,顶层注入会在\r\n * 0.1.0/0.1.1(无此服务)上让整段 apply 静默死。\r\n */\r\nexport const inject = ['slots', 'locale']\r\n\r\n/** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */\r\ninterface SlotsFace {\r\n inject: (_name: string, _fn: () => unknown) => () => void\r\n register: (_options: Record<string, unknown>, _component: unknown) => () => void\r\n entries: (_name: string) => Array<{\r\n options: { id?: string; order?: number; label?: unknown }\r\n component?: unknown\r\n inject?: () => unknown\r\n }>\r\n getVersion: (_name: string) => number\r\n subscribe: (_name: string, _listener: () => void) => () => void\r\n}\r\ninterface ConfigFormsFactoryFace {\r\n get: <T>(_entryId: string) => SettingsScope<T>\r\n}\r\ninterface SettingsScopeFactoryFace {\r\n bind: <T>(_opts: { namespace: string }) => SettingsScope<T>\r\n}\r\ninterface ClientCtx {\r\n slots: SlotsFace\r\n inject: (\r\n _names: string[],\r\n _fn: (sctx: { configForms?: ConfigFormsFactoryFace; settingsScope?: SettingsScopeFactoryFace }) => void,\r\n ) => void\r\n}\r\n\r\n/** 单面注册护栏:某条宿主线上未知槽名/缺服务的同步异常不得带走其它面。 */\r\nfunction safe(tag: string, fn: () => void): void {\r\n try {\r\n fn()\r\n } catch (error) {\r\n console.warn(`dsh-session-guard: client surface '${tag}' failed:`, error)\r\n }\r\n}\r\n\r\n/**\r\n * 双代持久设置腰(移植 input-traffic registerSettingsFaces):两个 scoped\r\n * 子注入各自产出同形 scope 后回调。M4 P0 重构后本函数**只承担数据臂**——\r\n * 回调仅写 scopeRef 活引用(0.1.7+ 原生优先,mounted 位防双挂),卡面注册\r\n * 已全部提到 apply 顶层(≤0.1.5 回调永不触发,注册不能关在这里)。\r\n */\r\nfunction registerSettingsFaces(\r\n ctx: ClientCtx,\r\n onScope: (scope: SettingsScope<SessionGuardConfig>, generation: 'configForms' | 'settingsScope') => void,\r\n): void {\r\n let mounted = false\r\n ctx.inject(['configForms'], (sctx) => {\r\n if (mounted) return\r\n mounted = true\r\n onScope(sctx.configForms!.get<SessionGuardConfig>('session-guard'), 'configForms')\r\n })\r\n ctx.inject(['settingsScope'], (sctx) => {\r\n if (mounted) return\r\n mounted = true\r\n onScope(sctx.settingsScope!.bind<SessionGuardConfig>({ namespace: 'session-guard' }), 'settingsScope')\r\n })\r\n}\r\n\r\n/** scope 活引用(perm-gate client/index.ts L166-257 形态):回调填、卡面现读。 */\r\nconst scopeRef: { scope: SettingsScope<SessionGuardConfig> | undefined } = { scope: undefined }\r\n\r\n/** 桥轨惰性单例:≤0.1.5(settingsScope 死路 / 0.1.0-0.1.1 无服务)的数据源。 */\r\nlet bridgeScopeSingleton: BridgeDocHandle<SessionGuardConfig> | undefined\r\nfunction bridgeScope(): BridgeDocHandle<SessionGuardConfig> {\r\n if (bridgeScopeSingleton === undefined) bridgeScopeSingleton = new BridgeDocHandle<SessionGuardConfig>('session-guard')\r\n return bridgeScopeSingleton\r\n}\r\n\r\n/** 卡面现取数据源:原生句柄解到即原生,否则桥兜底(perm-gate 同式)。 */\r\nfunction currentScope(): SettingsScope<SessionGuardConfig> {\r\n return scopeRef.scope ?? bridgeScope()\r\n}\r\n\r\n/** 两张配置卡(family tab + bundle 详情页)挂载;数据经 currentScope() 现取。 */\r\nfunction mountConfigCards(ctx: ClientCtx): void {\r\n // 插件族共用 tab 贡献卡:读写本插件 entry(`session-guard`)的持久配置。\r\n ctx.slots.inject('dsh-family.tab', () => ctx.slots.register({\r\n name: 'dsh-family.tab',\r\n id: 'session-guard',\r\n order: 20,\r\n label: '会话守护门禁',\r\n locale: 'session-guard',\r\n inject: () => ({ scope: currentScope() }),\r\n }, SessionGuardCard))\r\n\r\n // 插件页配置卡(DSH 0.2.0):Plugins 页不自动渲染 volatile 配置表单,须注册\r\n // `plugins.bundle.config` 席位(key = 包名),配置卡才渲染在 bundle 详情页。\r\n // 卡片组件与 props(scope)与上方 dsh-family.tab 注册完全同源。\r\n ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({\r\n name: 'plugins.bundle.config',\r\n key: 'dsh-session-guard',\r\n inject: () => ({ scope: currentScope() }),\r\n }, SessionGuardCard))\r\n}\r\n\r\nexport function apply(ctx: ClientCtx) {\r\n // 暂停会话按钮(v0.2.0):step 级门控的状态显示 + 手动解除。\r\n // order 20 < input-traffic 冻结按钮的 30 → 排在左侧,两者并列互不取代(ADR-001/002)。\r\n safe('conversation.input.right/pause', () => ctx.slots.inject('conversation.input.right', () => ctx.slots.register({\r\n name: 'conversation.input.right',\r\n id: 'session-guard-pause',\r\n order: 20,\r\n locale: 'session-guard',\r\n }, PauseButton)))\r\n\r\n // 状态徽标:输入区右侧,纯展示。\r\n safe('conversation.input.right/status', () => ctx.slots.inject('conversation.input.right', () => ctx.slots.register({\r\n name: 'conversation.input.right',\r\n id: 'session-guard-status',\r\n order: 40,\r\n locale: 'session-guard',\r\n }, StatusBadge)))\r\n\r\n // ── 配置卡三面(M4 P0 重构:注册提到 apply 顶层)────────────────────\r\n // 旧形态把注册关在 onScope 回调里——≤0.1.5 settingsScope 死路/0.1.0-0.1.1\r\n // 无服务 → 回调永不触发 → 三面永不注册、桥无消费者(perm-gate client/\r\n // index.ts L160-165 注释点名的反例)。现注册无条件执行,数据经 currentScope()\r\n // 现取:原生句柄解到即原生,否则桥兜底。\r\n safe('config-cards/family-tab', () => mountConfigCards(ctx))\r\n // 老设置页(≤0.1.5)插件列表配置卡(settings.plugin.item)已于 2026-10-08\r\n // 撤除(family-insection 第二轮):与会话守护门禁家族 tab 重复——家族 tab\r\n // 是唯一设置面(TL/free-search/browser-cdp/preset-manager 同批撤各自 item 卡)。\r\n\r\n // 双代腰 = 纯数据臂,**按代分流**(2026-10-08 深夜五格实测裁决,findings「实测裁决」节):\r\n // - configForms(0.1.7+):句柄写读皆活 → 入 scopeRef(原生优先,桥自动让位)。\r\n // - settingsScope(0.1.2/0.1.5):句柄**写活读死**——set() 落盘持久化,但\r\n // getSnapshot().value 恒 undefined(卡渲染默认值,磁盘是真值)。留空\r\n // scopeRef → currentScope() 落桥:桥 mutate 同样落宿主服务(写行为等价),\r\n // describe 读到持久化真值(读路径痊愈)。0.1.0/0.1.1 无 client 服务,\r\n // 同样落桥。\r\n registerSettingsFaces(ctx, (scope, generation) => {\r\n if (generation === 'configForms') scopeRef.scope = scope\r\n })\r\n\r\n // 家族节接管(2026-10 对齐 TL route A + bindInjectHooks 实证):让位选举。\r\n // 2026-10-08 T20-a 实证修正:≤0.1.7 壳的设置导航对同 id 异 priority 的遮蔽位\r\n // 也各列一行——「TL(p0) 恒头位、本插件 p10 恒阴影不占导航」假设不成立,双注册\r\n // = 双「起子插件设置」导航行(0.1.0~0.1.7 五格全现;0.1.0 摘除 guard 实验:\r\n // 单行归位、TL 节独座满血)。改为延迟让位:**四家同走一条派生阶梯**——TL rank 0 在\r\n // 200ms 落账、本插件 rank 1 到 1100ms 才查账本(落账时刻由 rank 决定,不再靠 inject\r\n // 注册顺序里谁先 fire),所以本插件查账本时 TL 早已坐好:已有 dsh-family → 让位退场;\r\n // 仍空(TL 缺席/落账失败)→ 接管。inject 仍必须 apply 期注册——slots 核只在「声明\r\n // 事件」触发工厂,宽限期后注册的 inject 等不到下一次声明(0.1.0 实机日志定案:\r\n // 4.1.7/4.1.8 的 grace 后注册工厂零执行);register 本就是指令式调用,移入定时器不\r\n // 改变机制(TL 自 2026-10-14 起也同样移入定时器)。子席位声明是 declare-or-withdraw:\r\n // TL 先声明 → 本插件放弃接管\r\n // (绝不退回不带 children 的裸注册——残废容器照样占住导航行,后来者按「已有\r\n // dsh-family」集体让位,结果是成员 tab 全灭而节还在,2026-10-14 面板丢失即此);\r\n // 同 id 同 priority 竞争 → 双抛,放弃。register 走本工厂触发\r\n // 管道(裸注册的 entry.inject 在 ≤0.1.5 壳上不解,4.1.5「半补位」真相)。\r\n safe('family-section-election', () => {\r\n console.log('[dsh-session-guard] election: registering settings.section inject (apply-time)')\r\n ctx.slots.inject('settings.section', () => {\r\n console.log('[dsh-session-guard] election: factory fired (settings.section declared)')\r\n setTimeout(() => {\r\n if (ctx.slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) {\r\n console.log('[dsh-session-guard] election: dsh-family already seated — stand down')\r\n return\r\n }\r\n const base = {\r\n name: 'settings.section',\r\n id: FAMILY_SECTION_ID,\r\n order: 40,\r\n priority: 10,\r\n label: () => '起子插件设置',\r\n inject: () => ({\r\n hooks: makeGuardFamilyTabsHooks(ctx.slots),\r\n renderContributor,\r\n }),\r\n }\r\n try {\r\n ctx.slots.register({\r\n ...base,\r\n children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },\r\n }, GuardFamilySection)\r\n console.log('[dsh-session-guard] family takeover registered (children declared)')\r\n } catch (error) {\r\n // Declare-or-withdraw: whoever holds the children table serves the family.\r\n // Seating ourselves without it would keep the nav row while rendering no\r\n // member tab at all — the failure mode this replaces.\r\n console.log(`[dsh-session-guard] children claim lost — withdrawing (${error instanceof Error ? error.message : error})`)\r\n }\r\n }, FAMILY_YIELD_MS)\r\n })\r\n })\r\n}\r\n\r\n/** 直接挂载面(≤0.1.5 壳无 renderSlot 的降级):从账本原始条目现挂激活卡。\r\n * entries 每代都带 component + inject(ui-slots SlotCore.entries 原始记录);\r\n * 任何失败(条目消失/抛错 inject)降级为 null,不炸接管节。 */\r\nfunction renderContributor(id: string): JSX.Element | null {\r\n try {\r\n const entry = ctx.slots.entries(FAMILY_CHILD_KEY).find(e => e.options.id === id)\r\n if (entry === undefined || typeof entry.component !== 'function') return null\r\n const injected = typeof entry.inject === 'function' ? entry.inject() : undefined\r\n return createElement(\r\n entry.component as (props: Record<string, unknown>) => JSX.Element | null,\r\n (injected ?? {}) as Record<string, unknown>,\r\n )\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\n/** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形)。 */\r\nfunction makeGuardFamilyTabsHooks(slots: SlotsFace): {\r\n getSnapshot: () => readonly FamilyTabEntry[]\r\n subscribe: (listener: () => void) => () => void\r\n} {\r\n let version = -1\r\n let tabs: readonly FamilyTabEntry[] = []\r\n return {\r\n getSnapshot: () => {\r\n const next = slots.getVersion(FAMILY_CHILD_KEY)\r\n if (next !== version) {\r\n version = next\r\n tabs = slots.entries(FAMILY_CHILD_KEY)\r\n .map(entry => ({\r\n id: entry.options.id ?? '',\r\n order: entry.options.order ?? 0,\r\n label: typeof entry.options.label === 'function'\r\n ? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()\r\n : String(entry.options.label ?? entry.options.id ?? ''),\r\n }))\r\n .sort((a, b) => a.order - b.order)\r\n }\r\n return tabs\r\n },\r\n subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),\r\n }\r\n}\r\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -48,8 +48,18 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
48
48
|
const FAMILY_SECTION_ID = 'dsh-family'
|
|
49
49
|
/** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
|
|
50
50
|
const FAMILY_CHILD_KEY = 'dsh-family.tab'
|
|
51
|
-
/**
|
|
52
|
-
|
|
51
|
+
/** 家族容器接管分级——唯一真源是 `mine-dsh-plugins/_tools/family-rank.mjs` 里那张
|
|
52
|
+
* **外部维护、已冻结(2026-10-14)的权重表**(规则:thinking-levels 钉 rank 0,其余按
|
|
53
|
+
* git 首提交日期「越新越低」;不再由 star/下载量派生——同量级插件之间那个分数没有信号,
|
|
54
|
+
* 实测 steward/mcp 只差 0.0003)。不得在注释/代码里另行推数。rank 越小越优先,
|
|
55
|
+
* 延迟 = BASE + RANK * STEP。本插件按冻结表为 rank 2(次于 thinking-levels / input-traffic)。
|
|
56
|
+
* 旧值 500ms 是拍的:它只够等 TL 落账,但排在它后面的 steward/mcp 无法据此错开。 */
|
|
57
|
+
const FAMILY_CLAIM_RANK = 2
|
|
58
|
+
const FAMILY_CLAIM_BASE_MS = 200
|
|
59
|
+
const FAMILY_CLAIM_STEP_MS = 900
|
|
60
|
+
/** 让位宽限(由分级派生):TL rank 0 在 200ms 落账,本插件 rank 1 到 1100ms 才查账本——
|
|
61
|
+
* 落账时刻由各家 rank 决定(不再依赖 inject 注册顺序里谁先 fire),此延迟后足以分辨「TL 在场」。 */
|
|
62
|
+
const FAMILY_YIELD_MS = FAMILY_CLAIM_BASE_MS + FAMILY_CLAIM_RANK * FAMILY_CLAIM_STEP_MS
|
|
53
63
|
|
|
54
64
|
/**
|
|
55
65
|
* 客户端所需服务:slots(状态徽标)+ locale。持久设置走双代腰
|
|
@@ -196,14 +206,17 @@ export function apply(ctx: ClientCtx) {
|
|
|
196
206
|
// 2026-10-08 T20-a 实证修正:≤0.1.7 壳的设置导航对同 id 异 priority 的遮蔽位
|
|
197
207
|
// 也各列一行——「TL(p0) 恒头位、本插件 p10 恒阴影不占导航」假设不成立,双注册
|
|
198
208
|
// = 双「起子插件设置」导航行(0.1.0~0.1.7 五格全现;0.1.0 摘除 guard 实验:
|
|
199
|
-
// 单行归位、TL
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
209
|
+
// 单行归位、TL 节独座满血)。改为延迟让位:**四家同走一条派生阶梯**——TL rank 0 在
|
|
210
|
+
// 200ms 落账、本插件 rank 1 到 1100ms 才查账本(落账时刻由 rank 决定,不再靠 inject
|
|
211
|
+
// 注册顺序里谁先 fire),所以本插件查账本时 TL 早已坐好:已有 dsh-family → 让位退场;
|
|
212
|
+
// 仍空(TL 缺席/落账失败)→ 接管。inject 仍必须 apply 期注册——slots 核只在「声明
|
|
203
213
|
// 事件」触发工厂,宽限期后注册的 inject 等不到下一次声明(0.1.0 实机日志定案:
|
|
204
|
-
// 4.1.7/4.1.8 的 grace 后注册工厂零执行);register
|
|
205
|
-
//
|
|
206
|
-
//
|
|
214
|
+
// 4.1.7/4.1.8 的 grace 后注册工厂零执行);register 本就是指令式调用,移入定时器不
|
|
215
|
+
// 改变机制(TL 自 2026-10-14 起也同样移入定时器)。子席位声明是 declare-or-withdraw:
|
|
216
|
+
// TL 先声明 → 本插件放弃接管
|
|
217
|
+
// (绝不退回不带 children 的裸注册——残废容器照样占住导航行,后来者按「已有
|
|
218
|
+
// dsh-family」集体让位,结果是成员 tab 全灭而节还在,2026-10-14 面板丢失即此);
|
|
219
|
+
// 同 id 同 priority 竞争 → 双抛,放弃。register 走本工厂触发
|
|
207
220
|
// 管道(裸注册的 entry.inject 在 ≤0.1.5 壳上不解,4.1.5「半补位」真相)。
|
|
208
221
|
safe('family-section-election', () => {
|
|
209
222
|
console.log('[dsh-session-guard] election: registering settings.section inject (apply-time)')
|
|
@@ -232,13 +245,10 @@ export function apply(ctx: ClientCtx) {
|
|
|
232
245
|
}, GuardFamilySection)
|
|
233
246
|
console.log('[dsh-session-guard] family takeover registered (children declared)')
|
|
234
247
|
} catch (error) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
} catch (secondary) {
|
|
240
|
-
console.log(`[dsh-session-guard] family takeover abandoned: ${secondary instanceof Error ? secondary.message : secondary}`)
|
|
241
|
-
}
|
|
248
|
+
// Declare-or-withdraw: whoever holds the children table serves the family.
|
|
249
|
+
// Seating ourselves without it would keep the nav row while rendering no
|
|
250
|
+
// member tab at all — the failure mode this replaces.
|
|
251
|
+
console.log(`[dsh-session-guard] children claim lost — withdrawing (${error instanceof Error ? error.message : error})`)
|
|
242
252
|
}
|
|
243
253
|
}, FAMILY_YIELD_MS)
|
|
244
254
|
})
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-session-guard — 修正表的 IO 层:按年缓存落盘 + 自动抓取 + 启动指纹校验。
|
|
3
|
+
*
|
|
4
|
+
* 数据链路(全部 fail-open,任何失败只降级不抛错):
|
|
5
|
+
* 1. 内置快照(holidays.js BUILTIN_TABLES,已定稿年份);
|
|
6
|
+
* 2. 磁盘缓存 `$DSH_HOME/.dsh/session-guard/holidays/<year>.json`
|
|
7
|
+
* (`DSH_SESSION_GUARD_HOLIDAY_DIR` 可覆盖根目录)——**启动时校验指纹**:
|
|
8
|
+
* 重算内容 sha256 与文件内记录比对,不一致视为损坏,弃用并触发重抓;
|
|
9
|
+
* 3. 远端 holiday-cn(jsDelivr 主源、raw.githubusercontent 备源)——
|
|
10
|
+
* 抓取成功且指纹有变化才原子写缓存(tmp + rename);指纹一致只刷新
|
|
11
|
+
* checkedAt,不写盘(2026 已定稿,日常核对几乎恒等)。
|
|
12
|
+
*
|
|
13
|
+
* 「按年自动尝试抓取」:启动核对一次,之后每小时 ensureFresh;相关年份 =
|
|
14
|
+
* 北京时间当年 + 12 月追加次年(国务院公告一般 11–12 月发布)。内置表
|
|
15
|
+
* 覆盖的年份仍会被远端覆盖更新(公告修正 / 补录),但抓取节奏不变。
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { dirname, join } from 'node:path'
|
|
19
|
+
import { storeRoot } from './store.js'
|
|
20
|
+
import { wallClock, BILLING_TIMEZONE } from './time.js'
|
|
21
|
+
import { BUILTIN_TABLES, buildMap, fingerprint, parseHolidayCn } from './holidays.js'
|
|
22
|
+
|
|
23
|
+
/** 单轮远端抓取超时(ms)。 */
|
|
24
|
+
const FETCH_TIMEOUT_MS = 10_000
|
|
25
|
+
/** 已到手年份表的复核间隔:24h。 */
|
|
26
|
+
const REFRESH_MS = 24 * 3600 * 1000
|
|
27
|
+
|
|
28
|
+
/** 缓存根目录(独立于 pause / 队列锁子目录)。 */
|
|
29
|
+
export function holidaysRoot() {
|
|
30
|
+
return process.env.DSH_SESSION_GUARD_HOLIDAY_DIR || join(storeRoot(), 'holidays')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 某年缓存文件路径。 */
|
|
34
|
+
export function holidayFilePath(year, root = holidaysRoot()) {
|
|
35
|
+
return join(root, `${Number(year)}.json`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 远端候选源:jsDelivr 主源(国内可达性好),GitHub raw 备源。 */
|
|
39
|
+
export function holidayUrls(year) {
|
|
40
|
+
const y = Number(year)
|
|
41
|
+
return [
|
|
42
|
+
`https://cdn.jsdelivr.net/gh/NateScarlet/holiday-cn@master/${y}.json`,
|
|
43
|
+
`https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/${y}.json`,
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 创建修正表存储。
|
|
49
|
+
* @param {object} [deps] { logger, fetchImpl, now, builtinTables, root }
|
|
50
|
+
* root 供单测注入临时目录;fetchImpl 供 mock。
|
|
51
|
+
* @returns {{
|
|
52
|
+
* loadAll: () => void,
|
|
53
|
+
* ensureFresh: (opts?: { autoFetch?: boolean }) => Promise<void>,
|
|
54
|
+
* map: () => Map<string, boolean>,
|
|
55
|
+
* info: () => object[],
|
|
56
|
+
* }}
|
|
57
|
+
*/
|
|
58
|
+
export function createHolidayStore(deps = {}) {
|
|
59
|
+
const logger = deps.logger
|
|
60
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch
|
|
61
|
+
const now = deps.now ?? (() => Date.now())
|
|
62
|
+
const builtin = deps.builtinTables ?? BUILTIN_TABLES
|
|
63
|
+
const root = deps.root ?? holidaysRoot()
|
|
64
|
+
|
|
65
|
+
/** @type {Map<number, {days: {date:string,isOffDay:boolean}[], sha:string, source:'builtin'|'cache'|'live', fetchedAt:number|null, checkedAt:number|null}>} */
|
|
66
|
+
const tables = new Map()
|
|
67
|
+
/** map() 的惰性合并缓存:任何表更新时置 null。 */
|
|
68
|
+
let merged = null
|
|
69
|
+
|
|
70
|
+
const warn = (msg, e) => {
|
|
71
|
+
const line = `[session-guard] ${msg}`
|
|
72
|
+
if (logger?.warn) logger.warn(e ? `${line}: ${String(e && e.message || e)}` : line)
|
|
73
|
+
else console.warn(e ? `${line}: ${String(e && e.message || e)}` : line)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function putTable(year, days, source, fetchedAt) {
|
|
77
|
+
tables.set(year, { days, sha: fingerprint(days), source, fetchedAt, checkedAt: now() })
|
|
78
|
+
merged = null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 装载单年:内置打底,缓存指纹校验通过则覆盖内置。 */
|
|
82
|
+
function loadYear(year) {
|
|
83
|
+
const seed = builtin.find((t) => t.year === year)
|
|
84
|
+
if (seed) putTable(year, seed.days.slice(), 'builtin', null)
|
|
85
|
+
let cached
|
|
86
|
+
try {
|
|
87
|
+
const f = holidayFilePath(year, root)
|
|
88
|
+
if (!existsSync(f)) return
|
|
89
|
+
cached = JSON.parse(readFileSync(f, 'utf8'))
|
|
90
|
+
} catch (e) {
|
|
91
|
+
warn('holiday cache unreadable (ignored)', e)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const parsed = parseHolidayCn(cached)
|
|
96
|
+
if (parsed.year !== year) throw new Error(`cache year ${parsed.year} != file slot ${year}`)
|
|
97
|
+
const sha = fingerprint(parsed.days)
|
|
98
|
+
if (cached.sha !== sha) {
|
|
99
|
+
// 启动指纹校验失败:内容损坏/被改,弃用缓存(ensureFresh 会重抓)。
|
|
100
|
+
warn(`holiday cache fingerprint mismatch for ${year}, discarding`)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
putTable(year, parsed.days, 'cache', Number(cached.fetchedAt) || null)
|
|
104
|
+
} catch (e) {
|
|
105
|
+
warn(`holiday cache invalid for ${year} (ignored)`, e)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 相关年份:北京当年 +(11/12 月)次年——国务院一般年底公布来年安排。 */
|
|
110
|
+
function relevantYears() {
|
|
111
|
+
const wc = wallClock(BILLING_TIMEZONE, new Date(now()))
|
|
112
|
+
const years = [wc.year]
|
|
113
|
+
if (wc.month >= 11) years.push(wc.year + 1)
|
|
114
|
+
return years
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function fetchYear(year) {
|
|
118
|
+
if (typeof fetchImpl !== 'function') return false
|
|
119
|
+
for (const url of holidayUrls(year)) {
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetchImpl(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) })
|
|
122
|
+
if (!res || res.ok !== true) continue
|
|
123
|
+
const parsed = parseHolidayCn(await res.json())
|
|
124
|
+
if (parsed.year !== year) throw new Error(`payload year ${parsed.year} != requested ${year}`)
|
|
125
|
+
const sha = fingerprint(parsed.days)
|
|
126
|
+
const cur = tables.get(year)
|
|
127
|
+
if (cur && cur.sha === sha) {
|
|
128
|
+
cur.checkedAt = now() // 指纹一致:不写盘,只记核对时间
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
mkdirSync(dirname(holidayFilePath(year, root)), { recursive: true })
|
|
133
|
+
const tmp = `${holidayFilePath(year, root)}.tmp`
|
|
134
|
+
writeFileSync(tmp, JSON.stringify({
|
|
135
|
+
year, sha, fetchedAt: now(), sourceUrl: url,
|
|
136
|
+
days: parsed.days,
|
|
137
|
+
}, null, 2))
|
|
138
|
+
renameSync(tmp, holidayFilePath(year, root))
|
|
139
|
+
} catch (e) {
|
|
140
|
+
warn(`holiday cache write failed for ${year} (memory table still updated)`, e)
|
|
141
|
+
}
|
|
142
|
+
putTable(year, parsed.days, 'live', now())
|
|
143
|
+
warn(`holiday table updated for ${year} (${cur ? 'fingerprint changed' : 'new'}, ${sha.slice(0, 17)}…)`)
|
|
144
|
+
return true
|
|
145
|
+
} catch (e) {
|
|
146
|
+
warn(`holiday fetch failed via ${url}`, e)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
/** 启动装载(同步):内置 + 缓存 + 指纹校验。 */
|
|
154
|
+
loadAll() {
|
|
155
|
+
for (const year of relevantYears()) loadYear(year)
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 按年自动抓取核对:缺表的年份立即抓;已有表超 24h 复核。
|
|
160
|
+
* autoFetch=false(设置关)时只补缺年(缓存/内置仍生效),不发请求。
|
|
161
|
+
*/
|
|
162
|
+
async ensureFresh(opts = {}) {
|
|
163
|
+
const autoFetch = opts.autoFetch !== false
|
|
164
|
+
for (const year of relevantYears()) {
|
|
165
|
+
const cur = tables.get(year)
|
|
166
|
+
const anchor = Math.max((cur && cur.fetchedAt) || 0, (cur && cur.checkedAt) || 0)
|
|
167
|
+
if (cur && now() - anchor <= REFRESH_MS) continue // 24h 内已抓/已核对
|
|
168
|
+
if (autoFetch !== true) continue // 关自动抓取时纯靠内置 + 缓存,不发请求
|
|
169
|
+
await fetchYear(year)
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/** 合并所有已装载年份的查表 Map('YYYY-MM-DD' → 是否放假日)。 */
|
|
174
|
+
map() {
|
|
175
|
+
if (merged) return merged
|
|
176
|
+
const days = []
|
|
177
|
+
for (const t of tables.values()) days.push(...t.days)
|
|
178
|
+
merged = buildMap(days)
|
|
179
|
+
return merged
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/** 诊断输出(/session-guard/status 透出用)。 */
|
|
183
|
+
info() {
|
|
184
|
+
return [...tables.entries()]
|
|
185
|
+
.sort((a, b) => a[0] - b[0])
|
|
186
|
+
.map(([year, t]) => ({
|
|
187
|
+
year,
|
|
188
|
+
source: t.source,
|
|
189
|
+
fingerprint: t.sha,
|
|
190
|
+
fetchedAt: t.fetchedAt,
|
|
191
|
+
checkedAt: t.checkedAt,
|
|
192
|
+
entries: t.days.length,
|
|
193
|
+
}))
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|
package/src/holidays.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-session-guard — 法定节假日 / 调休修正表(纯逻辑,零 IO,可单测)。
|
|
3
|
+
*
|
|
4
|
+
* 数据源:holiday-cn(NateScarlet/holiday-cn,每日自动解析国务院公告),
|
|
5
|
+
* 全年 JSON:`{ year, papers: [...], days: [{ name, date, isOffDay }] }`。
|
|
6
|
+
* `isOffDay: true` = 放假(含落在周末的假日);`false` = 调休补班日
|
|
7
|
+
* (本来是周六日但要上班)。表外日期回退自然周末判定。
|
|
8
|
+
*
|
|
9
|
+
* 内置快照只覆盖已定稿年份(2026,39 条 = 33 放假日 + 6 调休补班日);
|
|
10
|
+
* 未覆盖年份由 holiday-store.js 按年自动抓取,抓不到即退化为纯自然周末
|
|
11
|
+
* (fail-open,与插件一贯风格一致)。
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
|
|
15
|
+
/** 'YYYY-MM-DD' 严格形态(月/日补零),也是 Map 键与指纹序列化的唯一形态。 */
|
|
16
|
+
const DATE_RE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 2026 年国务院放假安排(公告 content_7047091,经 holiday-cn 机读)。
|
|
20
|
+
* 只保留判定需要的 date / isOffDay 两键;name 仅注释性内嵌于分组注释。
|
|
21
|
+
*/
|
|
22
|
+
export const BUILTIN_2026 = Object.freeze({
|
|
23
|
+
year: 2026,
|
|
24
|
+
paper: 'https://www.gov.cn/zhengce/zhengceku/202511/content_7047091.htm',
|
|
25
|
+
days: Object.freeze([
|
|
26
|
+
// 元旦:1/1(四)-1/3(六) 放假,1/4(日) 补班
|
|
27
|
+
{ date: '2026-01-01', isOffDay: true },
|
|
28
|
+
{ date: '2026-01-02', isOffDay: true },
|
|
29
|
+
{ date: '2026-01-03', isOffDay: true },
|
|
30
|
+
{ date: '2026-01-04', isOffDay: false },
|
|
31
|
+
// 春节:2/15-2/23 放假,2/14(六)、2/28(六) 补班
|
|
32
|
+
{ date: '2026-02-14', isOffDay: false },
|
|
33
|
+
{ date: '2026-02-15', isOffDay: true },
|
|
34
|
+
{ date: '2026-02-16', isOffDay: true },
|
|
35
|
+
{ date: '2026-02-17', isOffDay: true },
|
|
36
|
+
{ date: '2026-02-18', isOffDay: true },
|
|
37
|
+
{ date: '2026-02-19', isOffDay: true },
|
|
38
|
+
{ date: '2026-02-20', isOffDay: true },
|
|
39
|
+
{ date: '2026-02-21', isOffDay: true },
|
|
40
|
+
{ date: '2026-02-22', isOffDay: true },
|
|
41
|
+
{ date: '2026-02-23', isOffDay: true },
|
|
42
|
+
{ date: '2026-02-28', isOffDay: false },
|
|
43
|
+
// 清明:4/4-4/6 放假,无补班
|
|
44
|
+
{ date: '2026-04-04', isOffDay: true },
|
|
45
|
+
{ date: '2026-04-05', isOffDay: true },
|
|
46
|
+
{ date: '2026-04-06', isOffDay: true },
|
|
47
|
+
// 劳动节:5/1-5/5 放假,5/9(六) 补班
|
|
48
|
+
{ date: '2026-05-01', isOffDay: true },
|
|
49
|
+
{ date: '2026-05-02', isOffDay: true },
|
|
50
|
+
{ date: '2026-05-03', isOffDay: true },
|
|
51
|
+
{ date: '2026-05-04', isOffDay: true },
|
|
52
|
+
{ date: '2026-05-05', isOffDay: true },
|
|
53
|
+
{ date: '2026-05-09', isOffDay: false },
|
|
54
|
+
// 端午:6/19-6/21 放假,无补班
|
|
55
|
+
{ date: '2026-06-19', isOffDay: true },
|
|
56
|
+
{ date: '2026-06-20', isOffDay: true },
|
|
57
|
+
{ date: '2026-06-21', isOffDay: true },
|
|
58
|
+
// 中秋:9/25-9/27 放假,9/20(日) 补班
|
|
59
|
+
{ date: '2026-09-20', isOffDay: false },
|
|
60
|
+
{ date: '2026-09-25', isOffDay: true },
|
|
61
|
+
{ date: '2026-09-26', isOffDay: true },
|
|
62
|
+
{ date: '2026-09-27', isOffDay: true },
|
|
63
|
+
// 国庆:10/1-10/7 放假,10/10(六) 补班
|
|
64
|
+
{ date: '2026-10-01', isOffDay: true },
|
|
65
|
+
{ date: '2026-10-02', isOffDay: true },
|
|
66
|
+
{ date: '2026-10-03', isOffDay: true },
|
|
67
|
+
{ date: '2026-10-04', isOffDay: true },
|
|
68
|
+
{ date: '2026-10-05', isOffDay: true },
|
|
69
|
+
{ date: '2026-10-06', isOffDay: true },
|
|
70
|
+
{ date: '2026-10-07', isOffDay: true },
|
|
71
|
+
{ date: '2026-10-10', isOffDay: false },
|
|
72
|
+
]),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/** 内置快照数组(新定稿年份在此追加)。 */
|
|
76
|
+
export const BUILTIN_TABLES = Object.freeze([BUILTIN_2026])
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 校验并归一化 holiday-cn `days` 数组。
|
|
80
|
+
* 逐条要求 `date` 为补零 'YYYY-MM-DD' 且 `isOffDay` 为布尔;
|
|
81
|
+
* 排序去重(同日重复以末条为准,与上游「后公告覆盖前公告」一致)。
|
|
82
|
+
* @param {unknown} raw days 原始数组
|
|
83
|
+
* @returns {{date:string, isOffDay:boolean}[]} 归一化条目(新数组)
|
|
84
|
+
* @throws {Error} 非数组 / 空条目 / 形态非法
|
|
85
|
+
*/
|
|
86
|
+
export function normalizeDays(raw) {
|
|
87
|
+
if (!Array.isArray(raw)) throw new Error('holiday days: not an array')
|
|
88
|
+
const byDate = new Map()
|
|
89
|
+
for (const item of raw) {
|
|
90
|
+
const date = item && typeof item === 'object' ? item.date : undefined
|
|
91
|
+
if (typeof date !== 'string' || !DATE_RE.test(date)) {
|
|
92
|
+
throw new Error(`holiday days: bad date ${JSON.stringify(date)}`)
|
|
93
|
+
}
|
|
94
|
+
if (typeof item.isOffDay !== 'boolean') {
|
|
95
|
+
throw new Error(`holiday days: bad isOffDay for ${date}`)
|
|
96
|
+
}
|
|
97
|
+
byDate.set(date, { date, isOffDay: item.isOffDay })
|
|
98
|
+
}
|
|
99
|
+
return [...byDate.values()].sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 解析 holiday-cn 全年 JSON(远端响应 / 缓存文件通吃)。
|
|
104
|
+
* @param {unknown} json 已 parse 的对象
|
|
105
|
+
* @returns {{year:number, days:{date:string, isOffDay:boolean}[]}}
|
|
106
|
+
* @throws {Error} 形态不合 schema(year 与条目日期必须一致)
|
|
107
|
+
*/
|
|
108
|
+
export function parseHolidayCn(json) {
|
|
109
|
+
if (json === null || typeof json !== 'object') throw new Error('holiday json: not an object')
|
|
110
|
+
const year = Number(json.year)
|
|
111
|
+
if (!Number.isInteger(year) || year < 2000 || year > 2100) throw new Error('holiday json: bad year')
|
|
112
|
+
const days = normalizeDays(json.days)
|
|
113
|
+
for (const d of days) {
|
|
114
|
+
if (Number(d.date.slice(0, 4)) !== year) throw new Error(`holiday json: day ${d.date} outside ${year}`)
|
|
115
|
+
}
|
|
116
|
+
return { year, days }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 修正表指纹:对归一化(排序去重)后的 `date=isOffDay` 序列做 sha256。
|
|
121
|
+
* 只哈希判定语义相关字段——name / papers / 抓取时间不参与,
|
|
122
|
+
* 上游改文案不会引起伪变化。
|
|
123
|
+
* @param {{date:string, isOffDay:boolean}[]} days normalizeDays / parseHolidayCn 产物
|
|
124
|
+
* @returns {string} 'sha256:<hex>'
|
|
125
|
+
*/
|
|
126
|
+
export function fingerprint(days) {
|
|
127
|
+
const canon = days.map((d) => `${d.date}=${d.isOffDay ? 1 : 0}`).join(';')
|
|
128
|
+
return `sha256:${createHash('sha256').update(canon).digest('hex')}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 归一化 days → 查表 Map:'YYYY-MM-DD' → 是否放假日。
|
|
133
|
+
* @param {{date:string, isOffDay:boolean}[]} days
|
|
134
|
+
* @returns {Map<string, boolean>}
|
|
135
|
+
*/
|
|
136
|
+
export function buildMap(days) {
|
|
137
|
+
const m = new Map()
|
|
138
|
+
for (const d of days) m.set(d.date, d.isOffDay)
|
|
139
|
+
return m
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** wallClock 分量 → 补零 'YYYY-MM-DD'。 */
|
|
143
|
+
export function ymdOf(wc) {
|
|
144
|
+
return `${wc.year}-${String(wc.month).padStart(2, '0')}-${String(wc.day).padStart(2, '0')}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 修正表感知的「当天是否休息日」:
|
|
149
|
+
* - 表内条目:`isOffDay` 直接定乾坤(true 放假 / false 调休补班——周末也算上班);
|
|
150
|
+
* - 表外 / 无表(map 为 null 或空):回退自然周末(0=周日、6=周六)。
|
|
151
|
+
* @param {Map<string, boolean>|null|undefined} map buildMap 产物
|
|
152
|
+
* @param {string} ymd 补零日期串
|
|
153
|
+
* @param {number} weekday 该日期在用户时区的星期(0-6,同 wallClock 约定)
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
export function isRestDay(map, ymd, weekday) {
|
|
157
|
+
if (map instanceof Map && map.has(ymd)) return map.get(ymd) === true
|
|
158
|
+
return weekday === 0 || weekday === 6
|
|
159
|
+
}
|
package/src/index.js
CHANGED
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 高峰自动会话门:
|
|
5
5
|
* - 每 30s tick 判定状态(NORMAL ↔ PAUSED_PEAK),纯状态机见 scheduler.js。
|
|
6
|
-
* -
|
|
6
|
+
* - 入峰(且非周末/休息日):对所有 running root session 调 gate.stopNextTurn
|
|
7
7
|
* (有 taskControl → 会话门 safe+wait;无 → 回退锁等待队列)。
|
|
8
|
-
* -
|
|
8
|
+
* - 退峰/休息日:gate.resume 全部。
|
|
9
|
+
* - 法定假/调休修正表(holidayMode):内置快照 + 启动校验缓存指纹 + 按年自动
|
|
10
|
+
* 抓取 holiday-cn(holiday-store.js);法定假视同周末,调休补班周末按工作日。
|
|
9
11
|
* - `ctx.provide('sessionGuard')` 冗余端口,input-traffic 冻结按钮透传接入。
|
|
10
12
|
* - 设置:设置 → 插件 → session-guard 子板块,简单开关(enabled / weekendMode / queueFallback)。
|
|
11
13
|
* schemastery 为常规 dependency,设置经 `ctx.inject(['settings'])` 本地接口注册(src/settings.js,
|
|
12
14
|
* 对齐 dsh-thinking-levels;不 value-import dsh-settings);设置服务缺失时 fail-open 用默认配置照常运行。
|
|
13
15
|
*/
|
|
14
16
|
import { computeState, transition } from './scheduler.js'
|
|
15
|
-
import { wallClock,
|
|
17
|
+
import { wallClock, isRestForSettings, isInPeak, BILLING_TIMEZONE } from './time.js'
|
|
18
|
+
import { createHolidayStore } from './holiday-store.js'
|
|
16
19
|
import { createStore } from './store.js'
|
|
17
20
|
import { createGate } from './gate.js'
|
|
18
21
|
import { createBridge } from './bridge.js'
|
|
@@ -43,6 +46,21 @@ export function apply(ctx, config = {}) {
|
|
|
43
46
|
const store = createStore()
|
|
44
47
|
let lastState = null
|
|
45
48
|
|
|
49
|
+
// ── 法定假/调休修正表:内置快照打底,启动校验缓存指纹,按年自动抓取 ──
|
|
50
|
+
// holidayMap 只在 holidayMode 开启时挂进 cfg(readCfg);消费方(time.shouldPause
|
|
51
|
+
// 及其全部调用链)无感知:表缺席即退回自然周末。fail-open:抓取/解析失败只
|
|
52
|
+
// warn,不阻断 tick / 暂停主链路。DSH_SESSION_GUARD_HOLIDAY_FETCH=0 可断网
|
|
53
|
+
// (单测/离线环境):退化为纯内置 + 已缓存,不发起任何请求。
|
|
54
|
+
const holidays = createHolidayStore({
|
|
55
|
+
logger: ctx.logger,
|
|
56
|
+
fetchImpl: process.env.DSH_SESSION_GUARD_HOLIDAY_FETCH === '0' ? null : globalThis.fetch,
|
|
57
|
+
})
|
|
58
|
+
try {
|
|
59
|
+
holidays.loadAll() // 同步:内置 + 缓存(含启动指纹校验)
|
|
60
|
+
} catch (e) {
|
|
61
|
+
ctx.logger?.warn?.(`[session-guard] holiday load failed (weekend-only fallback): ${String(e)}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
/** 读实时设置(settings 服务不可用时回退默认)。 */
|
|
47
65
|
let configSource = config
|
|
48
66
|
/** 组合条目快照:volatile 字段解引,供默认值合并。 */
|
|
@@ -56,7 +74,10 @@ export function apply(ctx, config = {}) {
|
|
|
56
74
|
function readCfg() {
|
|
57
75
|
try {
|
|
58
76
|
const v = resolveEntryConfig()
|
|
59
|
-
|
|
77
|
+
const cfg = v && typeof v === 'object' ? { ...DEFAULT_SETTINGS, ...v } : { ...DEFAULT_SETTINGS }
|
|
78
|
+
// 修正表活引用:holidayMode 开启才挂;每次重组装取最新合并表(抓取更新后自然生效)
|
|
79
|
+
if (cfg.holidayMode === true) cfg.holidayMap = holidays.map()
|
|
80
|
+
return cfg
|
|
60
81
|
} catch {
|
|
61
82
|
return { ...DEFAULT_SETTINGS }
|
|
62
83
|
}
|
|
@@ -273,6 +294,15 @@ export function apply(ctx, config = {}) {
|
|
|
273
294
|
ctx.effect(() => ctx.timer.interval(tick, 30_000), 'session-guard: tick')
|
|
274
295
|
tick()
|
|
275
296
|
|
|
297
|
+
// ── 修正表自动刷新:启动后立即核对一次,之后每小时 ensureFresh ──
|
|
298
|
+
// 当年 +(11/12 月)次年;已有表 24h 内不重抓,指纹一致不写盘(见 holiday-store.js)。
|
|
299
|
+
const holidayRefresh = () => {
|
|
300
|
+
void holidays.ensureFresh({ autoFetch: readCfg().holidayAutoFetch === true })
|
|
301
|
+
.catch((e) => ctx.logger?.warn?.(`[session-guard] holiday refresh failed: ${String(e)}`))
|
|
302
|
+
}
|
|
303
|
+
ctx.effect(() => ctx.timer.interval(holidayRefresh, 3_600_000), 'session-guard: holiday refresh')
|
|
304
|
+
holidayRefresh()
|
|
305
|
+
|
|
276
306
|
// ── HTTP 路由 ──
|
|
277
307
|
// 0.1.5 compat:注册失败只记日志,不让 apply 抛错炸掉宿主插件加载(#5926 教训)。
|
|
278
308
|
// webServer.register 前缀路由 + SSE 在 0.1.5 契约不变(packages/host/webserver WebRoute)。
|
|
@@ -353,19 +383,21 @@ export function apply(ctx, config = {}) {
|
|
|
353
383
|
}
|
|
354
384
|
// GET /session-guard/status —— 全局当前阶段(状态徽标轮询用)
|
|
355
385
|
// 峰谷判定:固定北京时间(BILLING_TIMEZONE),与 DeepSeek 官方计费一致
|
|
356
|
-
//
|
|
386
|
+
// 周末/休息日判定:用配置时区;holidayMode 开启时查修正表(法定假=休,补班周末=工作)
|
|
357
387
|
if (method === 'GET' && url.pathname === '/session-guard/status') {
|
|
358
388
|
const cfg = readCfg()
|
|
359
389
|
const now = new Date()
|
|
360
390
|
const wcUser = wallClock(cfg.timezone, now)
|
|
361
391
|
const wcBilling = wallClock(BILLING_TIMEZONE, now)
|
|
362
|
-
const weekend =
|
|
392
|
+
const weekend = isRestForSettings(cfg, wcUser)
|
|
363
393
|
const peak = cfg.enabled && !weekend && isInPeak(wcBilling, cfg.peakWindows || [])
|
|
364
394
|
return json(200, {
|
|
365
395
|
ok: true,
|
|
366
396
|
status: {
|
|
367
397
|
phase: weekend ? 'weekend' : peak ? 'peak' : 'off-peak',
|
|
368
398
|
weekend,
|
|
399
|
+
holidayMode: cfg.holidayMode === true,
|
|
400
|
+
holidays: holidays.info(),
|
|
369
401
|
peak,
|
|
370
402
|
state: lastState,
|
|
371
403
|
enabled: cfg.enabled,
|
package/src/settings.js
CHANGED
|
@@ -20,7 +20,9 @@ export const NS = 'session-guard'
|
|
|
20
20
|
export const DEFAULT_SETTINGS = Object.freeze({
|
|
21
21
|
enabled: true, // 高峰自动处理开关(简单开关)
|
|
22
22
|
offPeakAutoResume: true, // 低谷自动恢复开关:低峰时段自动恢复被高峰暂停的会话
|
|
23
|
-
weekendMode: true, //
|
|
23
|
+
weekendMode: true, // 周末模式开关:识别周末/休息日,无视峰谷(简单开关)
|
|
24
|
+
holidayMode: true, // 法定节假日修正表:法定假视同周末,调休补班周末按工作日(简单开关)
|
|
25
|
+
holidayAutoFetch: true, // 按年自动抓取 holiday-cn 修正表(关=只用内置+已缓存;启动仍校验缓存指纹)
|
|
24
26
|
timezone: 'Asia/Shanghai',
|
|
25
27
|
peakWindows: [
|
|
26
28
|
{ start: '09:00', end: '12:00' },
|
|
@@ -57,6 +59,8 @@ export const SettingsSchema = z.object({
|
|
|
57
59
|
enabled: z.boolean().default(DEFAULT_SETTINGS.enabled).volatile(),
|
|
58
60
|
offPeakAutoResume: z.boolean().default(DEFAULT_SETTINGS.offPeakAutoResume).volatile(),
|
|
59
61
|
weekendMode: z.boolean().default(DEFAULT_SETTINGS.weekendMode).volatile(),
|
|
62
|
+
holidayMode: z.boolean().default(DEFAULT_SETTINGS.holidayMode).volatile(),
|
|
63
|
+
holidayAutoFetch: z.boolean().default(DEFAULT_SETTINGS.holidayAutoFetch).volatile(),
|
|
60
64
|
timezone: z.string().default(DEFAULT_SETTINGS.timezone).volatile(),
|
|
61
65
|
peakWindows: z
|
|
62
66
|
.array(z.object({ start: z.string(), end: z.string() }))
|
package/src/time.js
CHANGED
|
@@ -6,11 +6,16 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 周末判定基于用户配置时区(settings.timezone),因为"周末"是用户本地概念。
|
|
8
8
|
*
|
|
9
|
+
* 修正表(settings.holidayMap,holiday-cn 语义)存在时,"周末"升级为
|
|
10
|
+
* "休息日":法定节假日(含落在工作日的)视同周末;调休补班周末
|
|
11
|
+
* 视同工作日(峰谷窗口照常生效)。无表时退回纯自然周末。
|
|
12
|
+
*
|
|
9
13
|
* 两者都通过 `Intl.DateTimeFormat(timeZone)` 投影,避免裸 `getUTCDay()`
|
|
10
14
|
* 导致北京周末边界错 8 小时的 bug。
|
|
11
15
|
*
|
|
12
16
|
* 高峰时段为左闭右开 [start, end);跨午夜窗口(start > end)安全。
|
|
13
17
|
*/
|
|
18
|
+
import { isRestDay, ymdOf } from './holidays.js'
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* DeepSeek 峰谷计费基准时区(固定北京时间 UTC+8)。
|
|
@@ -67,6 +72,18 @@ export function isWeekend(weekday) {
|
|
|
67
72
|
return weekday === 0 || weekday === 6
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
/**
|
|
76
|
+
* 用户时区"当天是否休息日":holidayMap 存在则查修正表(含调休补班),
|
|
77
|
+
* 缺省退回自然周末。settings.holidayMap 由 host 装配层(readCfg)挂载,
|
|
78
|
+
* holidayMode 关闭时不挂——本函数只看表的存在性。
|
|
79
|
+
* @param {object} settings 同 shouldPause
|
|
80
|
+
* @param {{year:number,month:number,day:number,weekday:number}} wc wallClock 分量(用户时区)
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
export function isRestForSettings(settings, wc) {
|
|
84
|
+
return isRestDay(settings.holidayMap, ymdOf(wc), wc.weekday)
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
/** 某墙钟分钟是否处于任一高峰窗口内。 */
|
|
71
88
|
export function isInPeak(wc, windows) {
|
|
72
89
|
return windows.some((w) => {
|
|
@@ -80,19 +97,20 @@ export function isInPeak(wc, windows) {
|
|
|
80
97
|
/**
|
|
81
98
|
* 主判定:此刻是否应触发高峰暂停。
|
|
82
99
|
*
|
|
83
|
-
* -
|
|
84
|
-
*
|
|
100
|
+
* - 周末/休息日判定:使用 settings.timezone(用户本地时区),周末是用户本地概念;
|
|
101
|
+
* holidayMap 存在时升级为修正表语义(法定假=休,调休补班周末=工作);
|
|
102
|
+
* - 峰谷判定:固定使用 BILLING_TIMEZONE(北京时间),与 DeepSeek 官方计费一致。
|
|
85
103
|
*
|
|
86
|
-
* @param {object} settings { enabled, weekendMode, timezone, peakWindows }
|
|
104
|
+
* @param {object} settings { enabled, weekendMode, timezone, peakWindows, holidayMap? }
|
|
87
105
|
* @param {Date} date
|
|
88
106
|
* @returns {{pause:boolean, reason:string}}
|
|
89
|
-
* reason: 'disabled' | 'weekend' | 'peak' | 'off-peak'
|
|
107
|
+
* reason: 'disabled' | 'weekend'(休息日,含法定假) | 'peak' | 'off-peak'
|
|
90
108
|
*/
|
|
91
109
|
export function shouldPause(settings, date) {
|
|
92
110
|
if (!settings || settings.enabled !== true) return { pause: false, reason: 'disabled' }
|
|
93
|
-
//
|
|
111
|
+
// 周末/休息日判定:用用户配置时区(用户本地的周末)
|
|
94
112
|
const wcUser = wallClock(settings.timezone, date)
|
|
95
|
-
if (settings.weekendMode === true &&
|
|
113
|
+
if (settings.weekendMode === true && isRestForSettings(settings, wcUser)) {
|
|
96
114
|
return { pause: false, reason: 'weekend' }
|
|
97
115
|
}
|
|
98
116
|
// 峰谷判定:固定北京时间(DeepSeek 官方计费基准)
|
|
@@ -127,7 +145,8 @@ export function localMidnight(tz, year, month, day) {
|
|
|
127
145
|
* 从 `date` 出发、下一个「确定非峰」的时刻;找不到返回 null。
|
|
128
146
|
* 取两个候选的最小值:
|
|
129
147
|
* (a) 当前所在高峰窗口的结束时刻(计费时区,跨午夜安全);
|
|
130
|
-
* (b)
|
|
148
|
+
* (b) 下一个休息日的本地 00:00(周末/法定假整天非峰,仅 weekendMode 开启时;
|
|
149
|
+
* holidayMap 存在时同步识别法定假起点与调休补班周末)。
|
|
131
150
|
* @param {object} settings
|
|
132
151
|
* @param {Date} date 已知处于高峰
|
|
133
152
|
* @returns {number|null}
|
|
@@ -154,9 +173,11 @@ function nextOffPeakInstant(settings, date) {
|
|
|
154
173
|
}
|
|
155
174
|
if (settings.weekendMode === true) {
|
|
156
175
|
const wcUser = wallClock(settings.timezone, date)
|
|
157
|
-
|
|
176
|
+
// 往后找第一个"休息日"(自然周末或法定假);上限 14 天覆盖长假前的补班周末。
|
|
177
|
+
for (let k = 1; k <= 14; k++) {
|
|
158
178
|
const day = new Date(Date.UTC(wcUser.year, wcUser.month - 1, wcUser.day + k))
|
|
159
|
-
|
|
179
|
+
const ymd = `${day.getUTCFullYear()}-${String(day.getUTCMonth() + 1).padStart(2, '0')}-${String(day.getUTCDate()).padStart(2, '0')}`
|
|
180
|
+
if (!isRestDay(settings.holidayMap, ymd, day.getUTCDay())) continue
|
|
160
181
|
candidates.push(localMidnight(settings.timezone, day.getUTCFullYear(), day.getUTCMonth() + 1, day.getUTCDate()))
|
|
161
182
|
break
|
|
162
183
|
}
|