dsh-manage-session 0.2.3 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.ja.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # 変更履歴
2
2
 
3
+ ## 0.4.8
4
+
5
+ ### 修正 —— ST1:変換が第一-party プロデューサー名でホストと食い違う問題
6
+
7
+ - `migrateSessionSourceKind` は行ごとに `isFirstPartyLegacyProducer` を照合(ホスト
8
+ `dsh-session-format-v3-to-v4@0.1.7-rc.2` lib/index.js:51-84 の2表を一字一句収録、
9
+ 実測 5+25=**30** 名)。該当行はスキップし `skippedFirstParty` で正直に報告——
10
+ 第一-party 名はホストの改名表/同名 kind(role 敏感分岐含む)で処理されるため、
11
+ 無条件の接頭辞付与はホストが生成しない kind を書き出し、帰属を静かに壊します。
12
+ 第一方旧行のみのログは書き込まない。30 名対照テストとホスト公式デコーダ読み返し
13
+ 検証を追加(161 テスト全緑)。
14
+
15
+ ### 強化 —— R2:purge 側が署名変換バックアップを認識
16
+
17
+ - 統計とクリーンアップの両経路で `*.pre-sourcemigrate-<ts>` バックアップを認識:
18
+ 統計は行ごとに `backupBytes`/`backupCount` を追加(`bytes` の部分集合、単列で
19
+ 誤読防止)、クリーンアップ結果は `backupsRemoved` を追加。回帰 6 件を追加、
20
+ 全 167 テスト。
21
+
3
22
  ## 0.1.0-alpha.6
4
23
 
5
24
  ### 修正 —— アイコンとラベルの間隔を詰め、狭いサイドバーで隣を潰さない
package/CHANGELOG.ko.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # 변경 이력
2
2
 
3
+ ## 0.4.8
4
+
5
+ ### 수정 —— ST1:변환이 제1자 프로듀서명에서 호스트와 불일치
6
+
7
+ - `migrateSessionSourceKind`는 행마다 `isFirstPartyLegacyProducer`를 조회(호스트
8
+ `dsh-session-format-v3-to-v4@0.1.7-rc.2` lib/index.js:51-84의 두 표를 그대로 수록,
9
+ 실측 5+25=**30**명). 해당 행은 건너뛰고 `skippedFirstParty`로 정직하게 보고——
10
+ 제1자명은 호스트의 개명표/동명 kind(role 민감 분기 포함)으로 처리되므로 무조건
11
+ 접두사를 붙이면 호스트가 생성하지 않는 kind를 기록해 귀속이 조용히 손상됩니다.
12
+ 제1자 구형 행만 있는 로그는 기록하지 않음. 30명 대조 테스트와 호스트 공식
13
+ 디코더 재독 단언 추가(161 테스트 전부 통과).
14
+
15
+ ### 강화 —— R2:purge 측 서명 변환 백업 인식
16
+
17
+ - 통계와 정리 양 경로에서 `*.pre-sourcemigrate-<ts>` 백업을 인식: 통계는 행마다
18
+ `backupBytes`/`backupCount` 추가(`bytes`의 부분집합, 단열로 오독 방지),
19
+ 정리 결과에 `backupsRemoved` 추가. 회귀 6건 추가, 전체 167 테스트.
20
+
3
21
  ## 0.1.0-alpha.6
4
22
 
