dsh-manage-session 0.3.3 → 0.5.1

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.
@@ -73,6 +73,8 @@ export interface HistoryRow {
73
73
  bytes: number
74
74
  /** 投影缓存占用字节数。 */
75
75
  cacheBytes: number
76
+ /** 署名转换备份占用字节数(`bytes` 的子集,单列防误读)。 */
77
+ backupBytes: number
76
78
  }
77
79
 
78
80
  /** 历史文件列表响应。 */
@@ -91,6 +93,8 @@ export interface HistoryPurgeResponse {
91
93
  ok: boolean
92
94
  purged?: number
93
95
  freedBytes?: number
96
+ /** 一并删除的署名转换备份个数。 */
97
+ backupsRemoved?: number
94
98
  failures?: { sessionId: string; reason: string }[]
95
99
  requiresRestart?: boolean
96
100
  error?: string
@@ -98,7 +102,7 @@ export interface HistoryPurgeResponse {
98
102
 
99
103
  /** 一个 gate 的结果(与 host 侧 GateResult 对齐)。 */
100
104
  export interface HealthGate {
101
- id: 'generation' | 'log-integrity' | 'projection-cache' | 'lossless-json' | 'cold-read'
105
+ id: 'generation' | 'log-integrity' | 'projection-cache' | 'lossless-json' | 'cold-read' | 'source-kind'
102
106
  level: 'ok' | 'warn' | 'fail' | 'skipped'
103
107
  evidence: string
104
108
  attribution?: { projection?: string; package?: string; field?: string }
@@ -171,6 +175,15 @@ export interface HealthRepairResponse {
171
175
  error?: string
172
176
  }
173
177
 
178
+ /** 署名转换(session-health-source-migrate)响应。 */
179
+ export interface SourceMigrateResponse {
180
+ ok: boolean
181
+ outcome?: { ok: boolean; path: string; backup?: string; changedRows?: number; byPlugin?: Record<string, number>; error?: string }
182
+ /** 转换后的最新体检报告(无论成败都给出,便于面板对照)。 */
183
+ after?: HealthReport
184
+ error?: string
185
+ }
186
+
174
187
  /** 状态响应。 */
175
188
  export interface HealthStatusResponse {
176
189
  ok: boolean
@@ -2,8 +2,8 @@
2
2
  * 会话管家客户端半身:样式、字典、侧边栏入口(dsh-manage-session)与设置卡。
3
3
  *
4
4
  * 侧边栏入口点击后打开「养老院 / 体检」双页签对话框;两个页签的可见性由配置开关决定,
5
- * 关掉的子域不渲染页签,也不留空壳。开关读取走 settingsScope 的订阅快照,
6
- * 写通道只有设置卡(显式控件)。
5
+ * 关掉的子域不渲染页签,也不留空壳。开关读取走 configForms(0.1.7,entry id 键)/
6
+ * 旧宿主设置服务的订阅快照;配置写入由 volatile 字段自动生成的设置表单承担。
7
7
  */
8
8
  import type { Context } from 'cordis'
9
9
  import { createElement, useState, useSyncExternalStore, type ReactElement } from 'react'
@@ -13,8 +13,8 @@ import {
13
13
  STEWARD_SETTINGS_NAMESPACE,
14
14
  type StewardConfig,
15
15
  } from '../config.ts'
16
- import { StewardSettingsCard, type StewardCardScope } from './card.tsx'
17
- import { en, translate, zh, type LocaleKey } from './locales.ts'
16
+ import { StewardCardScope, StewardSettingsCard, type CardTranslate } from './card.tsx'
17
+ import { de, en, es, fr, it, ru, translate, zh, type LocaleKey } from './locales.ts'
18
18
  import { StewardFooter, StewardPanel, TAB_HEALTH, TAB_HISTORY } from './panel.tsx'
19
19
 
20
20
  export { STEWARD_ENTRY_ID, STEWARD_SETTINGS_NAMESPACE, TAB_HEALTH, TAB_HISTORY }
@@ -27,18 +27,29 @@ export const NS = 'dsh-manage-session'
27
27
  interface StewardSlotsService {
28
28
  inject(name: string, callback: () => unknown, label?: string): void
29
29
  register(config: Record<string, unknown>, component: unknown): unknown
30
+ entries(name: string): Array<{ options: { id?: string; order?: number; label?: unknown } }>
31
+ getVersion(name: string): number
32
+ subscribe(name: string, listener: () => void): () => void
30
33
  }
31
34
 
32
35
  /** locale 服务面。 */
33
36
  interface StewardLocaleService {
34
- register(ns: string, dictionaries: { zh: unknown; en: unknown }): () => void
37
+ register(
38
+ ns: string,
39
+ dictionaries: { zh: unknown; en: unknown; fr?: unknown; de?: unknown; it?: unknown; ru?: unknown; es?: unknown },
40
+ ): () => void
35
41
  }
36
42
 
37
- /** settingsScope 服务面。 */
43
+ /** 旧宿主设置服务面(0.1.7 前的回退路径,结构化镜像)。 */
38
44
  interface StewardSettingsScope {
39
45
  bind<U>(input: { namespace: string }): (StewardCardScope & { getSnapshot(): { value: U | undefined } }) | undefined
40
46
  }
41
47
 
48
+ /** configForms 服务面(0.1.7:以 profile entry id 取句柄)。 */
49
+ interface StewardConfigForms {
50
+ get<U>(entryId: string): StewardCardScope & { getSnapshot(): { value: U | undefined } }
51
+ }
52
+
42
53
  /** 客户端插件声明的注入面。 */
43
54
  export const inject = ['slots']
44
55
 
@@ -58,9 +69,31 @@ function injectStyles(): () => void {
58
69
  const style = document.createElement('style')
59
70
  style.id = id
60
71
  style.textContent = `
72
+ /* 侧栏 footer 槽位公约(2026-09-26;2026-10-01 收紧行距并强制居中):一行多
73
+ 入口(第三方 dsh-context 等)会互相挤占 —— 宿主 .footerActions 是 nowrap
74
+ flex 行。这里允许容器换行,并把本插件入口钉成独占一整行(flex-basis:100%);
75
+ 其余入口(含第三方)自然落到后续行,各行内部自行布局,谁也不挤谁。
76
+ justify-content:center 让行内所有入口(含不占满行的)统一居中;row-gap:0
77
+ 配合入口自身 32px 高度压缩纵向占位。类名用 [class*=] 中段匹配:宿主是 CSS
78
+ Module 哈希类名(实测形如 hHd-Xa_footerActions —— <hash>_<name>,哈希在前),
79
+ 中段跨版本稳定。 */
80
+ [class*="footerActions"]{flex-wrap:wrap;justify-content:center;row-gap:0;height:auto;min-height:0}
81
+ /* —— 第三方矫正:dsh-context「上下文洞察」入口(2026-10-01)——
82
+ .lc-ov-entry 按"独占整行"设计(width:calc(100% + 4px)、无 justify-content、
83
+ 42px 高、不对称 padding),与本槽位公约(每个入口独占一行、行内居中、32px)
84
+ 冲突,表现为文字靠左、纵向松散。这里按公约强制矫正;:not() 排除收起轨道的
85
+ 36px 圆钮形态。lc-ov-* 是 dsh-context 源码硬编码类名(非构建哈希),跨版本
86
+ 稳定(实测 0.56.1 / 0.60.0 规则一致)。两个自有插件各自携带同一矫正块:
87
+ 任一被禁用时矫正仍生效。 */
88
+ .lc-ov-entry:not(.lc-ov-entry-rail){width:auto!important;flex:0 0 100%!important;min-width:0!important;justify-content:center!important;height:32px!important;margin:0!important;padding:0 10px!important}
89
+ .lc-ov-entry-label{flex:0 1 auto!important}
90
+ /* 宽栏:独占一整行、放弃主动收缩(flex-shrink:0),行内居中;收起轨道回落自然宽度。 */
61
91
  .dss_entryWrap{flex:0 1 auto;display:inline-flex;align-items:center;min-width:0}
62
- .dss_entryWrapWide{margin-left:6px}
63
- .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}
92
+ .dss_entryWrapWide{flex:0 0 100%;width:100%;justify-content:center;margin-left:0}
93
+ /* 高度 32px(内容 22px 行高 + 上下各 5px):宿主默认 42px 的上下裕度在多行
94
+ 堆叠后过于松散;左右内边距对称(10px/10px),否则整行居中时按钮内容会因
95
+ 不对称 padding 向左偏 1px。 */
96
+ .dss_footerEntry{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:32px;padding:0 10px;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}
64
97
  .dss_footerEntry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
65
98
  .dss_footerEntryRail{width:28px;height:28px;padding:0;gap:0;border-radius:50%}
66
99
  .dss_footerIcon{flex:none;font-size:15px;line-height:1}
@@ -149,46 +182,133 @@ function injectStyles(): () => void {
149
182
 
150
183
  /**
151
184
  * 客户端插件主体。
152
- * @param ctx - 客户端上下文(slots、可选 locale / settingsScope)。
185
+ * @param ctx - 客户端上下文(slots、可选 locale / configForms)。
153
186
  */
154
187
  export function apply(ctx: Context): void {
155
188
  ctx.effect(() => injectStyles(), 'dsh-manage-session: stylesheet')
156
189
 
157
190
  const locale = ctx.get('locale') as StewardLocaleService | undefined
158
191
  if (locale !== undefined && typeof locale.register === 'function') {
159
- ctx.effect(() => locale.register(NS, { zh, en }), 'dsh-manage-session: dictionaries')
192
+ ctx.effect(() => locale.register(NS, { zh, en, fr, de, it, ru, es }), 'dsh-manage-session: dictionaries')
160
193
  }
161
194
 
162
- const settingsScope = ctx.get('settingsScope') as StewardSettingsScope | undefined
163
- const bound = settingsScope?.bind<StewardConfig>({ namespace: STEWARD_SETTINGS_NAMESPACE })
195
+ // 0.1.7:configForms 以 entry id 取句柄;旧宿主回退到按命名空间绑定。
196
+ const configForms = ctx.get('configForms') as StewardConfigForms | undefined
197
+ const legacySettings = ctx.get('settingsScope') as StewardSettingsScope | undefined
198
+ const bound = configForms?.get<StewardConfig>(STEWARD_ENTRY_ID)
199
+ ?? legacySettings?.bind<StewardConfig>({ namespace: STEWARD_SETTINGS_NAMESPACE })
164
200
 
165
201
  const slots = ctx.get('slots') as StewardSlotsService | undefined
166
202
  if (slots === undefined) return
167
203
 
168
204
  // 侧边栏入口:一个按钮,打开「养老院 / 体检」双页签面板。
169
205
  slots.inject('sidebar.footer.action', () => slots.register(
170
- { name: 'sidebar.footer.action', id: STEWARD_ENTRY_ID, order: 12 },
206
+ { name: 'sidebar.footer.action', id: STEWARD_ENTRY_ID, order: 6 },
171
207
  (props: StewardFooterProps) => createElement(StewardEntry, { ...props, scope: bound }),
172
208
  ), 'dsh-manage-session: sidebar footer entry')
173
209
 
174
- slots.inject('settings.plugin.item', () => slots.register({
175
- name: 'settings.plugin.item',
176
- id: STEWARD_SETTINGS_NAMESPACE,
177
- key: STEWARD_SETTINGS_NAMESPACE,
178
- locale: locale !== undefined ? NS : undefined,
179
- inject: () => ({
180
- scope: bound ?? {
181
- getSnapshot: () => ({
182
- status: 'ready' as const,
183
- value: DEFAULT_CONFIG,
184
- revision: undefined,
185
- writable: false,
186
- }),
187
- subscribe: () => () => {},
188
- set: async () => {},
189
- },
210
+ // 插件族共用设置 tab(dsh-thinking-levels 的顶级「起子插件设置」节声明该子
211
+ // 席位)。thinking-levels 缺席时本 inject 静默等待,不阻塞客户端半。
212
+ slots.inject('dsh-family.tab', () => slots.register(
213
+ {
214
+ name: 'dsh-family.tab',
215
+ id: STEWARD_ENTRY_ID,
216
+ order: 40,
217
+ // 账本 label:读期求值,跟随当前 locale(translate 内建 zh/en 兜底)。
218
+ label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'card.title'),
219
+ locale: NS,
220
+ },
221
+ (props: { t?: CardTranslate }) => createElement(StewardSettingsCard, {
222
+ scope: bound as StewardCardScope,
223
+ t: props.t,
190
224
  }),
191
- }, StewardSettingsCard), 'dsh-manage-session: plugin settings card')
225
+ ), 'dsh-manage-session: family settings tab')
226
+
227
+ // 家族节宿主选举(次优先级):宿主 dsh-thinking-levels 缺席时顶上「起子插件
228
+ // 设置」节。下载量排名 session-steward(1090) 低于 session-guard(1522) → 宽限期
229
+ // 更长,guard 先尝试接管;guard 也缺席时本插件按同一 id `dsh-family` 接管,
230
+ // 声明同一个 dsh-family.tab 子席位并通用渲染全部贡献卡。与 guard 的并发竞争
231
+ // 由 ui-slots 同 id 重复注册抛错仲裁,先到者胜、后到者放弃。
232
+ const familyTabsHooks = makeFamilyTabsHooks(slots)
233
+ ctx.effect(() => {
234
+ let claimed: unknown
235
+ const timer = setTimeout(() => {
236
+ if (slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) return
237
+ try {
238
+ claimed = slots.register({
239
+ name: 'settings.section',
240
+ id: FAMILY_SECTION_ID,
241
+ order: 40,
242
+ label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'family.title'),
243
+ locale: NS,
244
+ inject: () => ({ hooks: { tabs: familyTabsHooks } }),
245
+ children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },
246
+ }, StewardFamilySection)
247
+ } catch {
248
+ // 并发接管竞争落败(或设置壳未声明席位):胜出方的节服务整个家族。
249
+ }
250
+ }, FAMILY_HOST_GRACE_MS)
251
+ return () => { clearTimeout(timer); if (typeof claimed === 'function') (claimed as () => void)() }
252
+ }, 'dsh-manage-session: family fallback host')
253
+ }
254
+
255
+ /** 家族节固定 id(与 thinking-levels / guard 的注册严格一致)。 */
256
+ const FAMILY_SECTION_ID = 'dsh-family'
257
+ /** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
258
+ const FAMILY_CHILD_KEY = 'dsh-family.tab'
259
+ /** 接管宽限期:guard 2000ms 先试,本插件 2600ms 兜底。 */
260
+ const FAMILY_HOST_GRACE_MS = 2600
261
+
262
+ /** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形;locale 变更也触发重渲染)。 */
263
+ function makeFamilyTabsHooks(slots: StewardSlotsService): {
264
+ getSnapshot: () => readonly FamilyTabEntry[]
265
+ subscribe: (listener: () => void) => () => void
266
+ } {
267
+ let version = -1
268
+ let tabs: readonly FamilyTabEntry[] = []
269
+ return {
270
+ getSnapshot: () => {
271
+ const next = slots.getVersion(FAMILY_CHILD_KEY)
272
+ if (next !== version) {
273
+ version = next
274
+ tabs = slots.entries(FAMILY_CHILD_KEY)
275
+ .map(entry => ({
276
+ id: entry.options.id ?? '',
277
+ order: entry.options.order ?? 0,
278
+ label: typeof entry.options.label === 'function'
279
+ ? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()
280
+ : String(entry.options.label ?? entry.options.id ?? ''),
281
+ }))
282
+ .sort((a, b) => a.order - b.order)
283
+ }
284
+ return tabs
285
+ },
286
+ subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),
287
+ }
288
+ }
289
+
290
+ /** 账本条目(id/order/label,label 已解析为字符串)。 */
291
+ interface FamilyTabEntry {
292
+ id: string
293
+ order: number
294
+ label: string
295
+ }
296
+
297
+ /**
298
+ * 家族节接管组件(fallback host section):纯通用渲染 —— 把 `dsh-family.tab`
299
+ * 账本里的每张贡献卡(含本插件自己的,接管时本插件也声明了该子席位)按
300
+ * order 依次 renderSlot;各卡本身是默认展开的抽屉,整节即一页抽屉。
301
+ */
302
+ function StewardFamilySection(props: {
303
+ renderSlot: (key: string, owner?: object, opts?: { only?: string; fallback?: unknown }) => unknown
304
+ hooks: { tabs: { getSnapshot: () => readonly FamilyTabEntry[]; subscribe: (listener: () => void) => () => void } }
305
+ }): ReactElement {
306
+ const tabs = useSyncExternalStore(props.hooks.tabs.subscribe, props.hooks.tabs.getSnapshot)
307
+ return createElement('div', { style: { display: 'grid', gap: '12px' } },
308
+ ...tabs.map(row => createElement('div', { key: row.id },
309
+ props.renderSlot(FAMILY_CHILD_KEY, {}, { only: row.id, fallback: null }) as ReactElement,
310
+ )),
311
+ )
192
312
  }
193
313
 
194
314
  /** 入口按钮:持有面板开关;两个页签的可见性来自配置快照。 */