dsh-manage-session 2.0.3 → 2.0.5
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 +33 -33
- package/CHANGELOG.ko.md +33 -33
- package/CHANGELOG.md +33 -33
- package/INSTALL.ja.md +164 -164
- package/INSTALL.ko.md +164 -164
- package/INSTALL.md +164 -164
- package/INSTALL.zh.md +164 -164
- package/LICENSE +21 -21
- package/README.en.md +76 -62
- package/README.ja.md +53 -39
- package/README.ko.md +52 -38
- package/README.md +18 -4
- package/cordis.patch.yml +19 -19
- package/dsh.plugin.json +2 -2
- package/locale/de.json +6 -6
- package/locale/en.json +6 -6
- package/locale/es.json +6 -6
- package/locale/fr.json +6 -6
- package/locale/it.json +6 -6
- package/locale/ja.json +6 -6
- package/locale/ko.json +6 -6
- package/locale/ru.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +9 -9
- package/screenshots.json +3 -3
- package/src/client/bridge-scope.ts +202 -202
- package/src/client/card.tsx +154 -154
- package/src/client/host-api.ts +61 -61
- package/src/client/index.ts +594 -594
- package/src/client/locales.ts +893 -893
- package/src/client/manage-locales.ts +178 -178
- package/src/client/manage.tsx +430 -430
- package/src/client/panel.tsx +131 -131
- package/src/config.ts +43 -43
- package/src/host/health/attribution.ts +118 -118
- package/src/host/health/lossless.ts +128 -128
- package/src/host/health/source-kind.ts +350 -350
- package/src/host/history/archive-source.ts +250 -250
- package/src/host/history/archive.ts +249 -249
- package/src/host/history/favorites.ts +71 -71
- package/src/host/history/purge.ts +370 -370
- package/src/host/registry/archive-registry.ts +317 -317
- package/src/host/settings-bridge.ts +222 -222
package/src/client/index.ts
CHANGED
|
@@ -1,594 +1,594 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 会话管理客户端半身:样式、字典、侧栏入口(顶栏工具槽 / 底栏回落)与设置卡。
|
|
3
|
-
*
|
|
4
|
-
* 入口只有一个(会话管理):点开是「管理 / 体检」双页签。搜索子域已拆回
|
|
5
|
-
* dsh-search-index —— 本文件不再注册搜索入口、搜索面板或搜索设置卡。
|
|
6
|
-
*
|
|
7
|
-
* 入口位次由宿主能力决定(inject 探测两席位):
|
|
8
|
-
* - 顶栏工具行(`sidebar.panellist` 面板行 + `main` 整页槽)→ 面板行落在「插件」
|
|
9
|
-
* 之后、「更多」之前,点击在主栏整页打开;
|
|
10
|
-
* - 无该槽位(老线)→ 回落底栏 `sidebar.footer.action`,点击开对话框。
|
|
11
|
-
*
|
|
12
|
-
* 开关读取:configForms(0.1.7,entry id 键)/ legacy 宿主走自家 webServer 设置桥
|
|
13
|
-
* (settingsScope.bind 是数据死路,见 bridge-scope.ts);配置写入同链路。
|
|
14
|
-
*/
|
|
15
|
-
import type { Context } from 'cordis'
|
|
16
|
-
import { createElement, useState, useSyncExternalStore, type ReactElement } from 'react'
|
|
17
|
-
import {
|
|
18
|
-
DEFAULT_CONFIG,
|
|
19
|
-
STEWARD_ENTRY_ID,
|
|
20
|
-
STEWARD_SETTINGS_NAMESPACE,
|
|
21
|
-
type StewardConfig,
|
|
22
|
-
} from '../config.ts'
|
|
23
|
-
import { StewardCardScope, StewardSettingsCard, type CardTranslate } from './card.tsx'
|
|
24
|
-
import { stewardBridgeScope, withBridgeFallback, type BridgeScope } from './bridge-scope.ts'
|
|
25
|
-
import { dictionaries, translate, type LocaleKey } from './locales.ts'
|
|
26
|
-
import { StewardFooter, StewardPanel, TAB_HEALTH } from './panel.tsx'
|
|
27
|
-
import { openSessionThrough } from './host-api.ts'
|
|
28
|
-
import {
|
|
29
|
-
SESSION_MENU_DELETE_ID, SESSION_MENU_DELETE_ORDER, SESSION_MENU_SEAT, StewardDeleteMenuItem,
|
|
30
|
-
} from './session-menu.tsx'
|
|
31
|
-
import {
|
|
32
|
-
installLegacySessionMenuDelete, makeDirectDeleteProbe, purgeSessionById,
|
|
33
|
-
} from './session-menu-legacy.ts'
|
|
34
|
-
|
|
35
|
-
export { STEWARD_ENTRY_ID, STEWARD_SETTINGS_NAMESPACE, TAB_HEALTH }
|
|
36
|
-
export { translate } from './locales.ts'
|
|
37
|
-
|
|
38
|
-
/** 字典命名空间(locale.register 用)。 */
|
|
39
|
-
export const NS = 'dsh-manage-session'
|
|
40
|
-
|
|
41
|
-
/** 槽位服务面(结构化镜像)。 */
|
|
42
|
-
interface StewardSlotsService {
|
|
43
|
-
inject(name: string, callback: () => unknown, label?: string): () => void
|
|
44
|
-
register(config: Record<string, unknown>, component: unknown): unknown
|
|
45
|
-
entries(name: string): Array<{ options: { id?: string; key?: string; order?: number; label?: unknown } }>
|
|
46
|
-
getVersion(name: string): number
|
|
47
|
-
subscribe(name: string, listener: () => void): () => void
|
|
48
|
-
/** 槽位声明探测(部分宿主暴露;仅诊断用)。 */
|
|
49
|
-
specDynamic?(name: string): unknown
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** locale 服务面。 */
|
|
53
|
-
interface StewardLocaleService {
|
|
54
|
-
register(ns: string, dicts: Record<string, Record<string, string>>): () => void
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** configForms 服务面(0.1.7:以 profile entry id 取句柄)。 */
|
|
58
|
-
interface StewardConfigForms {
|
|
59
|
-
get<U>(entryId: string): StewardCardScope & { getSnapshot(): { value: U | undefined } }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** 客户端插件声明的注入面。 */
|
|
63
|
-
export const inject = ['slots', 'locale']
|
|
64
|
-
|
|
65
|
-
/** 侧边栏入口的 props(结构子集)。 */
|
|
66
|
-
export interface StewardFooterProps {
|
|
67
|
-
/** 侧边栏是否渲染宽栏内容(false = 56px 收起轨道)。 */
|
|
68
|
-
wide?: boolean
|
|
69
|
-
onClick?: () => void
|
|
70
|
-
title?: string
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** 注入样式(幂等)。 */
|
|
74
|
-
function injectStyles(): () => void {
|
|
75
|
-
const id = 'dsh-manage-session-styles'
|
|
76
|
-
if (typeof document === 'undefined') return () => {}
|
|
77
|
-
if (document.getElementById(id) !== null) return () => {}
|
|
78
|
-
const style = document.createElement('style')
|
|
79
|
-
style.id = id
|
|
80
|
-
style.textContent = `
|
|
81
|
-
/* 侧栏 footer 槽位公约(2026-09-26;2026-10-01 收紧行距并强制居中):一行多
|
|
82
|
-
入口(第三方 dsh-context 等)会互相挤占 —— 宿主 .footerActions 是 nowrap
|
|
83
|
-
flex 行。这里允许容器换行,并把本插件入口钉成独占一整行(flex-basis:100%);
|
|
84
|
-
其余入口(含第三方)自然落到后续行,各行内部自行布局,谁也不挤谁。
|
|
85
|
-
justify-content:center 让行内所有入口(含不占满行的)统一居中;row-gap:0
|
|
86
|
-
配合入口自身 32px 高度压缩纵向占位。类名用 [class*=] 中段匹配:宿主是 CSS
|
|
87
|
-
Module 哈希类名(实测形如 hHd-Xa_footerActions —— <hash>_<name>,哈希在前),
|
|
88
|
-
中段跨版本稳定。 */
|
|
89
|
-
[class*="footerActions"]{flex-wrap:wrap;justify-content:center;row-gap:0;height:auto;min-height:0}
|
|
90
|
-
/* —— 第三方矫正:dsh-context「上下文洞察」入口(2026-10-01)——
|
|
91
|
-
.lc-ov-entry 按"独占整行"设计(width:calc(100% + 4px)、无 justify-content、
|
|
92
|
-
42px 高、不对称 padding),与本槽位公约(每个入口独占一行、行内居中、32px)
|
|
93
|
-
冲突,表现为文字靠左、纵向松散。这里按公约强制矫正;:not() 排除收起轨道的
|
|
94
|
-
36px 圆钮形态。lc-ov-* 是 dsh-context 源码硬编码类名(非构建哈希),跨版本
|
|
95
|
-
稳定(实测 0.56.1 / 0.60.0 规则一致)。两个自有插件各自携带同一矫正块:
|
|
96
|
-
任一被禁用时矫正仍生效。 */
|
|
97
|
-
.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}
|
|
98
|
-
.lc-ov-entry-label{flex:0 1 auto!important}
|
|
99
|
-
/* 宽栏:独占一整行、放弃主动收缩(flex-shrink:0),行内居中;收起轨道回落自然宽度。 */
|
|
100
|
-
.dms_entryWrap{flex:0 1 auto;display:inline-flex;align-items:center;min-width:0}
|
|
101
|
-
.dms_entryWrapWide{flex:0 0 100%;width:100%;justify-content:center;margin-left:0}
|
|
102
|
-
/* 高度 32px(内容 22px 行高 + 上下各 5px):宿主默认 42px 的上下裕度在多行
|
|
103
|
-
堆叠后过于松散;左右内边距对称(10px/10px),否则整行居中时按钮内容会因
|
|
104
|
-
不对称 padding 向左偏 1px。 */
|
|
105
|
-
.dms_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}
|
|
106
|
-
.dms_footerEntry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
107
|
-
.dms_footerEntryRail{width:28px;height:28px;padding:0;gap:0;border-radius:50%}
|
|
108
|
-
.dms_footerIcon{flex:none;font-size:15px;line-height:1}
|
|
109
|
-
.dms_footerLabel{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
110
|
-
.dms_backdrop{position:fixed;inset:0;background:rgba(15,20,30,.42);z-index:1000}
|
|
111
|
-
.dms_panel{position:fixed;z-index:1001;left:50%;top:6vh;transform:translateX(-50%);width:min(860px,94vw);max-height:86vh;overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(0,0,0,.18);padding:10px 14px 14px}
|
|
112
|
-
/* 整页形态(顶栏工具行入口):取消浮动定位与遮罩观感,填满主栏。 */
|
|
113
|
-
.dms_panel.dms_panelPage{position:relative;left:auto;top:auto;transform:none;z-index:auto;width:100%;max-width:none;height:100%;max-height:none;box-sizing:border-box;border:none;border-radius:0;box-shadow:none;background:transparent;padding:14px 18px 16px}
|
|
114
|
-
.dms_dialogHead{display:flex;align-items:center;gap:10px;min-height:34px}
|
|
115
|
-
.dms_dialogTitle{font-weight:600;color:var(--dsw-alias-label-primary,#111827);flex:1 1 auto;min-width:0}
|
|
116
|
-
.dms_tabRow{display:inline-flex;gap:4px}
|
|
117
|
-
.dms_tab{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:3px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
|
|
118
|
-
.dms_tabActive{background:var(--dsw-alias-interactive-bg-active,rgba(63,99,216,.12));color:var(--dsw-alias-label-primary,#111827)}
|
|
119
|
-
.dms_tabBody{display:flex;flex-direction:column;gap:8px;padding-top:8px}
|
|
120
|
-
.dms_actBtn{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:4px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-primary,#111827)}
|
|
121
|
-
.dms_actBtn:disabled{opacity:.5;cursor:default}
|
|
122
|
-
.dms_btnRow{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
|
|
123
|
-
.dms_list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}
|
|
124
|
-
.dms_row{display:flex;gap:8px;align-items:center;padding:5px 8px;border-radius:8px;border:1px solid transparent}
|
|
125
|
-
.dms_row:hover{border-color:var(--dsw-alias-border-l2,#e5e7eb)}
|
|
126
|
-
.dms_meta{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
|
|
127
|
-
.dms_uuid{font-family:var(--ds-font-family-code,monospace)}
|
|
128
|
-
/* 按行的磁盘占用:右对齐、不参与标题压缩,方便纵向比对「哪条最占地方」。 */
|
|
129
|
-
.dms_status{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
|
|
130
|
-
.dms_error{font-size:12px;color:var(--dsw-alias-state-error-primary,#dc2626)}
|
|
131
|
-
.dms_empty{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af);padding:10px 2px}
|
|
132
|
-
.dms_gateList{display:flex;flex-direction:column;gap:6px}
|
|
133
|
-
.dms_gate{display:flex;flex-wrap:wrap;gap:6px;align-items:baseline;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 10px}
|
|
134
|
-
.dms_gateName{font-weight:600;font-size:12px}
|
|
135
|
-
.dms_gateEvidence{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1 1 100%}
|
|
136
|
-
.dms_levelPill{font-size:11px;border-radius:999px;padding:1px 8px;border:1px solid transparent}
|
|
137
|
-
.dms_level_ok{background:rgba(22,163,74,.12);color:#15803d}
|
|
138
|
-
.dms_level_warn{background:rgba(217,119,6,.14);color:#b45309}
|
|
139
|
-
.dms_level_fail{background:rgba(220,38,38,.14);color:#b91c1c}
|
|
140
|
-
.dms_level_skipped{background:rgba(107,114,128,.14);color:#4b5563}
|
|
141
|
-
.dms_progressRow{display:flex;align-items:center;gap:8px;margin-top:2px}
|
|
142
|
-
.dms_progressTrack{flex:1;height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#eef0f3);overflow:hidden}
|
|
143
|
-
.dms_progressFill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary,#3d5be0);transition:width .2s ease}
|
|
144
|
-
.dms_progressIndeterminate{width:35%;animation:dssSlide 1.1s ease-in-out infinite}
|
|
145
|
-
.dms_why{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1;min-width:0}
|
|
146
|
-
.dms_cacheRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px}
|
|
147
|
-
.dms_spinner{width:11px;height:11px;margin-right:6px;border-radius:50%;display:inline-block;vertical-align:-1px;border:2px solid currentColor;border-top-color:transparent;animation:dssSpin .7s linear infinite}
|
|
148
|
-
@keyframes dssSpin{to{transform:rotate(360deg)}}
|
|
149
|
-
@keyframes dssSlide{0%{margin-left:-35%}100%{margin-left:100%}}
|
|
150
|
-
@media (prefers-reduced-motion:reduce){.dms_spinner{animation:none}.dms_progressIndeterminate{animation:none;width:100%;opacity:.5}.dms_setChev{transition:none}.dms_setCard{transition:none}}
|
|
151
|
-
.dms_verdict{font-size:12px;line-height:18px;border-radius:8px;padding:6px 10px;margin-top:6px;border:1px solid transparent}
|
|
152
|
-
.dms_verdict_repaired{background:rgba(22,163,74,.10);color:#15803d;border-color:rgba(22,163,74,.28)}
|
|
153
|
-
.dms_verdict_repaired-with-residual{background:rgba(217,119,6,.10);color:#b45309;border-color:rgba(217,119,6,.28)}
|
|
154
|
-
.dms_verdict_nothing-to-do{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
|
|
155
|
-
.dms_verdict_not-applicable{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
|
|
156
|
-
.dms_verdict_failed{background:rgba(220,38,38,.10);color:#b91c1c;border-color:rgba(220,38,38,.28)}
|
|
157
|
-
.dms_phaseRow{display:flex;gap:8px;align-items:center}
|
|
158
|
-
.dms_phase{font-weight:600;font-size:12px}
|
|
159
|
-
.dms_cmdBlock{display:flex;flex-direction:column;gap:4px;border:1px dashed var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 8px}
|
|
160
|
-
.dms_cmdLine{display:flex;gap:6px;align-items:center}
|
|
161
|
-
.dms_cmd{flex:1 1 auto;font-family:var(--ds-font-family-code,monospace);font-size:11px;white-space:pre-wrap;word-break:break-all}
|
|
162
|
-
.dms_setCard{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.05));border-radius:12px;transition:border-color .16s,background .16s}
|
|
163
|
-
.dms_setHead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:none;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}
|
|
164
|
-
.dms_setHeadText{display:flex;flex-direction:column;gap:2px;flex:1 1 0%;min-width:0}
|
|
165
|
-
.dms_setCardTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#111827)}
|
|
166
|
-
.dms_setCardDesc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9ca3af)}
|
|
167
|
-
.dms_setChev{color:var(--dsw-alias-label-tertiary,#9ca3af);flex:0 0 auto;transition:transform .16s}
|
|
168
|
-
.dms_setCardOpen .dms_setChev{transform:rotate(180deg)}
|
|
169
|
-
.dms_setBody{display:flex;flex-direction:column;gap:2px;padding:0 16px 12px}
|
|
170
|
-
.dms_setRow{display:flex;align-items:center;gap:10px;padding:6px 0}
|
|
171
|
-
.dms_setText{display:flex;flex-direction:column;flex:1 1 auto;min-width:0}
|
|
172
|
-
.dms_setTitle{font-size:13px;color:var(--dsw-alias-label-primary,#111827)}
|
|
173
|
-
.dms_setDesc{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
|
|
174
|
-
.dms_toggle{display:inline-flex;align-items:center;cursor:pointer;position:relative}
|
|
175
|
-
.dms_toggle input{position:absolute;opacity:0;width:0;height:0}
|
|
176
|
-
.dms_toggleTrack{width:34px;height:18px;border-radius:999px;background:var(--dsw-alias-border-l2,#d1d5db);position:relative;transition:background .15s ease}
|
|
177
|
-
.dms_toggleKnob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:left .15s ease}
|
|
178
|
-
.dms_toggleOn .dms_toggleTrack{background:var(--dsw-alias-button-primary-fill,#3f63d8)}
|
|
179
|
-
.dms_toggleOn .dms_toggleKnob{left:18px}
|
|
180
|
-
`
|
|
181
|
-
document.head.appendChild(style)
|
|
182
|
-
return () => { style.remove() }
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* 客户端插件主体。
|
|
187
|
-
* @param ctx - 客户端上下文(slots、可选 locale / configForms)。
|
|
188
|
-
*/
|
|
189
|
-
export function apply(ctx: Context): void {
|
|
190
|
-
ctx.effect(() => injectStyles(), 'dsh-manage-session: stylesheet')
|
|
191
|
-
|
|
192
|
-
const locale = ctx.get('locale') as StewardLocaleService | undefined
|
|
193
|
-
if (locale !== undefined && typeof locale.register === 'function') {
|
|
194
|
-
ctx.effect(() => locale.register(NS, dictionaries), 'dsh-manage-session: dictionaries')
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 数据面回退链(台账 A2 定案 + 0.1.7 实测补丁):首选 configForms 原生句柄
|
|
198
|
-
// (entry id 键),**渲染期**观测到 unavailable(出生死或 loading→死,A2 死
|
|
199
|
-
// 路签名)就地落桥——自家 webServer 设置桥(settings-describe/mutate)。
|
|
200
|
-
const configForms = ctx.get('configForms') as StewardConfigForms | undefined
|
|
201
|
-
const bound = withBridgeFallback(
|
|
202
|
-
configForms?.get<StewardConfig>(STEWARD_ENTRY_ID) as BridgeScope<StewardConfig> | undefined,
|
|
203
|
-
stewardBridgeScope(),
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
const slots = ctx.get('slots') as StewardSlotsService | undefined
|
|
207
|
-
if (slots === undefined) return
|
|
208
|
-
|
|
209
|
-
// 管理台行点击打开会话:click 时经 openSessionThrough 解析(uiWorkspace.openSession
|
|
210
|
-
// 0.1.7+ 优先,旧宿主回退 sessions.open)—— 应用序早于导航模块,必须惰性取。
|
|
211
|
-
const openSession = (sessionId: string): void => {
|
|
212
|
-
openSessionThrough((name) => ctx.get(name), sessionId)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 顶栏页的关闭动作:回退到会话(layout 服务惰性取——装载序不保证)。
|
|
216
|
-
const closeToolPanel = (): void => {
|
|
217
|
-
try {
|
|
218
|
-
const layout = ctx.get('layout') as { selectPanel?: (id: string | null) => void } | undefined
|
|
219
|
-
layout?.selectPanel?.(null)
|
|
220
|
-
} catch {
|
|
221
|
-
/* 布局服务缺席:页面保留,用户可点「会话」行返回 */
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// 侧栏入口(一枚)——**位次由宿主能力决定**:顶栏工具行 or 底栏回落。
|
|
226
|
-
// 席位能力用 inject 探测(席位被声明时才回调),不依赖 spec 查询。
|
|
227
|
-
ctx.effect(() => {
|
|
228
|
-
let rowSeat = false
|
|
229
|
-
let mainSeat = false
|
|
230
|
-
let footerOff: (() => void) | undefined
|
|
231
|
-
let topOff: (() => void) | undefined
|
|
232
|
-
|
|
233
|
-
const sync = (): void => {
|
|
234
|
-
const topReady = rowSeat && mainSeat
|
|
235
|
-
if (topReady && topOff === undefined) {
|
|
236
|
-
if (footerOff !== undefined) { const off = footerOff; footerOff = undefined; off() }
|
|
237
|
-
topOff = mountTopToolEntries(slots, bound, openSession, closeToolPanel)
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
if (!topReady && footerOff === undefined && topOff === undefined) {
|
|
241
|
-
footerOff = mountFooterEntries(slots, bound, openSession)
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// 接线期不让 probe 回调触发 sync:两席位先后落位时先到的会误判「顶栏不齐」。
|
|
246
|
-
let wired = false
|
|
247
|
-
const onSeat = (): void => { if (wired) sync() }
|
|
248
|
-
|
|
249
|
-
const offRowProbe = slots.inject(TOP_TOOL_SEAT, () => {
|
|
250
|
-
rowSeat = true
|
|
251
|
-
onSeat()
|
|
252
|
-
return () => { rowSeat = false }
|
|
253
|
-
}, 'dsh-manage-session: probe top tool seat')
|
|
254
|
-
|
|
255
|
-
const offMainProbe = slots.inject(MAIN_SEAT, () => {
|
|
256
|
-
mainSeat = true
|
|
257
|
-
onSeat()
|
|
258
|
-
return () => { mainSeat = false }
|
|
259
|
-
}, 'dsh-manage-session: probe main page seat')
|
|
260
|
-
|
|
261
|
-
wired = true
|
|
262
|
-
sync()
|
|
263
|
-
|
|
264
|
-
return () => {
|
|
265
|
-
offRowProbe()
|
|
266
|
-
offMainProbe()
|
|
267
|
-
if (footerOff !== undefined) { const off = footerOff; footerOff = undefined; off() }
|
|
268
|
-
if (topOff !== undefined) { const off = topOff; topOff = undefined; off() }
|
|
269
|
-
}
|
|
270
|
-
}, 'dsh-manage-session: sidebar entry')
|
|
271
|
-
|
|
272
|
-
// 插件族共用设置 tab(dsh-thinking-levels 的顶级「起子插件设置」节声明该子席位)。
|
|
273
|
-
// thinking-levels 缺席时本 inject 静默等待,不阻塞客户端半。
|
|
274
|
-
slots.inject('dsh-family.tab', () => slots.register(
|
|
275
|
-
{
|
|
276
|
-
name: 'dsh-family.tab',
|
|
277
|
-
id: STEWARD_ENTRY_ID,
|
|
278
|
-
order: 40,
|
|
279
|
-
// 账本 label:读期求值,跟随当前 locale(translate 内建 zh/en 兜底)。
|
|
280
|
-
label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'card.title'),
|
|
281
|
-
locale: NS,
|
|
282
|
-
},
|
|
283
|
-
(props: { t?: CardTranslate }) => createElement(StewardSettingsCard, {
|
|
284
|
-
scope: bound as StewardCardScope,
|
|
285
|
-
t: props.t,
|
|
286
|
-
}),
|
|
287
|
-
), 'dsh-manage-session: family settings tab')
|
|
288
|
-
|
|
289
|
-
// Plugins 页配置卡(`plugins.bundle.config`,key = 包名):0.1.7 宿主的 Plugins
|
|
290
|
-
// 页只渲染 key 与包名相等的条目。
|
|
291
|
-
slots.inject('plugins.bundle.config', () => slots.register(
|
|
292
|
-
{ name: 'plugins.bundle.config', key: STEWARD_ENTRY_ID },
|
|
293
|
-
(props: { t?: CardTranslate }) => createElement(StewardSettingsCard, {
|
|
294
|
-
scope: bound as StewardCardScope,
|
|
295
|
-
t: props.t,
|
|
296
|
-
}),
|
|
297
|
-
), 'dsh-manage-session: plugins page config card')
|
|
298
|
-
|
|
299
|
-
// 侧栏会话菜单删除臂(按左侧边栏换代识别):席位自 0.1.7 存在,本行 order=500
|
|
300
|
-
// 落末位;≤0.1.5 菜单写死无席位 → inject 静默等待,删除入口由 DOM 注入臂承担。
|
|
301
|
-
// 行可见性由组件自探针 directDelete(缺省关),非 true 不渲染。
|
|
302
|
-
slots.inject(SESSION_MENU_SEAT, () => slots.register(
|
|
303
|
-
{ name: SESSION_MENU_SEAT, id: SESSION_MENU_DELETE_ID, order: SESSION_MENU_DELETE_ORDER, locale: NS },
|
|
304
|
-
StewardDeleteMenuItem,
|
|
305
|
-
), 'dsh-manage-session: session menu delete row')
|
|
306
|
-
|
|
307
|
-
// 老线(≤0.1.5)菜单无席位——DOM 注入臂补上删除行。
|
|
308
|
-
if (typeof document !== 'undefined') {
|
|
309
|
-
const probe = makeDirectDeleteProbe()
|
|
310
|
-
ctx.effect(() => installLegacySessionMenuDelete({
|
|
311
|
-
doc: document,
|
|
312
|
-
purge: purgeSessionById,
|
|
313
|
-
isEnabled: probe,
|
|
314
|
-
lang: () => (typeof navigator !== 'undefined' ? navigator.language : undefined),
|
|
315
|
-
}), 'dsh-manage-session: legacy session menu delete injector')
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// 家族节宿主选举(次优先级):宿主 dsh-thinking-levels 缺席时顶上「起子插件
|
|
319
|
-
// 设置」节。下载量排名低于 session-guard → 宽限期更长,guard 先尝试接管;
|
|
320
|
-
// guard 也缺席时本插件按同一 id `dsh-family` 接管。
|
|
321
|
-
const familyTabsHooks = makeFamilyTabsHooks(slots)
|
|
322
|
-
ctx.effect(() => {
|
|
323
|
-
let claimed: unknown
|
|
324
|
-
const timer = setTimeout(() => {
|
|
325
|
-
if (slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) return
|
|
326
|
-
try {
|
|
327
|
-
claimed = slots.register({
|
|
328
|
-
name: 'settings.section',
|
|
329
|
-
id: FAMILY_SECTION_ID,
|
|
330
|
-
order: 40,
|
|
331
|
-
label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'family.title'),
|
|
332
|
-
locale: NS,
|
|
333
|
-
inject: () => ({ hooks: { tabs: familyTabsHooks } }),
|
|
334
|
-
children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },
|
|
335
|
-
}, StewardFamilySection)
|
|
336
|
-
} catch {
|
|
337
|
-
// 并发接管竞争落败(或设置壳未声明席位):胜出方的节服务整个家族。
|
|
338
|
-
}
|
|
339
|
-
}, FAMILY_HOST_GRACE_MS)
|
|
340
|
-
return () => { clearTimeout(timer); if (typeof claimed === 'function') (claimed as () => void)() }
|
|
341
|
-
}, 'dsh-manage-session: family fallback host')
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/** 家族节固定 id(与 thinking-levels / guard 的注册严格一致)。 */
|
|
345
|
-
const FAMILY_SECTION_ID = 'dsh-family'
|
|
346
|
-
/** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
|
|
347
|
-
const FAMILY_CHILD_KEY = 'dsh-family.tab'
|
|
348
|
-
/** 家族容器接管分级——唯一真源 `mine-dsh-plugins/_tools/family-rank.mjs`(离线实测
|
|
349
|
-
* GitHub star + npm 周下载量加权)。延迟 = BASE + RANK * STEP。 */
|
|
350
|
-
const FAMILY_CLAIM_RANK = 3
|
|
351
|
-
const FAMILY_CLAIM_BASE_MS = 200
|
|
352
|
-
const FAMILY_CLAIM_STEP_MS = 900
|
|
353
|
-
/** 接管宽限期(由分级派生)。 */
|
|
354
|
-
const FAMILY_HOST_GRACE_MS = FAMILY_CLAIM_BASE_MS + FAMILY_CLAIM_RANK * FAMILY_CLAIM_STEP_MS
|
|
355
|
-
|
|
356
|
-
/** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形)。 */
|
|
357
|
-
function makeFamilyTabsHooks(slots: StewardSlotsService): {
|
|
358
|
-
getSnapshot: () => readonly FamilyTabEntry[]
|
|
359
|
-
subscribe: (listener: () => void) => () => void
|
|
360
|
-
} {
|
|
361
|
-
let version = -1
|
|
362
|
-
let tabs: readonly FamilyTabEntry[] = []
|
|
363
|
-
return {
|
|
364
|
-
getSnapshot: () => {
|
|
365
|
-
const next = slots.getVersion(FAMILY_CHILD_KEY)
|
|
366
|
-
if (next !== version) {
|
|
367
|
-
version = next
|
|
368
|
-
tabs = slots.entries(FAMILY_CHILD_KEY)
|
|
369
|
-
.map(entry => ({
|
|
370
|
-
id: entry.options.id ?? '',
|
|
371
|
-
order: entry.options.order ?? 0,
|
|
372
|
-
label: typeof entry.options.label === 'function'
|
|
373
|
-
? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()
|
|
374
|
-
: String(entry.options.label ?? entry.options.id ?? ''),
|
|
375
|
-
}))
|
|
376
|
-
.sort((a, b) => a.order - b.order)
|
|
377
|
-
}
|
|
378
|
-
return tabs
|
|
379
|
-
},
|
|
380
|
-
subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/** 账本条目(id/order/label,label 已解析为字符串)。 */
|
|
385
|
-
interface FamilyTabEntry {
|
|
386
|
-
id: string
|
|
387
|
-
order: number
|
|
388
|
-
label: string
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/** 家族节接管组件:纯通用渲染——把 `dsh-family.tab` 账本里的每张贡献卡按 order 渲染。 */
|
|
392
|
-
function StewardFamilySection(props: {
|
|
393
|
-
renderSlot: (key: string, owner?: object, opts?: { only?: string; fallback?: unknown }) => unknown
|
|
394
|
-
hooks: { tabs: { getSnapshot: () => readonly FamilyTabEntry[]; subscribe: (listener: () => void) => () => void } }
|
|
395
|
-
}): ReactElement {
|
|
396
|
-
const tabs = useSyncExternalStore(props.hooks.tabs.subscribe, props.hooks.tabs.getSnapshot)
|
|
397
|
-
return createElement('div', { style: { display: 'grid', gap: '12px' } },
|
|
398
|
-
...tabs.map(row => createElement('div', { key: row.id },
|
|
399
|
-
props.renderSlot(FAMILY_CHILD_KEY, {}, { only: row.id, fallback: null }) as ReactElement,
|
|
400
|
-
)),
|
|
401
|
-
)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/** 入口按钮:持有面板开关;两个页签的可见性来自配置快照。 */
|
|
405
|
-
export function StewardEntry(
|
|
406
|
-
props: StewardFooterProps & {
|
|
407
|
-
scope?: (StewardCardScope & { getSnapshot(): { value: StewardConfig | undefined } }) | undefined
|
|
408
|
-
openSession?: (sessionId: string) => void
|
|
409
|
-
},
|
|
410
|
-
): ReactElement {
|
|
411
|
-
const [open, setOpen] = useState(false)
|
|
412
|
-
const snapshot = useSyncExternalStore(
|
|
413
|
-
(listener) => (props.scope !== undefined ? props.scope.subscribe(listener) : () => {}),
|
|
414
|
-
() => (props.scope !== undefined ? props.scope.getSnapshot() : { value: DEFAULT_CONFIG }),
|
|
415
|
-
)
|
|
416
|
-
const config: StewardConfig = { ...DEFAULT_CONFIG, ...(snapshot.value ?? {}) }
|
|
417
|
-
const healthCheck = config.healthCheck !== false
|
|
418
|
-
const visible = config.enabled !== false && healthCheck
|
|
419
|
-
const wide = props.wide === true
|
|
420
|
-
// 入口文案与面板标题同源:字典是唯一出处,组件不再持有兜底中文。
|
|
421
|
-
const label = translate(undefined, 'panel.title')
|
|
422
|
-
|
|
423
|
-
if (!visible) return createElement('span', { className: 'dms_entryWrap' })
|
|
424
|
-
return createElement('span', { className: wide ? 'dms_entryWrap dms_entryWrapWide' : 'dms_entryWrap' }, [
|
|
425
|
-
createElement(StewardFooter, {
|
|
426
|
-
key: 'btn',
|
|
427
|
-
title: label,
|
|
428
|
-
label,
|
|
429
|
-
wide,
|
|
430
|
-
onClick: () => { setOpen(!open) },
|
|
431
|
-
}),
|
|
432
|
-
open
|
|
433
|
-
? createElement(StewardPanel, {
|
|
434
|
-
key: 'panel',
|
|
435
|
-
healthCheck,
|
|
436
|
-
openSession: props.openSession,
|
|
437
|
-
onClose: () => { setOpen(false) },
|
|
438
|
-
})
|
|
439
|
-
: null,
|
|
440
|
-
])
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/* ------------------------------------------------ 侧栏入口位次(顶栏工具槽 / 底栏) */
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* 顶栏工具槽位:面板行列表。宿主在「插件」(order 0)与「更多」启动器之间渲染
|
|
447
|
-
* 这些行,行内 icon + label 默认左对齐;点击切换主栏面板。
|
|
448
|
-
*/
|
|
449
|
-
const TOP_TOOL_SEAT = 'sidebar.panellist'
|
|
450
|
-
/** 主栏整页槽位(keyed):面板行点击后在此渲染整页内容。 */
|
|
451
|
-
const MAIN_SEAT = 'main'
|
|
452
|
-
/** 管理台页 id(同时作为 `sidebar.panellist` 行的 id 与 `main` 的 key)。 */
|
|
453
|
-
const MANAGER_PANEL_ID = 'manage-session'
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* 宿主是否提供顶栏工具槽位(诊断用):面板行席位 + 主栏整页席位同时被声明才算。
|
|
457
|
-
* 实际位次判定走 apply 内的 inject 探测(服务面不保证暴露 specDynamic)。
|
|
458
|
-
* @param slots - 槽位服务面。
|
|
459
|
-
* @returns 两席都已声明时 `true`。
|
|
460
|
-
*/
|
|
461
|
-
function hasPanelToolSeat(slots: StewardSlotsService): boolean {
|
|
462
|
-
if (typeof slots.specDynamic !== 'function') return false
|
|
463
|
-
try {
|
|
464
|
-
return slots.specDynamic(TOP_TOOL_SEAT) !== undefined && slots.specDynamic(MAIN_SEAT) !== undefined
|
|
465
|
-
} catch {
|
|
466
|
-
return false
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
export { hasPanelToolSeat }
|
|
471
|
-
|
|
472
|
-
/** 组合注销函数(逆序执行,幂等)。 */
|
|
473
|
-
function composeDisposers(offs: Array<() => void>): () => void {
|
|
474
|
-
let done = false
|
|
475
|
-
return () => {
|
|
476
|
-
if (done) return
|
|
477
|
-
done = true
|
|
478
|
-
for (const off of offs.reverse()) {
|
|
479
|
-
try { off() } catch { /* 单个席位解装失败不阻断其余回收 */ }
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* 按开关状态维持一对注册(主栏页 + 面板行)。快照缺字段一律按「可见」处理。
|
|
486
|
-
* @param scope - 设置 scope(缺失时视为缺省值)。
|
|
487
|
-
* @param visible - 可见性判定。
|
|
488
|
-
* @param mount - 可见时注册并返回注销函数。
|
|
489
|
-
* @returns 解装函数。
|
|
490
|
-
*/
|
|
491
|
-
function gatedMount<V extends object>(
|
|
492
|
-
scope: BridgeScope<V> | undefined,
|
|
493
|
-
visible: (value: Partial<V>) => boolean,
|
|
494
|
-
mount: () => () => void,
|
|
495
|
-
): () => void {
|
|
496
|
-
let dispose: (() => void) | undefined
|
|
497
|
-
const apply = (): void => {
|
|
498
|
-
const value = (scope?.getSnapshot?.().value ?? {}) as Partial<V>
|
|
499
|
-
const want = visible(value)
|
|
500
|
-
if (want && dispose === undefined) dispose = mount()
|
|
501
|
-
else if (!want && dispose !== undefined) {
|
|
502
|
-
const off = dispose
|
|
503
|
-
dispose = undefined
|
|
504
|
-
off()
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
apply()
|
|
508
|
-
const unsubscribe = scope?.subscribe(apply)
|
|
509
|
-
return () => {
|
|
510
|
-
unsubscribe?.()
|
|
511
|
-
if (dispose !== undefined) {
|
|
512
|
-
const off = dispose
|
|
513
|
-
dispose = undefined
|
|
514
|
-
off()
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/** 底栏回落形态(老宿主 / 无顶栏工具行):一枚入口开对话框。 */
|
|
520
|
-
function mountFooterEntries(
|
|
521
|
-
slots: StewardSlotsService,
|
|
522
|
-
bound: BridgeScope<StewardConfig>,
|
|
523
|
-
openSession: (sessionId: string) => void,
|
|
524
|
-
): () => void {
|
|
525
|
-
const off = slots.inject('sidebar.footer.action', () => slots.register(
|
|
526
|
-
{ name: 'sidebar.footer.action', id: STEWARD_ENTRY_ID, order: 6 },
|
|
527
|
-
(props: StewardFooterProps) => createElement(StewardEntry, {
|
|
528
|
-
...props,
|
|
529
|
-
scope: bound as unknown as StewardCardScope,
|
|
530
|
-
openSession,
|
|
531
|
-
}),
|
|
532
|
-
), 'dsh-manage-session: sidebar footer entry')
|
|
533
|
-
return composeDisposers([off])
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* 顶栏工具行形态:面板行(order 6,落在「插件」行之后、「更多」之前)加主栏整页。
|
|
538
|
-
* 点击行 → 宿主 `selectPanel(id)` 在主栏渲染整页;页内关闭动作回退到会话。
|
|
539
|
-
* @param closePanel - 关闭页回退动作(`layout.selectPanel(null)`)。
|
|
540
|
-
*/
|
|
541
|
-
function mountTopToolEntries(
|
|
542
|
-
slots: StewardSlotsService,
|
|
543
|
-
bound: BridgeScope<StewardConfig>,
|
|
544
|
-
openSession: (sessionId: string) => void,
|
|
545
|
-
closePanel: () => void,
|
|
546
|
-
): () => void {
|
|
547
|
-
/** 管理页:管理台 / 体检 双页签,页签可见性与底栏入口同源。 */
|
|
548
|
-
const ManagerPage = (): ReactElement => {
|
|
549
|
-
const snapshot = useSyncExternalStore(
|
|
550
|
-
(listener) => bound.subscribe(listener),
|
|
551
|
-
() => bound.getSnapshot(),
|
|
552
|
-
)
|
|
553
|
-
const config: StewardConfig = { ...DEFAULT_CONFIG, ...(snapshot.value ?? {}) }
|
|
554
|
-
return createElement(StewardPanel, {
|
|
555
|
-
variant: 'page',
|
|
556
|
-
healthCheck: config.healthCheck !== false,
|
|
557
|
-
openSession,
|
|
558
|
-
onClose: closePanel,
|
|
559
|
-
})
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const off = gatedMount(bound, value => value.enabled !== false && value.healthCheck !== false, () =>
|
|
563
|
-
composeDisposers([
|
|
564
|
-
slots.inject(MAIN_SEAT, () => slots.register(
|
|
565
|
-
{ name: MAIN_SEAT, key: MANAGER_PANEL_ID, locale: NS },
|
|
566
|
-
ManagerPage,
|
|
567
|
-
), `dsh-manage-session: main page ${MANAGER_PANEL_ID}`),
|
|
568
|
-
slots.inject(TOP_TOOL_SEAT, () => slots.register(
|
|
569
|
-
{
|
|
570
|
-
name: TOP_TOOL_SEAT,
|
|
571
|
-
id: MANAGER_PANEL_ID,
|
|
572
|
-
order: 6,
|
|
573
|
-
label: () => translate(undefined, 'card.title'),
|
|
574
|
-
locale: NS,
|
|
575
|
-
},
|
|
576
|
-
ManagerToolIcon,
|
|
577
|
-
), `dsh-manage-session: top tool row ${MANAGER_PANEL_ID}`),
|
|
578
|
-
]))
|
|
579
|
-
|
|
580
|
-
return off
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/** 顶栏行图标:会话管理(列表行 + 勾选,随行字色)。 */
|
|
584
|
-
function ManagerToolIcon({ size }: { size?: number }): ReactElement {
|
|
585
|
-
const edge = size ?? 16
|
|
586
|
-
return createElement('svg', {
|
|
587
|
-
width: edge, height: edge, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true,
|
|
588
|
-
},
|
|
589
|
-
createElement('path', { d: 'M2 4h12M2 8h12M2 12h7', stroke: 'currentColor', strokeWidth: 1.75, strokeLinecap: 'round' }),
|
|
590
|
-
createElement('path', { d: 'M11.2 11.2l1.5 1.5 2.6-3', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }))
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/** 导出字典查找 type 供消费者复用。 */
|
|
594
|
-
export type { LocaleKey }
|
|
1
|
+
/**
|
|
2
|
+
* 会话管理客户端半身:样式、字典、侧栏入口(顶栏工具槽 / 底栏回落)与设置卡。
|
|
3
|
+
*
|
|
4
|
+
* 入口只有一个(会话管理):点开是「管理 / 体检」双页签。搜索子域已拆回
|
|
5
|
+
* dsh-search-index —— 本文件不再注册搜索入口、搜索面板或搜索设置卡。
|
|
6
|
+
*
|
|
7
|
+
* 入口位次由宿主能力决定(inject 探测两席位):
|
|
8
|
+
* - 顶栏工具行(`sidebar.panellist` 面板行 + `main` 整页槽)→ 面板行落在「插件」
|
|
9
|
+
* 之后、「更多」之前,点击在主栏整页打开;
|
|
10
|
+
* - 无该槽位(老线)→ 回落底栏 `sidebar.footer.action`,点击开对话框。
|
|
11
|
+
*
|
|
12
|
+
* 开关读取:configForms(0.1.7,entry id 键)/ legacy 宿主走自家 webServer 设置桥
|
|
13
|
+
* (settingsScope.bind 是数据死路,见 bridge-scope.ts);配置写入同链路。
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from 'cordis'
|
|
16
|
+
import { createElement, useState, useSyncExternalStore, type ReactElement } from 'react'
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_CONFIG,
|
|
19
|
+
STEWARD_ENTRY_ID,
|
|
20
|
+
STEWARD_SETTINGS_NAMESPACE,
|
|
21
|
+
type StewardConfig,
|
|
22
|
+
} from '../config.ts'
|
|
23
|
+
import { StewardCardScope, StewardSettingsCard, type CardTranslate } from './card.tsx'
|
|
24
|
+
import { stewardBridgeScope, withBridgeFallback, type BridgeScope } from './bridge-scope.ts'
|
|
25
|
+
import { dictionaries, translate, type LocaleKey } from './locales.ts'
|
|
26
|
+
import { StewardFooter, StewardPanel, TAB_HEALTH } from './panel.tsx'
|
|
27
|
+
import { openSessionThrough } from './host-api.ts'
|
|
28
|
+
import {
|
|
29
|
+
SESSION_MENU_DELETE_ID, SESSION_MENU_DELETE_ORDER, SESSION_MENU_SEAT, StewardDeleteMenuItem,
|
|
30
|
+
} from './session-menu.tsx'
|
|
31
|
+
import {
|
|
32
|
+
installLegacySessionMenuDelete, makeDirectDeleteProbe, purgeSessionById,
|
|
33
|
+
} from './session-menu-legacy.ts'
|
|
34
|
+
|
|
35
|
+
export { STEWARD_ENTRY_ID, STEWARD_SETTINGS_NAMESPACE, TAB_HEALTH }
|
|
36
|
+
export { translate } from './locales.ts'
|
|
37
|
+
|
|
38
|
+
/** 字典命名空间(locale.register 用)。 */
|
|
39
|
+
export const NS = 'dsh-manage-session'
|
|
40
|
+
|
|
41
|
+
/** 槽位服务面(结构化镜像)。 */
|
|
42
|
+
interface StewardSlotsService {
|
|
43
|
+
inject(name: string, callback: () => unknown, label?: string): () => void
|
|
44
|
+
register(config: Record<string, unknown>, component: unknown): unknown
|
|
45
|
+
entries(name: string): Array<{ options: { id?: string; key?: string; order?: number; label?: unknown } }>
|
|
46
|
+
getVersion(name: string): number
|
|
47
|
+
subscribe(name: string, listener: () => void): () => void
|
|
48
|
+
/** 槽位声明探测(部分宿主暴露;仅诊断用)。 */
|
|
49
|
+
specDynamic?(name: string): unknown
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** locale 服务面。 */
|
|
53
|
+
interface StewardLocaleService {
|
|
54
|
+
register(ns: string, dicts: Record<string, Record<string, string>>): () => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** configForms 服务面(0.1.7:以 profile entry id 取句柄)。 */
|
|
58
|
+
interface StewardConfigForms {
|
|
59
|
+
get<U>(entryId: string): StewardCardScope & { getSnapshot(): { value: U | undefined } }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 客户端插件声明的注入面。 */
|
|
63
|
+
export const inject = ['slots', 'locale']
|
|
64
|
+
|
|
65
|
+
/** 侧边栏入口的 props(结构子集)。 */
|
|
66
|
+
export interface StewardFooterProps {
|
|
67
|
+
/** 侧边栏是否渲染宽栏内容(false = 56px 收起轨道)。 */
|
|
68
|
+
wide?: boolean
|
|
69
|
+
onClick?: () => void
|
|
70
|
+
title?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 注入样式(幂等)。 */
|
|
74
|
+
function injectStyles(): () => void {
|
|
75
|
+
const id = 'dsh-manage-session-styles'
|
|
76
|
+
if (typeof document === 'undefined') return () => {}
|
|
77
|
+
if (document.getElementById(id) !== null) return () => {}
|
|
78
|
+
const style = document.createElement('style')
|
|
79
|
+
style.id = id
|
|
80
|
+
style.textContent = `
|
|
81
|
+
/* 侧栏 footer 槽位公约(2026-09-26;2026-10-01 收紧行距并强制居中):一行多
|
|
82
|
+
入口(第三方 dsh-context 等)会互相挤占 —— 宿主 .footerActions 是 nowrap
|
|
83
|
+
flex 行。这里允许容器换行,并把本插件入口钉成独占一整行(flex-basis:100%);
|
|
84
|
+
其余入口(含第三方)自然落到后续行,各行内部自行布局,谁也不挤谁。
|
|
85
|
+
justify-content:center 让行内所有入口(含不占满行的)统一居中;row-gap:0
|
|
86
|
+
配合入口自身 32px 高度压缩纵向占位。类名用 [class*=] 中段匹配:宿主是 CSS
|
|
87
|
+
Module 哈希类名(实测形如 hHd-Xa_footerActions —— <hash>_<name>,哈希在前),
|
|
88
|
+
中段跨版本稳定。 */
|
|
89
|
+
[class*="footerActions"]{flex-wrap:wrap;justify-content:center;row-gap:0;height:auto;min-height:0}
|
|
90
|
+
/* —— 第三方矫正:dsh-context「上下文洞察」入口(2026-10-01)——
|
|
91
|
+
.lc-ov-entry 按"独占整行"设计(width:calc(100% + 4px)、无 justify-content、
|
|
92
|
+
42px 高、不对称 padding),与本槽位公约(每个入口独占一行、行内居中、32px)
|
|
93
|
+
冲突,表现为文字靠左、纵向松散。这里按公约强制矫正;:not() 排除收起轨道的
|
|
94
|
+
36px 圆钮形态。lc-ov-* 是 dsh-context 源码硬编码类名(非构建哈希),跨版本
|
|
95
|
+
稳定(实测 0.56.1 / 0.60.0 规则一致)。两个自有插件各自携带同一矫正块:
|
|
96
|
+
任一被禁用时矫正仍生效。 */
|
|
97
|
+
.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}
|
|
98
|
+
.lc-ov-entry-label{flex:0 1 auto!important}
|
|
99
|
+
/* 宽栏:独占一整行、放弃主动收缩(flex-shrink:0),行内居中;收起轨道回落自然宽度。 */
|
|
100
|
+
.dms_entryWrap{flex:0 1 auto;display:inline-flex;align-items:center;min-width:0}
|
|
101
|
+
.dms_entryWrapWide{flex:0 0 100%;width:100%;justify-content:center;margin-left:0}
|
|
102
|
+
/* 高度 32px(内容 22px 行高 + 上下各 5px):宿主默认 42px 的上下裕度在多行
|
|
103
|
+
堆叠后过于松散;左右内边距对称(10px/10px),否则整行居中时按钮内容会因
|
|
104
|
+
不对称 padding 向左偏 1px。 */
|
|
105
|
+
.dms_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}
|
|
106
|
+
.dms_footerEntry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
107
|
+
.dms_footerEntryRail{width:28px;height:28px;padding:0;gap:0;border-radius:50%}
|
|
108
|
+
.dms_footerIcon{flex:none;font-size:15px;line-height:1}
|
|
109
|
+
.dms_footerLabel{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
110
|
+
.dms_backdrop{position:fixed;inset:0;background:rgba(15,20,30,.42);z-index:1000}
|
|
111
|
+
.dms_panel{position:fixed;z-index:1001;left:50%;top:6vh;transform:translateX(-50%);width:min(860px,94vw);max-height:86vh;overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(0,0,0,.18);padding:10px 14px 14px}
|
|
112
|
+
/* 整页形态(顶栏工具行入口):取消浮动定位与遮罩观感,填满主栏。 */
|
|
113
|
+
.dms_panel.dms_panelPage{position:relative;left:auto;top:auto;transform:none;z-index:auto;width:100%;max-width:none;height:100%;max-height:none;box-sizing:border-box;border:none;border-radius:0;box-shadow:none;background:transparent;padding:14px 18px 16px}
|
|
114
|
+
.dms_dialogHead{display:flex;align-items:center;gap:10px;min-height:34px}
|
|
115
|
+
.dms_dialogTitle{font-weight:600;color:var(--dsw-alias-label-primary,#111827);flex:1 1 auto;min-width:0}
|
|
116
|
+
.dms_tabRow{display:inline-flex;gap:4px}
|
|
117
|
+
.dms_tab{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:3px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
|
|
118
|
+
.dms_tabActive{background:var(--dsw-alias-interactive-bg-active,rgba(63,99,216,.12));color:var(--dsw-alias-label-primary,#111827)}
|
|
119
|
+
.dms_tabBody{display:flex;flex-direction:column;gap:8px;padding-top:8px}
|
|
120
|
+
.dms_actBtn{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;border-radius:8px;padding:4px 10px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-primary,#111827)}
|
|
121
|
+
.dms_actBtn:disabled{opacity:.5;cursor:default}
|
|
122
|
+
.dms_btnRow{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
|
|
123
|
+
.dms_list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}
|
|
124
|
+
.dms_row{display:flex;gap:8px;align-items:center;padding:5px 8px;border-radius:8px;border:1px solid transparent}
|
|
125
|
+
.dms_row:hover{border-color:var(--dsw-alias-border-l2,#e5e7eb)}
|
|
126
|
+
.dms_meta{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
|
|
127
|
+
.dms_uuid{font-family:var(--ds-font-family-code,monospace)}
|
|
128
|
+
/* 按行的磁盘占用:右对齐、不参与标题压缩,方便纵向比对「哪条最占地方」。 */
|
|
129
|
+
.dms_status{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
|
|
130
|
+
.dms_error{font-size:12px;color:var(--dsw-alias-state-error-primary,#dc2626)}
|
|
131
|
+
.dms_empty{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af);padding:10px 2px}
|
|
132
|
+
.dms_gateList{display:flex;flex-direction:column;gap:6px}
|
|
133
|
+
.dms_gate{display:flex;flex-wrap:wrap;gap:6px;align-items:baseline;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 10px}
|
|
134
|
+
.dms_gateName{font-weight:600;font-size:12px}
|
|
135
|
+
.dms_gateEvidence{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1 1 100%}
|
|
136
|
+
.dms_levelPill{font-size:11px;border-radius:999px;padding:1px 8px;border:1px solid transparent}
|
|
137
|
+
.dms_level_ok{background:rgba(22,163,74,.12);color:#15803d}
|
|
138
|
+
.dms_level_warn{background:rgba(217,119,6,.14);color:#b45309}
|
|
139
|
+
.dms_level_fail{background:rgba(220,38,38,.14);color:#b91c1c}
|
|
140
|
+
.dms_level_skipped{background:rgba(107,114,128,.14);color:#4b5563}
|
|
141
|
+
.dms_progressRow{display:flex;align-items:center;gap:8px;margin-top:2px}
|
|
142
|
+
.dms_progressTrack{flex:1;height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#eef0f3);overflow:hidden}
|
|
143
|
+
.dms_progressFill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary,#3d5be0);transition:width .2s ease}
|
|
144
|
+
.dms_progressIndeterminate{width:35%;animation:dssSlide 1.1s ease-in-out infinite}
|
|
145
|
+
.dms_why{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1;min-width:0}
|
|
146
|
+
.dms_cacheRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px}
|
|
147
|
+
.dms_spinner{width:11px;height:11px;margin-right:6px;border-radius:50%;display:inline-block;vertical-align:-1px;border:2px solid currentColor;border-top-color:transparent;animation:dssSpin .7s linear infinite}
|
|
148
|
+
@keyframes dssSpin{to{transform:rotate(360deg)}}
|
|
149
|
+
@keyframes dssSlide{0%{margin-left:-35%}100%{margin-left:100%}}
|
|
150
|
+
@media (prefers-reduced-motion:reduce){.dms_spinner{animation:none}.dms_progressIndeterminate{animation:none;width:100%;opacity:.5}.dms_setChev{transition:none}.dms_setCard{transition:none}}
|
|
151
|
+
.dms_verdict{font-size:12px;line-height:18px;border-radius:8px;padding:6px 10px;margin-top:6px;border:1px solid transparent}
|
|
152
|
+
.dms_verdict_repaired{background:rgba(22,163,74,.10);color:#15803d;border-color:rgba(22,163,74,.28)}
|
|
153
|
+
.dms_verdict_repaired-with-residual{background:rgba(217,119,6,.10);color:#b45309;border-color:rgba(217,119,6,.28)}
|
|
154
|
+
.dms_verdict_nothing-to-do{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
|
|
155
|
+
.dms_verdict_not-applicable{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
|
|
156
|
+
.dms_verdict_failed{background:rgba(220,38,38,.10);color:#b91c1c;border-color:rgba(220,38,38,.28)}
|
|
157
|
+
.dms_phaseRow{display:flex;gap:8px;align-items:center}
|
|
158
|
+
.dms_phase{font-weight:600;font-size:12px}
|
|
159
|
+
.dms_cmdBlock{display:flex;flex-direction:column;gap:4px;border:1px dashed var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 8px}
|
|
160
|
+
.dms_cmdLine{display:flex;gap:6px;align-items:center}
|
|
161
|
+
.dms_cmd{flex:1 1 auto;font-family:var(--ds-font-family-code,monospace);font-size:11px;white-space:pre-wrap;word-break:break-all}
|
|
162
|
+
.dms_setCard{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.05));border-radius:12px;transition:border-color .16s,background .16s}
|
|
163
|
+
.dms_setHead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:none;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}
|
|
164
|
+
.dms_setHeadText{display:flex;flex-direction:column;gap:2px;flex:1 1 0%;min-width:0}
|
|
165
|
+
.dms_setCardTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#111827)}
|
|
166
|
+
.dms_setCardDesc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9ca3af)}
|
|
167
|
+
.dms_setChev{color:var(--dsw-alias-label-tertiary,#9ca3af);flex:0 0 auto;transition:transform .16s}
|
|
168
|
+
.dms_setCardOpen .dms_setChev{transform:rotate(180deg)}
|
|
169
|
+
.dms_setBody{display:flex;flex-direction:column;gap:2px;padding:0 16px 12px}
|
|
170
|
+
.dms_setRow{display:flex;align-items:center;gap:10px;padding:6px 0}
|
|
171
|
+
.dms_setText{display:flex;flex-direction:column;flex:1 1 auto;min-width:0}
|
|
172
|
+
.dms_setTitle{font-size:13px;color:var(--dsw-alias-label-primary,#111827)}
|
|
173
|
+
.dms_setDesc{font-size:12px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
|
|
174
|
+
.dms_toggle{display:inline-flex;align-items:center;cursor:pointer;position:relative}
|
|
175
|
+
.dms_toggle input{position:absolute;opacity:0;width:0;height:0}
|
|
176
|
+
.dms_toggleTrack{width:34px;height:18px;border-radius:999px;background:var(--dsw-alias-border-l2,#d1d5db);position:relative;transition:background .15s ease}
|
|
177
|
+
.dms_toggleKnob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:left .15s ease}
|
|
178
|
+
.dms_toggleOn .dms_toggleTrack{background:var(--dsw-alias-button-primary-fill,#3f63d8)}
|
|
179
|
+
.dms_toggleOn .dms_toggleKnob{left:18px}
|
|
180
|
+
`
|
|
181
|
+
document.head.appendChild(style)
|
|
182
|
+
return () => { style.remove() }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 客户端插件主体。
|
|
187
|
+
* @param ctx - 客户端上下文(slots、可选 locale / configForms)。
|
|
188
|
+
*/
|
|
189
|
+
export function apply(ctx: Context): void {
|
|
190
|
+
ctx.effect(() => injectStyles(), 'dsh-manage-session: stylesheet')
|
|
191
|
+
|
|
192
|
+
const locale = ctx.get('locale') as StewardLocaleService | undefined
|
|
193
|
+
if (locale !== undefined && typeof locale.register === 'function') {
|
|
194
|
+
ctx.effect(() => locale.register(NS, dictionaries), 'dsh-manage-session: dictionaries')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 数据面回退链(台账 A2 定案 + 0.1.7 实测补丁):首选 configForms 原生句柄
|
|
198
|
+
// (entry id 键),**渲染期**观测到 unavailable(出生死或 loading→死,A2 死
|
|
199
|
+
// 路签名)就地落桥——自家 webServer 设置桥(settings-describe/mutate)。
|
|
200
|
+
const configForms = ctx.get('configForms') as StewardConfigForms | undefined
|
|
201
|
+
const bound = withBridgeFallback(
|
|
202
|
+
configForms?.get<StewardConfig>(STEWARD_ENTRY_ID) as BridgeScope<StewardConfig> | undefined,
|
|
203
|
+
stewardBridgeScope(),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const slots = ctx.get('slots') as StewardSlotsService | undefined
|
|
207
|
+
if (slots === undefined) return
|
|
208
|
+
|
|
209
|
+
// 管理台行点击打开会话:click 时经 openSessionThrough 解析(uiWorkspace.openSession
|
|
210
|
+
// 0.1.7+ 优先,旧宿主回退 sessions.open)—— 应用序早于导航模块,必须惰性取。
|
|
211
|
+
const openSession = (sessionId: string): void => {
|
|
212
|
+
openSessionThrough((name) => ctx.get(name), sessionId)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 顶栏页的关闭动作:回退到会话(layout 服务惰性取——装载序不保证)。
|
|
216
|
+
const closeToolPanel = (): void => {
|
|
217
|
+
try {
|
|
218
|
+
const layout = ctx.get('layout') as { selectPanel?: (id: string | null) => void } | undefined
|
|
219
|
+
layout?.selectPanel?.(null)
|
|
220
|
+
} catch {
|
|
221
|
+
/* 布局服务缺席:页面保留,用户可点「会话」行返回 */
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 侧栏入口(一枚)——**位次由宿主能力决定**:顶栏工具行 or 底栏回落。
|
|
226
|
+
// 席位能力用 inject 探测(席位被声明时才回调),不依赖 spec 查询。
|
|
227
|
+
ctx.effect(() => {
|
|
228
|
+
let rowSeat = false
|
|
229
|
+
let mainSeat = false
|
|
230
|
+
let footerOff: (() => void) | undefined
|
|
231
|
+
let topOff: (() => void) | undefined
|
|
232
|
+
|
|
233
|
+
const sync = (): void => {
|
|
234
|
+
const topReady = rowSeat && mainSeat
|
|
235
|
+
if (topReady && topOff === undefined) {
|
|
236
|
+
if (footerOff !== undefined) { const off = footerOff; footerOff = undefined; off() }
|
|
237
|
+
topOff = mountTopToolEntries(slots, bound, openSession, closeToolPanel)
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
if (!topReady && footerOff === undefined && topOff === undefined) {
|
|
241
|
+
footerOff = mountFooterEntries(slots, bound, openSession)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 接线期不让 probe 回调触发 sync:两席位先后落位时先到的会误判「顶栏不齐」。
|
|
246
|
+
let wired = false
|
|
247
|
+
const onSeat = (): void => { if (wired) sync() }
|
|
248
|
+
|
|
249
|
+
const offRowProbe = slots.inject(TOP_TOOL_SEAT, () => {
|
|
250
|
+
rowSeat = true
|
|
251
|
+
onSeat()
|
|
252
|
+
return () => { rowSeat = false }
|
|
253
|
+
}, 'dsh-manage-session: probe top tool seat')
|
|
254
|
+
|
|
255
|
+
const offMainProbe = slots.inject(MAIN_SEAT, () => {
|
|
256
|
+
mainSeat = true
|
|
257
|
+
onSeat()
|
|
258
|
+
return () => { mainSeat = false }
|
|
259
|
+
}, 'dsh-manage-session: probe main page seat')
|
|
260
|
+
|
|
261
|
+
wired = true
|
|
262
|
+
sync()
|
|
263
|
+
|
|
264
|
+
return () => {
|
|
265
|
+
offRowProbe()
|
|
266
|
+
offMainProbe()
|
|
267
|
+
if (footerOff !== undefined) { const off = footerOff; footerOff = undefined; off() }
|
|
268
|
+
if (topOff !== undefined) { const off = topOff; topOff = undefined; off() }
|
|
269
|
+
}
|
|
270
|
+
}, 'dsh-manage-session: sidebar entry')
|
|
271
|
+
|
|
272
|
+
// 插件族共用设置 tab(dsh-thinking-levels 的顶级「起子插件设置」节声明该子席位)。
|
|
273
|
+
// thinking-levels 缺席时本 inject 静默等待,不阻塞客户端半。
|
|
274
|
+
slots.inject('dsh-family.tab', () => slots.register(
|
|
275
|
+
{
|
|
276
|
+
name: 'dsh-family.tab',
|
|
277
|
+
id: STEWARD_ENTRY_ID,
|
|
278
|
+
order: 40,
|
|
279
|
+
// 账本 label:读期求值,跟随当前 locale(translate 内建 zh/en 兜底)。
|
|
280
|
+
label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'card.title'),
|
|
281
|
+
locale: NS,
|
|
282
|
+
},
|
|
283
|
+
(props: { t?: CardTranslate }) => createElement(StewardSettingsCard, {
|
|
284
|
+
scope: bound as StewardCardScope,
|
|
285
|
+
t: props.t,
|
|
286
|
+
}),
|
|
287
|
+
), 'dsh-manage-session: family settings tab')
|
|
288
|
+
|
|
289
|
+
// Plugins 页配置卡(`plugins.bundle.config`,key = 包名):0.1.7 宿主的 Plugins
|
|
290
|
+
// 页只渲染 key 与包名相等的条目。
|
|
291
|
+
slots.inject('plugins.bundle.config', () => slots.register(
|
|
292
|
+
{ name: 'plugins.bundle.config', key: STEWARD_ENTRY_ID },
|
|
293
|
+
(props: { t?: CardTranslate }) => createElement(StewardSettingsCard, {
|
|
294
|
+
scope: bound as StewardCardScope,
|
|
295
|
+
t: props.t,
|
|
296
|
+
}),
|
|
297
|
+
), 'dsh-manage-session: plugins page config card')
|
|
298
|
+
|
|
299
|
+
// 侧栏会话菜单删除臂(按左侧边栏换代识别):席位自 0.1.7 存在,本行 order=500
|
|
300
|
+
// 落末位;≤0.1.5 菜单写死无席位 → inject 静默等待,删除入口由 DOM 注入臂承担。
|
|
301
|
+
// 行可见性由组件自探针 directDelete(缺省关),非 true 不渲染。
|
|
302
|
+
slots.inject(SESSION_MENU_SEAT, () => slots.register(
|
|
303
|
+
{ name: SESSION_MENU_SEAT, id: SESSION_MENU_DELETE_ID, order: SESSION_MENU_DELETE_ORDER, locale: NS },
|
|
304
|
+
StewardDeleteMenuItem,
|
|
305
|
+
), 'dsh-manage-session: session menu delete row')
|
|
306
|
+
|
|
307
|
+
// 老线(≤0.1.5)菜单无席位——DOM 注入臂补上删除行。
|
|
308
|
+
if (typeof document !== 'undefined') {
|
|
309
|
+
const probe = makeDirectDeleteProbe()
|
|
310
|
+
ctx.effect(() => installLegacySessionMenuDelete({
|
|
311
|
+
doc: document,
|
|
312
|
+
purge: purgeSessionById,
|
|
313
|
+
isEnabled: probe,
|
|
314
|
+
lang: () => (typeof navigator !== 'undefined' ? navigator.language : undefined),
|
|
315
|
+
}), 'dsh-manage-session: legacy session menu delete injector')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 家族节宿主选举(次优先级):宿主 dsh-thinking-levels 缺席时顶上「起子插件
|
|
319
|
+
// 设置」节。下载量排名低于 session-guard → 宽限期更长,guard 先尝试接管;
|
|
320
|
+
// guard 也缺席时本插件按同一 id `dsh-family` 接管。
|
|
321
|
+
const familyTabsHooks = makeFamilyTabsHooks(slots)
|
|
322
|
+
ctx.effect(() => {
|
|
323
|
+
let claimed: unknown
|
|
324
|
+
const timer = setTimeout(() => {
|
|
325
|
+
if (slots.entries('settings.section').some(e => e.options.id === FAMILY_SECTION_ID)) return
|
|
326
|
+
try {
|
|
327
|
+
claimed = slots.register({
|
|
328
|
+
name: 'settings.section',
|
|
329
|
+
id: FAMILY_SECTION_ID,
|
|
330
|
+
order: 40,
|
|
331
|
+
label: () => translate((locale as { bind?: (n: string) => CardTranslate } | undefined)?.bind?.(NS), 'family.title'),
|
|
332
|
+
locale: NS,
|
|
333
|
+
inject: () => ({ hooks: { tabs: familyTabsHooks } }),
|
|
334
|
+
children: { [FAMILY_CHILD_KEY]: { kind: 'list', scope: 'root' } },
|
|
335
|
+
}, StewardFamilySection)
|
|
336
|
+
} catch {
|
|
337
|
+
// 并发接管竞争落败(或设置壳未声明席位):胜出方的节服务整个家族。
|
|
338
|
+
}
|
|
339
|
+
}, FAMILY_HOST_GRACE_MS)
|
|
340
|
+
return () => { clearTimeout(timer); if (typeof claimed === 'function') (claimed as () => void)() }
|
|
341
|
+
}, 'dsh-manage-session: family fallback host')
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** 家族节固定 id(与 thinking-levels / guard 的注册严格一致)。 */
|
|
345
|
+
const FAMILY_SECTION_ID = 'dsh-family'
|
|
346
|
+
/** 家族子席位 key(与 thinking-levels 的声明严格一致)。 */
|
|
347
|
+
const FAMILY_CHILD_KEY = 'dsh-family.tab'
|
|
348
|
+
/** 家族容器接管分级——唯一真源 `mine-dsh-plugins/_tools/family-rank.mjs`(离线实测
|
|
349
|
+
* GitHub star + npm 周下载量加权)。延迟 = BASE + RANK * STEP。 */
|
|
350
|
+
const FAMILY_CLAIM_RANK = 3
|
|
351
|
+
const FAMILY_CLAIM_BASE_MS = 200
|
|
352
|
+
const FAMILY_CLAIM_STEP_MS = 900
|
|
353
|
+
/** 接管宽限期(由分级派生)。 */
|
|
354
|
+
const FAMILY_HOST_GRACE_MS = FAMILY_CLAIM_BASE_MS + FAMILY_CLAIM_RANK * FAMILY_CLAIM_STEP_MS
|
|
355
|
+
|
|
356
|
+
/** 接管节的账本投影 hooks(与 thinking-levels 的 FamilySectionInjected 同形)。 */
|
|
357
|
+
function makeFamilyTabsHooks(slots: StewardSlotsService): {
|
|
358
|
+
getSnapshot: () => readonly FamilyTabEntry[]
|
|
359
|
+
subscribe: (listener: () => void) => () => void
|
|
360
|
+
} {
|
|
361
|
+
let version = -1
|
|
362
|
+
let tabs: readonly FamilyTabEntry[] = []
|
|
363
|
+
return {
|
|
364
|
+
getSnapshot: () => {
|
|
365
|
+
const next = slots.getVersion(FAMILY_CHILD_KEY)
|
|
366
|
+
if (next !== version) {
|
|
367
|
+
version = next
|
|
368
|
+
tabs = slots.entries(FAMILY_CHILD_KEY)
|
|
369
|
+
.map(entry => ({
|
|
370
|
+
id: entry.options.id ?? '',
|
|
371
|
+
order: entry.options.order ?? 0,
|
|
372
|
+
label: typeof entry.options.label === 'function'
|
|
373
|
+
? (() => { try { return String((entry.options.label as () => unknown)() ?? entry.options.id ?? '') } catch { return String(entry.options.id ?? '') } })()
|
|
374
|
+
: String(entry.options.label ?? entry.options.id ?? ''),
|
|
375
|
+
}))
|
|
376
|
+
.sort((a, b) => a.order - b.order)
|
|
377
|
+
}
|
|
378
|
+
return tabs
|
|
379
|
+
},
|
|
380
|
+
subscribe: (listener: () => void) => slots.subscribe(FAMILY_CHILD_KEY, listener),
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** 账本条目(id/order/label,label 已解析为字符串)。 */
|
|
385
|
+
interface FamilyTabEntry {
|
|
386
|
+
id: string
|
|
387
|
+
order: number
|
|
388
|
+
label: string
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** 家族节接管组件:纯通用渲染——把 `dsh-family.tab` 账本里的每张贡献卡按 order 渲染。 */
|
|
392
|
+
function StewardFamilySection(props: {
|
|
393
|
+
renderSlot: (key: string, owner?: object, opts?: { only?: string; fallback?: unknown }) => unknown
|
|
394
|
+
hooks: { tabs: { getSnapshot: () => readonly FamilyTabEntry[]; subscribe: (listener: () => void) => () => void } }
|
|
395
|
+
}): ReactElement {
|
|
396
|
+
const tabs = useSyncExternalStore(props.hooks.tabs.subscribe, props.hooks.tabs.getSnapshot)
|
|
397
|
+
return createElement('div', { style: { display: 'grid', gap: '12px' } },
|
|
398
|
+
...tabs.map(row => createElement('div', { key: row.id },
|
|
399
|
+
props.renderSlot(FAMILY_CHILD_KEY, {}, { only: row.id, fallback: null }) as ReactElement,
|
|
400
|
+
)),
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** 入口按钮:持有面板开关;两个页签的可见性来自配置快照。 */
|
|
405
|
+
export function StewardEntry(
|
|
406
|
+
props: StewardFooterProps & {
|
|
407
|
+
scope?: (StewardCardScope & { getSnapshot(): { value: StewardConfig | undefined } }) | undefined
|
|
408
|
+
openSession?: (sessionId: string) => void
|
|
409
|
+
},
|
|
410
|
+
): ReactElement {
|
|
411
|
+
const [open, setOpen] = useState(false)
|
|
412
|
+
const snapshot = useSyncExternalStore(
|
|
413
|
+
(listener) => (props.scope !== undefined ? props.scope.subscribe(listener) : () => {}),
|
|
414
|
+
() => (props.scope !== undefined ? props.scope.getSnapshot() : { value: DEFAULT_CONFIG }),
|
|
415
|
+
)
|
|
416
|
+
const config: StewardConfig = { ...DEFAULT_CONFIG, ...(snapshot.value ?? {}) }
|
|
417
|
+
const healthCheck = config.healthCheck !== false
|
|
418
|
+
const visible = config.enabled !== false && healthCheck
|
|
419
|
+
const wide = props.wide === true
|
|
420
|
+
// 入口文案与面板标题同源:字典是唯一出处,组件不再持有兜底中文。
|
|
421
|
+
const label = translate(undefined, 'panel.title')
|
|
422
|
+
|
|
423
|
+
if (!visible) return createElement('span', { className: 'dms_entryWrap' })
|
|
424
|
+
return createElement('span', { className: wide ? 'dms_entryWrap dms_entryWrapWide' : 'dms_entryWrap' }, [
|
|
425
|
+
createElement(StewardFooter, {
|
|
426
|
+
key: 'btn',
|
|
427
|
+
title: label,
|
|
428
|
+
label,
|
|
429
|
+
wide,
|
|
430
|
+
onClick: () => { setOpen(!open) },
|
|
431
|
+
}),
|
|
432
|
+
open
|
|
433
|
+
? createElement(StewardPanel, {
|
|
434
|
+
key: 'panel',
|
|
435
|
+
healthCheck,
|
|
436
|
+
openSession: props.openSession,
|
|
437
|
+
onClose: () => { setOpen(false) },
|
|
438
|
+
})
|
|
439
|
+
: null,
|
|
440
|
+
])
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* ------------------------------------------------ 侧栏入口位次(顶栏工具槽 / 底栏) */
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 顶栏工具槽位:面板行列表。宿主在「插件」(order 0)与「更多」启动器之间渲染
|
|
447
|
+
* 这些行,行内 icon + label 默认左对齐;点击切换主栏面板。
|
|
448
|
+
*/
|
|
449
|
+
const TOP_TOOL_SEAT = 'sidebar.panellist'
|
|
450
|
+
/** 主栏整页槽位(keyed):面板行点击后在此渲染整页内容。 */
|
|
451
|
+
const MAIN_SEAT = 'main'
|
|
452
|
+
/** 管理台页 id(同时作为 `sidebar.panellist` 行的 id 与 `main` 的 key)。 */
|
|
453
|
+
const MANAGER_PANEL_ID = 'manage-session'
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 宿主是否提供顶栏工具槽位(诊断用):面板行席位 + 主栏整页席位同时被声明才算。
|
|
457
|
+
* 实际位次判定走 apply 内的 inject 探测(服务面不保证暴露 specDynamic)。
|
|
458
|
+
* @param slots - 槽位服务面。
|
|
459
|
+
* @returns 两席都已声明时 `true`。
|
|
460
|
+
*/
|
|
461
|
+
function hasPanelToolSeat(slots: StewardSlotsService): boolean {
|
|
462
|
+
if (typeof slots.specDynamic !== 'function') return false
|
|
463
|
+
try {
|
|
464
|
+
return slots.specDynamic(TOP_TOOL_SEAT) !== undefined && slots.specDynamic(MAIN_SEAT) !== undefined
|
|
465
|
+
} catch {
|
|
466
|
+
return false
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export { hasPanelToolSeat }
|
|
471
|
+
|
|
472
|
+
/** 组合注销函数(逆序执行,幂等)。 */
|
|
473
|
+
function composeDisposers(offs: Array<() => void>): () => void {
|
|
474
|
+
let done = false
|
|
475
|
+
return () => {
|
|
476
|
+
if (done) return
|
|
477
|
+
done = true
|
|
478
|
+
for (const off of offs.reverse()) {
|
|
479
|
+
try { off() } catch { /* 单个席位解装失败不阻断其余回收 */ }
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 按开关状态维持一对注册(主栏页 + 面板行)。快照缺字段一律按「可见」处理。
|
|
486
|
+
* @param scope - 设置 scope(缺失时视为缺省值)。
|
|
487
|
+
* @param visible - 可见性判定。
|
|
488
|
+
* @param mount - 可见时注册并返回注销函数。
|
|
489
|
+
* @returns 解装函数。
|
|
490
|
+
*/
|
|
491
|
+
function gatedMount<V extends object>(
|
|
492
|
+
scope: BridgeScope<V> | undefined,
|
|
493
|
+
visible: (value: Partial<V>) => boolean,
|
|
494
|
+
mount: () => () => void,
|
|
495
|
+
): () => void {
|
|
496
|
+
let dispose: (() => void) | undefined
|
|
497
|
+
const apply = (): void => {
|
|
498
|
+
const value = (scope?.getSnapshot?.().value ?? {}) as Partial<V>
|
|
499
|
+
const want = visible(value)
|
|
500
|
+
if (want && dispose === undefined) dispose = mount()
|
|
501
|
+
else if (!want && dispose !== undefined) {
|
|
502
|
+
const off = dispose
|
|
503
|
+
dispose = undefined
|
|
504
|
+
off()
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
apply()
|
|
508
|
+
const unsubscribe = scope?.subscribe(apply)
|
|
509
|
+
return () => {
|
|
510
|
+
unsubscribe?.()
|
|
511
|
+
if (dispose !== undefined) {
|
|
512
|
+
const off = dispose
|
|
513
|
+
dispose = undefined
|
|
514
|
+
off()
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** 底栏回落形态(老宿主 / 无顶栏工具行):一枚入口开对话框。 */
|
|
520
|
+
function mountFooterEntries(
|
|
521
|
+
slots: StewardSlotsService,
|
|
522
|
+
bound: BridgeScope<StewardConfig>,
|
|
523
|
+
openSession: (sessionId: string) => void,
|
|
524
|
+
): () => void {
|
|
525
|
+
const off = slots.inject('sidebar.footer.action', () => slots.register(
|
|
526
|
+
{ name: 'sidebar.footer.action', id: STEWARD_ENTRY_ID, order: 6 },
|
|
527
|
+
(props: StewardFooterProps) => createElement(StewardEntry, {
|
|
528
|
+
...props,
|
|
529
|
+
scope: bound as unknown as StewardCardScope,
|
|
530
|
+
openSession,
|
|
531
|
+
}),
|
|
532
|
+
), 'dsh-manage-session: sidebar footer entry')
|
|
533
|
+
return composeDisposers([off])
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* 顶栏工具行形态:面板行(order 6,落在「插件」行之后、「更多」之前)加主栏整页。
|
|
538
|
+
* 点击行 → 宿主 `selectPanel(id)` 在主栏渲染整页;页内关闭动作回退到会话。
|
|
539
|
+
* @param closePanel - 关闭页回退动作(`layout.selectPanel(null)`)。
|
|
540
|
+
*/
|
|
541
|
+
function mountTopToolEntries(
|
|
542
|
+
slots: StewardSlotsService,
|
|
543
|
+
bound: BridgeScope<StewardConfig>,
|
|
544
|
+
openSession: (sessionId: string) => void,
|
|
545
|
+
closePanel: () => void,
|
|
546
|
+
): () => void {
|
|
547
|
+
/** 管理页:管理台 / 体检 双页签,页签可见性与底栏入口同源。 */
|
|
548
|
+
const ManagerPage = (): ReactElement => {
|
|
549
|
+
const snapshot = useSyncExternalStore(
|
|
550
|
+
(listener) => bound.subscribe(listener),
|
|
551
|
+
() => bound.getSnapshot(),
|
|
552
|
+
)
|
|
553
|
+
const config: StewardConfig = { ...DEFAULT_CONFIG, ...(snapshot.value ?? {}) }
|
|
554
|
+
return createElement(StewardPanel, {
|
|
555
|
+
variant: 'page',
|
|
556
|
+
healthCheck: config.healthCheck !== false,
|
|
557
|
+
openSession,
|
|
558
|
+
onClose: closePanel,
|
|
559
|
+
})
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const off = gatedMount(bound, value => value.enabled !== false && value.healthCheck !== false, () =>
|
|
563
|
+
composeDisposers([
|
|
564
|
+
slots.inject(MAIN_SEAT, () => slots.register(
|
|
565
|
+
{ name: MAIN_SEAT, key: MANAGER_PANEL_ID, locale: NS },
|
|
566
|
+
ManagerPage,
|
|
567
|
+
), `dsh-manage-session: main page ${MANAGER_PANEL_ID}`),
|
|
568
|
+
slots.inject(TOP_TOOL_SEAT, () => slots.register(
|
|
569
|
+
{
|
|
570
|
+
name: TOP_TOOL_SEAT,
|
|
571
|
+
id: MANAGER_PANEL_ID,
|
|
572
|
+
order: 6,
|
|
573
|
+
label: () => translate(undefined, 'card.title'),
|
|
574
|
+
locale: NS,
|
|
575
|
+
},
|
|
576
|
+
ManagerToolIcon,
|
|
577
|
+
), `dsh-manage-session: top tool row ${MANAGER_PANEL_ID}`),
|
|
578
|
+
]))
|
|
579
|
+
|
|
580
|
+
return off
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** 顶栏行图标:会话管理(列表行 + 勾选,随行字色)。 */
|
|
584
|
+
function ManagerToolIcon({ size }: { size?: number }): ReactElement {
|
|
585
|
+
const edge = size ?? 16
|
|
586
|
+
return createElement('svg', {
|
|
587
|
+
width: edge, height: edge, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true,
|
|
588
|
+
},
|
|
589
|
+
createElement('path', { d: 'M2 4h12M2 8h12M2 12h7', stroke: 'currentColor', strokeWidth: 1.75, strokeLinecap: 'round' }),
|
|
590
|
+
createElement('path', { d: 'M11.2 11.2l1.5 1.5 2.6-3', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }))
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** 导出字典查找 type 供消费者复用。 */
|
|
594
|
+
export type { LocaleKey }
|