5
23
  ### 수정 — 아이콘과 레이블 간격 축소, 좁은 사이드바에서 이웃을 밀어내지 않음
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.8
4
+
5
+ ### 修复 —— ST1:转换对第一方生产者名与宿主分叉
6
+
7
+ - `migrateSessionSourceKind` 逐行查 `isFirstPartyLegacyProducer`(两张表逐字照录宿主
8
+ `dsh-session-format-v3-to-v4@0.1.7-rc.2` lib/index.js:51-84,实测 5+25=**30** 个第一方名),
9
+ 命中即跳过该行并以 `skippedFirstParty` 如实上报——第一方名走宿主改名表/同名裸 kind
10
+ (含 role 敏感分支),无条件加前缀会写出宿主永不产出的 kind,静默损坏归因。
11
+ 只含第一方旧行时不写盘。新增 30 名对照测试与宿主官方解码器读回断言(161 测试全绿)。
12
+
13
+ ### 加固 —— R2:purge 侧识别署名转换备份
14
+
15
+ - 归档统计与清理两路识别 `*.pre-sourcemigrate-<ts>` 备份(后缀常量
16
+ `SOURCE_MIGRATE_BACKUP_SUFFIX` 已在 0.4.7 导出,本次接线):
17
+ - **统计**:`locateSessionUsage` / `session-history-list` 每行新增 `backupBytes` /
18
+ `backupCount`——备份是 `bytes` 的子集,单列出来免得被当作日志体积误读;
19
+ - **清理**:`session-history-purge` 结果新增 `backupsRemoved`(整目录删除本就连带
20
+ 备份,此处置个数让「备份一并处置」如实上报,不再是黑箱)。
21
+ - 新增 6 条回归(后缀判定、统计单列、如实 0、清理上报、连带保留、列表带出),全套 167 测试。
22
+
23
+ ## 0.4.7
24
+
25
+ ### 新增 —— 插件署名格式体检门(source-kind)与旧署名转换
26
+
27
+ - **背景**:宿主 0.1.7-rc.1 起(会话格式 v4)拒绝裸 `source: { kind: 'plugin', plugin }` 署名
28
+ (SessionFormatError,整轮失败);genui 之外的未适配插件(session-guard、prime-memory 等)
29
+ 已按清单修写路径,但存量日志里由它们写入的旧署名行仍在——未适配插件的读回判断会失配。
30
+ - **检测(只读)**:体检新增第 6 门 `source-kind`,按日志 header 的格式代次分线:
31
+ - **版本依赖表**(详见 `src/host/health/source-kind.ts` 头注):v1/v2 早期线(未考证)与
32
+ v3(0.1.6 时代,待考证)旧行合法、不动(宿主 v3→v4 迁移负责);**v4(≥0.1.7-rc.1)拒绝
33
+ 旧署名**,观测到即 warn。
34
+ - v4 之前的版本线记 ok 不抬档——不越权处理宿主迁移契约内的数据(防过度操作)。
35
+ - **转换(显式触发)**:新增 API `session-health-source-migrate` 与体检面板「转换旧署名」按钮
36
+ (仅 source-kind 门 warn 时出现)。把 `{ kind: 'plugin', plugin: N }` 改写为
37
+ `{ kind: 'plugin:N' }`(删除 plugin 字段),其余字节原样保留。
38
+ - **红线三闸**:只动 source 署名字段;改前整文件备份(`*.pre-sourcemigrate-<ts>`,可回退);
39
+ 完整性不合格(撕裂尾帧/解码失败/seq 缺口)或 header.version < 4 时拒绝执行。这是
40
+ 「禁止改写会话日志」红线的唯一显式例外,经用户逐会话触发。
41
+ - genui 不在掌控范围,本插件不做也不会替它转换(其历史行转换后由其读回判断自行失配与否自负)。
42
+
3
43
  ## 0.1.0-alpha.6
4
44
 
5
45
  ### 修复 —— 图标与文案间距收紧;窄栏下不再挤掉邻居
package/README.en.md CHANGED
@@ -10,6 +10,20 @@ A DSH web plugin for **session history files** and **session health checks**. It
10
10
  > Naming boundary: **this package ships no search or index features**. Search/indexing belongs to
11
11
  > `dsh-search-index`; neither side mentions or re-interprets the other's fields.
12
12
 
13
+ ## Screenshots
14
+
15
+ A **Session Steward** entry sits next to Search and Settings at the bottom of the sidebar:
16
+
17
+ ![Sidebar entry](assets/left-sidebar.png)
18
+
19
+ **Retirement Home (history files)** — the official archive set, each row carrying its size and whether the on-disk entity still exists; select rows to un-archive or purge:
20
+
21
+ ![Retirement Home](assets/archive.png)
22
+
23
+ **Checkup** — the entry point of the four-gate checkup, which ends in a reversible prescription:
24
+
25
+ ![Checkup](assets/doctor.png)
26
+
13
27
  ## Install
14
28
 
15
29
  ```bash
package/README.ja.md CHANGED
@@ -10,6 +10,20 @@
10
10
  > 命名の境界:**本パッケージは検索・インデックス機能を一切提供しません**。検索/インデックスは
11
11
  > `dsh-search-index` に属し、双方が相手のフィールドに言及したり再解釈したりすることはありません。
12
12
 
13
+ ## スクリーンショット
14
+
15
+ サイドバー下部に **「会话管家」**(セッション・スチュワード)のエントリが追加され、検索・設定と並びます:
16
+
17
+ ![サイドバーのエントリ](assets/left-sidebar.png)
18
+
19
+ **カルテ室(履歴ファイル)** — 公式アーカイブ集合の一覧。各行にサイズとディスク上の実体の有無を表示し、選択してアーカイブ解除または整理ができます:
20
+
21
+ ![カルテ室](assets/archive.png)
22
+
23
+ **健診** — 四つのゲートによる健診の入口。スキャン後に可逆的な処方を提示します:
24
+
25
+ ![健診](assets/doctor.png)
26
+
13
27
  ## インストール
14
28
 
15
29
  ```bash
package/README.ko.md CHANGED
@@ -9,6 +9,20 @@
9
9
  > 이름 경계: **이 패키지는 검색이나 인덱스 기능을 제공하지 않습니다**. 검색/인덱싱은
10
10
  > `dsh-search-index`에 속하며, 어느 쪽도 상대방의 필드를 언급하거나 재해석하지 않습니다.
11
11
 
12
+ ## 스크린샷
13
+
14
+ 사이드바 하단에 **「会话管家」**(세션 스튜어드) 항목이 추가되어 검색, 설정과 나란히 표시됩니다:
15
+
16
+ ![사이드바 항목](assets/left-sidebar.png)
17
+
18
+ **기록 보관소(히스토리 파일)** — 공식 아카이브 집합 목록. 각 행에 용량과 디스크 실체 존재 여부를 표시하며, 선택하여 아카이브 해제 또는 정리를 할 수 있습니다:
19
+
20
+ ![기록 보관소](assets/archive.png)
21
+
22
+ **점검** — 네 개의 gate 점검 입구이며, 검사 후 되돌릴 수 있는 처방을 제시합니다:
23
+
24
+ ![점검](assets/doctor.png)
25
+
12
26
  ## 설치
13
27
 
14
28
  ```bash
package/README.md CHANGED
@@ -28,6 +28,20 @@ DSH Web 插件:**会话历史文件 + 会话健康检查**。
28
28
  > 命名边界:**本插件不提供搜索与索引**。搜索/独立索引属于另一片(`dsh-search-index`),
29
29
  > 两侧互不出现对方的子域字眼,也不互相解释对方字段。
30
30
 
31
+ ## 界面预览
32
+
33
+ 侧边栏底部新增 **「会话管家」** 入口,与搜索、设置并列:
34
+
35
+ ![侧边栏入口](assets/left-sidebar.png)
36
+
37
+ **养老院(会话历史文件)**——官方归档集合一览,每条给出体积与磁盘实体状态;勾选后可取消归档或清理:
38
+
39
+ ![养老院(全部收口状态)](assets/archive.png)
40
+
41
+ **体检**——四门体检的入口,扫完给出可逆处方:
42
+
43
+ ![体检](assets/doctor.png)
44
+
31
45
  ## 安装
32
46
 
33
47
  ```bash
package/dsh.plugin.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "id": "dsh-manage-session",
3
3
  "name": "dsh-manage-session",
4
- "version": "0.2.3",
4
+ "version": "0.4.1",
5
5
  "description": {
6
- "en": "Session Steward: browse and bulk-prune the official session archive set, and run a four-gate session health checkup that ends in a reversible, backup-first prescription. Never rewrites session logs.",
7
- "zh": "会话管家:浏览并批量清理官方会话归档集合,并对会话做四门体检,最终给出可逆、先备份的处方。不改写会话日志。"
6
+ "en": "Session Steward (DSH 0.1.5 line): browse and bulk-prune the official session archive set, and run a four-gate session health checkup that ends in a reversible, backup-first prescription. Never rewrites session logs.",
7
+ "zh": "会话管家(DSH 0.1.5 专线):浏览并批量清理官方会话归档集合,并对会话做四门体检,最终给出可逆、先备份的处方。不改写会话日志。"
8
8
  },
9
9
  "homepage": "https://github.com/drscrewdriver/dsh-manage-session",
10
10
  "repository": {
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "license": "MIT",
15
15
  "engines": {
16
- "dsh": ">=0.1.2-rc.1 <0.1.3-0"
16
+ "dsh": ">=0.1.7-rc.1 <0.1.8-0"
17
17
  },
18
18
  "components": {
19
19
  "host": "lib/index.js",
package/lib/client.js CHANGED
@@ -31,6 +31,7 @@ window.__ModuleLoader__.load({
31
31
  "card.health.desc": "体检 → 处方 → 出院。关闭后不注册体检接口、不渲染该页签。",
32
32
  "card.readonly": "当前会话无法修改设置(只读挂载)。",
33
33
  "card.unavailable": "设置服务不可用,无法读取配置。",
34
+ "family.title": "起子插件设置",
34
35
  "panel.title": "会话管家",
35
36
  "panel.tab.history": "养老院",
36
37
  "panel.tab.health": "体检",
@@ -64,15 +65,18 @@ window.__ModuleLoader__.load({
64
65
  "health.scanning": "体检中…",
65
66
  "health.progress": "{done}/{total} · 已用 {sec}s",
66
67
  "health.empty": "未发现异常会话。",
67
- "health.clean": "四门全绿。",
68
+ "health.clean": "全部检查通过。",
68
69
  "health.level.ok": "正常",
69
70
  "health.level.warn": "注意",
70
71
  "health.level.skipped": "未检查",
71
72
  "health.level.fail": "异常",
73
+ "health.priority.high": "优先",
74
+ "health.gate.generation": "代次产物",
72
75
  "health.gate.log-integrity": "日志完整性",
73
76
  "health.gate.projection-cache": "投影缓存",
74
77
  "health.gate.lossless-json": "无损 JSON",
75
78
  "health.gate.cold-read": "可接续性",
79
+ "health.gate.source-kind": "插件署名",
76
80
  "health.attribution": "归属",
77
81
  "health.field": "字段",
78
82
  "health.unknownOwner": "归属未知",
@@ -81,6 +85,11 @@ window.__ModuleLoader__.load({
81
85
  "health.phase.discharge": "③ 出院",
82
86
  "health.repair": "执行可逆处置",
83
87
  "health.repairing": "处置中…",
88
+ "health.sourceMigrate": "转换旧署名",
89
+ "health.sourceMigrating": "转换中…",
90
+ "health.sourceMigrated": "已转换 {n} 行旧署名(备份:{backup})",
91
+ "health.sourceMigrateNone": "没有需要转换的旧署名行",
92
+ "health.sourceMigrateFail": "转换失败:{error}",
84
93
  "health.before": "处置前",
85
94
  "health.after": "处置后",
86
95
  "health.quarantined": "已隔离投影缓存记录(重启 DSH 后重折叠)",
@@ -109,6 +118,7 @@ window.__ModuleLoader__.load({
109
118
  "card.health.desc": "Checkup → Prescription → Discharge. When off, health routes and the tab are gone.",
110
119
  "card.readonly": "This session cannot edit settings (read-only mount).",
111
120
  "card.unavailable": "Settings service unavailable; configuration cannot be read.",
121
+ "family.title": "Screwdriver plugin settings",
112
122
  "panel.title": "Session Steward",
113
123
  "panel.tab.history": "Retirement Home",
114
124
  "panel.tab.health": "Checkup",
@@ -142,15 +152,18 @@ window.__ModuleLoader__.load({
142
152
  "health.scanning": "Running…",
143
153
  "health.progress": "{done}/{total} · {sec}s elapsed",
144
154
  "health.empty": "No problem session found.",
145
- "health.clean": "All four gates are green.",
155
+ "health.clean": "All checks passed.",
146
156
  "health.level.ok": "ok",
147
157
  "health.level.warn": "warn",
148
158
  "health.level.skipped": "Not checked",
149
159
  "health.level.fail": "fail",
160
+ "health.priority.high": "Priority",
161
+ "health.gate.generation": "Generation artifact",
150
162
  "health.gate.log-integrity": "Log integrity",
151
163
  "health.gate.projection-cache": "Projection cache",
152
164
  "health.gate.lossless-json": "Lossless JSON",
153
165
  "health.gate.cold-read": "Continuability",
166
+ "health.gate.source-kind": "Plugin signature",
154
167
  "health.attribution": "Owner",
155
168
  "health.field": "Field",
156
169
  "health.unknownOwner": "owner unknown",
@@ -159,6 +172,11 @@ window.__ModuleLoader__.load({
159
172
  "health.phase.discharge": "3) Discharge",
160
173
  "health.repair": "Apply reversible repair",
161
174
  "health.repairing": "Repairing…",
175
+ "health.sourceMigrate": "Migrate legacy signatures",
176
+ "health.sourceMigrating": "Migrating…",
177
+ "health.sourceMigrated": "Migrated {n} legacy signature rows (backup: {backup})",
178
+ "health.sourceMigrateNone": "No legacy signature rows to migrate",
179
+ "health.sourceMigrateFail": "Migration failed: {error}",
162
180
  "health.before": "Before",
163
181
  "health.after": "After",
164
182
  "health.quarantined": "Projection cache quarantined (refolds after a DSH restart)",
@@ -192,17 +210,14 @@ window.__ModuleLoader__.load({
192
210
  /**
193
211
  * 设置卡(插件配置行):一个可折叠抽屉 —— 会话历史文件 / 健康检查。
194
212
  *
195
- * **chrome 必须自己渲染**:DSH 的「插件」设置分区(`dsh-client-ui-settings-plugins`)
196
- * 只做一件事——按 settings 命名空间分发 `settings.plugin.item`:
213
+ * **chrome 必须自己渲染**:宿主不为贡献卡提供任何外壳,标题、说明、
214
+ * 展开/收起全归插件自己所有。早期版本只渲染裸行,与同分区其它插件
215
+ * 的抽屉形态不一致,还让 `card.title` / `card.desc` 成为死文案。
197
216
  *
198
- * renderSlot('settings.plugin.item', {}, { entryKey: ns })
199
- *
200
- * 它**不提供任何外壳**。所以卡片的标题、说明、展开/收起全归插件自己所有
201
- * (同分区里 search-index 等卡片都是这个形态)。早期版本只渲染了裸行,
202
- * 结果既与分区内其它插件的抽屉形态不一致,又让 `card.title` / `card.desc`
203
- * 变成定义了却没人渲染的死文案。
204
- *
205
- * 读写走 settingsScope 的显式 `set`(用户在控件上的直接操作,非隐式提交)。
217
+ * 读写走 configForms(`configForms.get(STEWARD_ENTRY_ID)`,以本插件 entry 为键;
218
+ * 旧宿主回退 settingsScope 绑定)的显式 `set`。0.1.7 起本卡挂在插件族共用
219
+ * 设置节(`dsh-family.tab`,dsh-thinking-levels 顶级「起子插件设置」节声明)里,
220
+ * 与同节其它抽屉一样默认展开。
206
221
  */
207
222
  /** 一行设置项。 */
208
223
  function Row(props) {
@@ -265,10 +280,10 @@ window.__ModuleLoader__.load({
265
280
  strokeLinejoin: "round"
266
281
  }))]);
267
282
  }
268
- /** 插件设置卡主体(抽屉)。默认收起,与「插件」分区内其它卡片一致。 */
283
+ /** 插件设置卡主体(抽屉)。默认展开,family 设置节内直接呈现完整面板。 */
269
284
  function StewardSettingsCard({ t, scope }) {
270
285
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
271
- const [open, setOpen] = (0, react.useState)(false);
286
+ const [open, setOpen] = (0, react.useState)(true);
272
287
  const value = snapshot.value ?? {};
273
288
  const writable = snapshot.writable;
274
289
  const rows = [];
@@ -669,16 +684,18 @@ window.__ModuleLoader__.load({
669
684
  //#endregion
670
685
  //#region src/client/health-panel.tsx
671
686
  /**
672
- * 体检面板:体检(四门)→ 处方(命令清单)→ 出院(可逆处置 + before/after 对照)。
687
+ * 体检面板:体检(五门)→ 处方(命令清单)→ 出院(可逆处置 + before/after 对照)。
673
688
  *
674
689
  * 三态流转完全由 host 侧报告驱动,客户端只做展示与触发;重型动作都在 host,
675
690
  * 渲染周期内不发同步重活。
676
691
  */
677
692
  const GATE_LABEL = {
693
+ "generation": "health.gate.generation",
678
694
  "log-integrity": "health.gate.log-integrity",
679
695
  "projection-cache": "health.gate.projection-cache",
680
696
  "lossless-json": "health.gate.lossless-json",
681
- "cold-read": "health.gate.cold-read"
697
+ "cold-read": "health.gate.cold-read",
698
+ "source-kind": "health.gate.source-kind"
682
699
  };
683
700
  const LEVEL_LABEL = {
684
701
  ok: "health.level.ok",
@@ -728,6 +745,9 @@ window.__ModuleLoader__.load({
728
745
  const [error, setError] = (0, react.useState)(null);
729
746
  const [detail, setDetail] = (0, react.useState)(null);
730
747
  const [repairing, setRepairing] = (0, react.useState)(false);
748
+ const [migrating, setMigrating] = (0, react.useState)(false);
749
+ /** 署名转换结果提示(一次性;点击转换或返回列表时清除)。 */
750
+ const [migrateNote, setMigrateNote] = (0, react.useState)(null);
731
751
  const [discharge, setDischarge] = (0, react.useState)(null);
732
752
  const [copied, setCopied] = (0, react.useState)(null);
733
753
  /** 分批扫描进度:done = 已访问会话数,total = 语料总数(0 表示尚未拿到分母)。 */
@@ -783,6 +803,7 @@ window.__ModuleLoader__.load({
783
803
  setError(null);
784
804
  setDetail(null);
785
805
  setDischarge(null);
806
+ setMigrateNote(null);
786
807
  setFindings(null);
787
808
  setCacheInfo(null);
788
809
  setProgress({
@@ -851,6 +872,7 @@ window.__ModuleLoader__.load({
851
872
  const openDetail = (sessionId) => {
852
873
  setError(null);
853
874
  setDischarge(null);
875
+ setMigrateNote(null);
854
876
  callHostAny("session-health-session", { sessionId }, 12e4).then((res) => {
855
877
  if (res.ok === true) {
856
878
  setDetail(res);
@@ -881,6 +903,28 @@ window.__ModuleLoader__.load({
881
903
  setCopied(text);
882
904
  } catch {}
883
905
  };
906
+ const runSourceMigrate = (sessionId) => {
907
+ setMigrating(true);
908
+ setError(null);
909
+ setMigrateNote(null);
910
+ callHostAny("session-health-source-migrate", { sessionId }, 12e4).then((res) => {
911
+ if (res.after !== void 0) {
912
+ setDetail({
913
+ ok: true,
914
+ report: res.after,
915
+ prescriptions: detail?.prescriptions ?? []
916
+ });
917
+ reconcileRow(res.after);
918
+ }
919
+ const changed = res.outcome?.changedRows ?? 0;
920
+ if (res.ok === true && changed > 0) setMigrateNote(translate(t, "health.sourceMigrated", {
921
+ n: changed,
922
+ backup: res.outcome?.backup ?? ""
923
+ }));
924
+ else if (res.ok === true) setMigrateNote(translate(t, "health.sourceMigrateNone"));
925
+ else setMigrateNote(translate(t, "health.sourceMigrateFail", { error: res.outcome?.error ?? res.error ?? "未知原因" }));
926
+ }).catch((err) => setError(String(err instanceof Error ? err.message : err))).finally(() => setMigrating(false));
927
+ };
884
928
  const report = detail?.report ?? discharge?.after;
885
929
  const lines = [];
886
930
  lines.push((0, react.createElement)("div", {
@@ -1004,6 +1048,10 @@ window.__ModuleLoader__.load({
1004
1048
  key: "lv",
1005
1049
  className: `dss_levelPill dss_level_${item.level}`
1006
1050
  }, translate(t, LEVEL_LABEL[item.level])),
1051
+ item.priority === "high" && (0, react.createElement)("span", {
1052
+ key: "p",
1053
+ className: "dss_meta"
1054
+ }, translate(t, "health.priority.high")),
1007
1055
  (0, react.createElement)("span", {
1008
1056
  key: "id",
1009
1057
  className: "dss_meta dss_uuid"
@@ -1043,6 +1091,25 @@ window.__ModuleLoader__.load({
1043
1091
  className: "dss_spinner",
1044
1092
  "aria-hidden": "true"
1045
1093
  }) : null, (0, react.createElement)("span", { key: "tx" }, repairing ? translate(t, "health.repairing") : translate(t, "health.repair"))])]));
1094
+ if (report.gates.some((gate) => gate.id === "source-kind" && gate.level === "warn")) lines.push((0, react.createElement)("div", {
1095
+ key: "migrate",
1096
+ className: "dss_phaseRow"
1097
+ }, [(0, react.createElement)("button", {
1098
+ key: "m",
1099
+ type: "button",
1100
+ className: "dss_actBtn",
1101
+ disabled: migrating,
1102
+ onClick: () => {
1103
+ runSourceMigrate(report.sessionId);
1104
+ }
1105
+ }, [migrating ? (0, react.createElement)("span", {
1106
+ key: "sp",
1107
+ className: "dss_spinner",
1108
+ "aria-hidden": "true"
1109
+ }) : null, (0, react.createElement)("span", { key: "tx" }, migrating ? translate(t, "health.sourceMigrating") : translate(t, "health.sourceMigrate"))]), migrateNote !== null && (0, react.createElement)("span", {
1110
+ key: "note",
1111
+ className: "dss_status"
1112
+ }, migrateNote)]));
1046
1113
  const prescriptions = detail?.prescriptions ?? discharge?.prescriptions ?? [];
1047
1114
  if (prescriptions.length > 0) lines.push((0, react.createElement)("div", {
1048
1115
  key: "cmds",
@@ -1218,8 +1285,16 @@ window.__ModuleLoader__.load({
1218
1285
  const style = document.createElement("style");
1219
1286
  style.id = id;
1220
1287
  style.textContent = `
1288
+ /* 侧栏 footer 槽位公约(2026-09-26):一行多入口(第三方 dsh-context 等)会互相
1289
+ 挤占 —— 宿主 .footerActions 是 nowrap flex 行。这里允许容器换行,并把本插件
1290
+ 入口钉成独占一整行(flex-basis:100%);其余入口(含第三方)自然落到后续行,
1291
+ 各行内部自行布局,谁也不挤谁。类名用 [class*=] 中段匹配:宿主是 CSS Module
1292
+ 哈希类名(实测形如 hHd-Xa_footerActions —— <hash>_<name>,哈希在前),
1293
+ 中段跨版本稳定。 */
1294
+ [class*="footerActions"]{flex-wrap:wrap;row-gap:2px;height:auto;min-height:0}
1295
+ /* 宽栏:独占一整行、放弃主动收缩(flex-shrink:0),行内居中;收起轨道回落自然宽度。 */
1221
1296
  .dss_entryWrap{flex:0 1 auto;display:inline-flex;align-items:center;min-width:0}
1222
- .dss_entryWrapWide{margin-left:6px}
1297
+ .dss_entryWrapWide{flex:0 0 100%;width:100%;justify-content:center;margin-left:0}
1223
1298
  .dss_footerEntry{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:42px;padding:0 10px 0 8px;border:none;border-radius:12px;background:transparent;cursor:pointer;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:14px;line-height:22px;white-space:nowrap;overflow:hidden;transition:background-color 160ms ease-out,color 160ms ease-out}
1224
1299
  .dss_footerEntry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
1225
1300
  .dss_footerEntryRail{width:28px;height:28px;padding:0;gap:0;border-radius:50%}
@@ -1310,7 +1385,7 @@ window.__ModuleLoader__.load({
1310
1385
  }
1311
1386
  /**
1312
1387
  * 客户端插件主体。
1313
- * @param ctx - 客户端上下文(slots、可选 locale / settingsScope)。
1388
+ * @param ctx - 客户端上下文(slots、可选 locale / configForms)。
1314
1389
  */
1315
1390
  function apply(ctx) {
1316
1391
  ctx.effect(() => injectStyles(), "dsh-manage-session: stylesheet");
@@ -1319,33 +1394,101 @@ window.__ModuleLoader__.load({
1319
1394
  zh,
1320
1395
  en
1321
1396
  }), "dsh-manage-session: dictionaries");
1322
- const bound = ctx.get("settingsScope")?.bind({ namespace: STEWARD_SETTINGS_NAMESPACE });
1397
+ const configForms = ctx.get("configForms");
1398
+ const legacySettings = ctx.get("settingsScope");
1399
+ const bound = configForms?.get("dsh-manage-session") ?? legacySettings?.bind({ namespace: "session-steward" });
1323
1400
  const slots = ctx.get("slots");
1324
1401
  if (slots === void 0) return;
1325
1402
  slots.inject("sidebar.footer.action", () => slots.register({
1326
1403
  name: "sidebar.footer.action",
1327
1404
  id: STEWARD_ENTRY_ID,
1328
- order: 12
1405
+ order: 6
1329
1406
  }, (props) => (0, react.createElement)(StewardEntry, {
1330
1407
  ...props,
1331
1408
  scope: bound
1332
1409
  })), "dsh-manage-session: sidebar footer entry");
1333
- slots.inject("settings.plugin.item", () => slots.register({
1334
- name: "settings.plugin.item",
1335
- id: STEWARD_SETTINGS_NAMESPACE,
1336
- key: STEWARD_SETTINGS_NAMESPACE,
1337
- locale: locale !== void 0 ? NS : void 0,
1338
- inject: () => ({ scope: bound ?? {
1339
- getSnapshot: () => ({
1340
- status: "ready",
1341
- value: DEFAULT_CONFIG,
1342
- revision: void 0,
1343
- writable: false
1344
- }),
1345
- subscribe: () => () => {},
1346
- set: async () => {}
1347
- } })
1348
- }, StewardSettingsCard), "dsh-manage-session: plugin settings card");
1410
+ slots.inject("dsh-family.tab", () => slots.register({
1411
+ name: "dsh-family.tab",
1412
+ id: STEWARD_ENTRY_ID,
1413
+ order: 40,
1414
+ label: () => translate(locale?.bind?.(NS), "card.title"),
1415
+ locale: NS
1416
+ }, (props) => (0, react.createElement)(StewardSettingsCard, {
1417
+ scope: bound,
1418
+ t: props.t
1419
+ })), "dsh-manage-session: family settings tab");
1420
+ const familyTabsHooks = makeFamilyTabsHooks(slots);
1421
+ ctx.effect(() => {
1422
+ let claimed;
1423
+ const timer = setTimeout(() => {
1424
+ if (slots.entries("settings.section").some((e) => e.options.id === FAMILY_SECTION_ID)) return;
1425
+ try {
1426
+ claimed = slots.register({
1427
+ name: "settings.section",
1428
+ id: FAMILY_SECTION_ID,
1429
+ order: 40,
1430
+ label: () => translate(locale?.bind?.(NS), "family.title"),
1431
+ locale: NS,
1432
+ inject: () => ({ hooks: { tabs: familyTabsHooks } }),
1433
+ children: { [FAMILY_CHILD_KEY]: {
1434
+ kind: "list",
1435
+ scope: "root"
1436
+ } }
1437
+ }, StewardFamilySection);
1438
+ } catch {}
1439
+ }, FAMILY_HOST_GRACE_MS);
1440
+ return () => {
1441
+ clearTimeout(timer);
1442
+ if (typeof claimed === "function") claimed();
1443
+ };
1444
+ }, "dsh-manage-session: family fallback host");
1445
+ }
1446
+ /** 家族节固定 id(与 thinking-levels / guard 的注册严格一致)。 */
1447
+ const FAMILY_SECTION_ID = "dsh-family";
1448
+ /** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
1449
+ const FAMILY_CHILD_KEY = "dsh-family.tab";
1450
+ /** 接管宽限期:guard 2000ms 先试,本插件 2600ms 兜底。 */
1451
+ const FAMILY_HOST_GRACE_MS = 2600;
1452
+ /** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形;locale 变更也触发重渲染)。 */
1453
+ function makeFamilyTabsHooks(slots) {
1454
+ let version = -1;
1455
+ let tabs = [];
1456
+ return {
1457
+ getSnapshot: () => {
1458
+ const next = slots.getVersion(FAMILY_CHILD_KEY);
1459
+ if (next !== version) {
1460
+ version = next;
1461
+ tabs = slots.entries(FAMILY_CHILD_KEY).map((entry) => ({
1462
+ id: entry.options.id ?? "",
1463
+ order: entry.options.order ?? 0,
1464
+ label: typeof entry.options.label === "function" ? (() => {
1465
+ try {
1466
+ return String(entry.options.label() ?? entry.options.id ?? "");
1467
+ } catch {
1468
+ return String(entry.options.id ?? "");
1469
+ }
1470
+ })() : String(entry.options.label ?? entry.options.id ?? "")
1471
+ })).sort((a, b) => a.order - b.order);
1472
+ }
1473
+ return tabs;
1474
+ },
1475
+ subscribe: (listener) => slots.subscribe(FAMILY_CHILD_KEY, listener)
1476
+ };
1477
+ }
1478
+ /**
1479
+ * 家族节接管组件(fallback host section):纯通用渲染 —— 把 `dsh-family.tab`
1480
+ * 账本里的每张贡献卡(含本插件自己的,接管时本插件也声明了该子席位)按
1481
+ * order 依次 renderSlot;各卡本身是默认展开的抽屉,整节即一页抽屉。
1482
+ */
1483
+ function StewardFamilySection(props) {
1484
+ const tabs = (0, react.useSyncExternalStore)(props.hooks.tabs.subscribe, props.hooks.tabs.getSnapshot);
1485
+ return (0, react.createElement)("div", { style: {
1486
+ display: "grid",
1487
+ gap: "12px"
1488
+ } }, ...tabs.map((row) => (0, react.createElement)("div", { key: row.id }, props.renderSlot(FAMILY_CHILD_KEY, {}, {
1489
+ only: row.id,
1490
+ fallback: null
1491
+ }))));
1349
1492
  }
1350
1493
  /** 入口按钮:持有面板开关;两个页签的可见性来自配置快照。 */
1351
1494
  function StewardEntry(props) {