dsh-manage-session 0.1.0-alpha.0 → 0.2.3
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 +159 -0
- package/CHANGELOG.ko.md +160 -0
- package/CHANGELOG.md +232 -0
- package/INSTALL.ja.md +2 -2
- package/INSTALL.ko.md +2 -2
- package/INSTALL.md +4 -4
- package/INSTALL.zh.md +4 -4
- package/README.en.md +63 -63
- package/README.ja.md +63 -63
- package/README.ko.md +61 -61
- package/README.md +96 -75
- package/dsh.plugin.json +22 -22
- package/lib/client.js +560 -120
- package/lib/index.d.ts +333 -78
- package/lib/index.js +694 -97
- package/package.json +121 -104
- package/screenshots.json +3 -3
- package/src/client/card.tsx +153 -106
- package/src/client/health-panel.tsx +397 -205
- package/src/client/history-panel.tsx +150 -35
- package/src/client/host-api.ts +177 -135
- package/src/client/index.ts +234 -198
- package/src/client/locales.ts +203 -153
- package/src/client/panel.tsx +118 -106
- package/src/config.ts +1 -1
- package/src/host/health/cache.ts +97 -0
- package/src/host/health/decode.ts +307 -307
- package/src/host/health/gates.ts +331 -312
- package/src/host/health/repair.ts +217 -126
- package/src/host/health/scan.ts +156 -114
- package/src/host/history/archive-source.ts +103 -42
- package/src/host/history/archive.ts +54 -5
- package/src/host/history/purge.ts +323 -0
- package/src/index.ts +504 -417
package/lib/client.js
CHANGED
|
@@ -26,13 +26,13 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
"card.enabled": "启用会话管家",
|
|
27
27
|
"card.enabled.desc": "关闭后不注册任何接口,侧边栏入口整体消失。",
|
|
28
28
|
"card.history": "会话历史文件",
|
|
29
|
-
"card.history.desc": "
|
|
29
|
+
"card.history.desc": "归档浏览与清理(养老院)。关闭后不注册历史接口、不渲染该页签。",
|
|
30
30
|
"card.health": "健康检查",
|
|
31
31
|
"card.health.desc": "体检 → 处方 → 出院。关闭后不注册体检接口、不渲染该页签。",
|
|
32
32
|
"card.readonly": "当前会话无法修改设置(只读挂载)。",
|
|
33
33
|
"card.unavailable": "设置服务不可用,无法读取配置。",
|
|
34
34
|
"panel.title": "会话管家",
|
|
35
|
-
"panel.tab.history": "
|
|
35
|
+
"panel.tab.history": "养老院",
|
|
36
36
|
"panel.tab.health": "体检",
|
|
37
37
|
"panel.close": "关闭",
|
|
38
38
|
"panel.untitled": "(无标题)",
|
|
@@ -42,20 +42,32 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
"history.done": "完成",
|
|
43
43
|
"history.selectAll": "全选",
|
|
44
44
|
"history.unselectAll": "取消全选",
|
|
45
|
-
"history.
|
|
46
|
-
"history.
|
|
47
|
-
"history.
|
|
48
|
-
"history.
|
|
45
|
+
"history.unarchive": "取消归档状态 ({n})",
|
|
46
|
+
"history.unarchiving": "取消归档中…",
|
|
47
|
+
"history.purge": "清理归档文件 ({n}) · 释放 {size}",
|
|
48
|
+
"history.purging": "清理中…",
|
|
49
|
+
"history.editingHint": "编辑模式:勾选会话后二选一 ——「取消归档状态」只把它从官方归档数组移除(可逆,重启后回到侧边栏);「清理归档文件」真删磁盘上的转录与投影缓存(不可逆)。",
|
|
50
|
+
"history.restartHint": "已从归档数组移除 {removed} 个 id(剩余 {remaining})。请**立刻重启 DSH**:宿主退出前任何归档或工作区改动都会把整份内存快照写回文件,这次操作会被还原。",
|
|
51
|
+
"history.pendingRestart": "宿主内存里仍有 {n} 个归档未失效:列表已按存储文件显示(这些已不在),但它们对应的会话在重启 DSH 前仍被侧边栏隐藏。",
|
|
52
|
+
"history.count": "共 {n} 条 · 占 {size}",
|
|
53
|
+
"history.size": "转录 {t} · 缓存 {c}",
|
|
54
|
+
"history.sizeUnknown": "磁盘上无实体",
|
|
49
55
|
"history.source.registry": "来源:宿主归档注册表",
|
|
50
56
|
"history.source.storage-file": "来源:存储文件",
|
|
51
57
|
"history.source.none": "两处都读不到归档集合",
|
|
52
|
-
"history.
|
|
58
|
+
"history.confirmUnarchive": "确认取消这 {n} 个会话的归档状态?\n\n{summary}\n\n只改归档数组(自动备份),磁盘文件不动。重启 DSH 后这些会话回到原工作区。",
|
|
59
|
+
"history.confirmPurge": "确认清理这 {n} 个归档会话的磁盘文件?\n\n{summary}\n\n合计释放约 {size}。\n将删除:转录目录(含旧格式副本)、逐条投影缓存,并从归档数组与工作区成员表中移除。\n\n**此操作不可逆,会话将无法恢复。**",
|
|
60
|
+
"history.purgeResult": "已清理 {purged} 个归档会话,释放 {size}。请重启 DSH 使其彻底消失。",
|
|
61
|
+
"history.purgePartial": "已清理 {purged} 个,释放 {size};{failed} 个失败(见下方明细)。",
|
|
62
|
+
"history.purgeFailures": "失败明细",
|
|
53
63
|
"health.scan": "开始体检",
|
|
54
64
|
"health.scanning": "体检中…",
|
|
65
|
+
"health.progress": "{done}/{total} · 已用 {sec}s",
|
|
55
66
|
"health.empty": "未发现异常会话。",
|
|
56
67
|
"health.clean": "四门全绿。",
|
|
57
68
|
"health.level.ok": "正常",
|
|
58
69
|
"health.level.warn": "注意",
|
|
70
|
+
"health.level.skipped": "未检查",
|
|
59
71
|
"health.level.fail": "异常",
|
|
60
72
|
"health.gate.log-integrity": "日志完整性",
|
|
61
73
|
"health.gate.projection-cache": "投影缓存",
|
|
@@ -71,11 +83,19 @@ window.__ModuleLoader__.load({
|
|
|
71
83
|
"health.repairing": "处置中…",
|
|
72
84
|
"health.before": "处置前",
|
|
73
85
|
"health.after": "处置后",
|
|
86
|
+
"health.quarantined": "已隔离投影缓存记录(重启 DSH 后重折叠)",
|
|
74
87
|
"health.commands": "可执行命令",
|
|
75
88
|
"health.copy": "复制",
|
|
76
89
|
"health.copied": "已复制",
|
|
77
90
|
"health.detail": "查看详情",
|
|
78
|
-
"health.back": "返回列表"
|
|
91
|
+
"health.back": "返回列表",
|
|
92
|
+
"health.cache.hint": "结果来自缓存 · {ago}",
|
|
93
|
+
"health.cache.refresh": "刷新",
|
|
94
|
+
"health.cache.corpusChanged": "语料已变化({was} → {now}),建议刷新",
|
|
95
|
+
"health.ago.justNow": "刚刚",
|
|
96
|
+
"health.ago.minutes": "{n} 分钟前",
|
|
97
|
+
"health.ago.hours": "{n} 小时前",
|
|
98
|
+
"health.ago.days": "{n} 天前"
|
|
79
99
|
};
|
|
80
100
|
/** English dictionary (same keys). */
|
|
81
101
|
const en = {
|
|
@@ -84,13 +104,13 @@ window.__ModuleLoader__.load({
|
|
|
84
104
|
"card.enabled": "Enable Session Steward",
|
|
85
105
|
"card.enabled.desc": "When off, no API is registered and the sidebar entry disappears.",
|
|
86
106
|
"card.history": "Session history files",
|
|
87
|
-
"card.history.desc": "Archive browsing and cleanup (
|
|
107
|
+
"card.history.desc": "Archive browsing and cleanup (Retirement Home). When off, history routes and the tab are gone.",
|
|
88
108
|
"card.health": "Health check",
|
|
89
109
|
"card.health.desc": "Checkup → Prescription → Discharge. When off, health routes and the tab are gone.",
|
|
90
110
|
"card.readonly": "This session cannot edit settings (read-only mount).",
|
|
91
111
|
"card.unavailable": "Settings service unavailable; configuration cannot be read.",
|
|
92
112
|
"panel.title": "Session Steward",
|
|
93
|
-
"panel.tab.history": "
|
|
113
|
+
"panel.tab.history": "Retirement Home",
|
|
94
114
|
"panel.tab.health": "Checkup",
|
|
95
115
|
"panel.close": "Close",
|
|
96
116
|
"panel.untitled": "(untitled)",
|
|
@@ -100,20 +120,32 @@ window.__ModuleLoader__.load({
|
|
|
100
120
|
"history.done": "Done",
|
|
101
121
|
"history.selectAll": "Select all",
|
|
102
122
|
"history.unselectAll": "Clear selection",
|
|
103
|
-
"history.
|
|
104
|
-
"history.
|
|
105
|
-
"history.
|
|
106
|
-
"history.
|
|
123
|
+
"history.unarchive": "Unarchive state ({n})",
|
|
124
|
+
"history.unarchiving": "Unarchiving…",
|
|
125
|
+
"history.purge": "Purge archive files ({n}) · free {size}",
|
|
126
|
+
"history.purging": "Purging…",
|
|
127
|
+
"history.editingHint": "Edit mode: tick sessions, then pick one — \"Unarchive state\" only removes them from the official archive array (reversible; they return to the sidebar after a restart); \"Purge archive files\" really deletes the transcript and projection cache from disk (irreversible).",
|
|
128
|
+
"history.restartHint": "Removed {removed} id(s) from the archive array ({remaining} left). **Restart DSH now**: until the host exits, any archive or workspace change rewrites the whole in-memory snapshot back to the file and undoes this.",
|
|
129
|
+
"history.pendingRestart": "{n} archived id(s) are still live in host memory: the list follows the storage file (they are already gone), but their sessions stay hidden from the sidebar until DSH restarts.",
|
|
130
|
+
"history.count": "{n} total · {size} on disk",
|
|
131
|
+
"history.size": "transcript {t} · cache {c}",
|
|
132
|
+
"history.sizeUnknown": "no on-disk entity",
|
|
107
133
|
"history.source.registry": "Source: host archive registry",
|
|
108
134
|
"history.source.storage-file": "Source: storage file",
|
|
109
135
|
"history.source.none": "The archive set is unreadable from both sources",
|
|
110
|
-
"history.
|
|
136
|
+
"history.confirmUnarchive": "Unarchive these {n} session(s)?\n\n{summary}\n\nOnly the archive array is edited (auto backup); nothing on disk is touched. After a DSH restart these sessions return to their workspaces.",
|
|
137
|
+
"history.confirmPurge": "Purge the on-disk files of these {n} archived session(s)?\n\n{summary}\n\nAbout {size} will be freed.\nThis deletes: the transcript directory (including older-format copies) and the per-session projection cache, and removes them from the archive array and the workspace membership tables.\n\n**This cannot be undone — the sessions will be unrecoverable.**",
|
|
138
|
+
"history.purgeResult": "Purged {purged} archived session(s), freed {size}. Restart DSH to make them disappear completely.",
|
|
139
|
+
"history.purgePartial": "Purged {purged}, freed {size}; {failed} failed (see details below).",
|
|
140
|
+
"history.purgeFailures": "Failure details",
|
|
111
141
|
"health.scan": "Run checkup",
|
|
112
142
|
"health.scanning": "Running…",
|
|
143
|
+
"health.progress": "{done}/{total} · {sec}s elapsed",
|
|
113
144
|
"health.empty": "No problem session found.",
|
|
114
145
|
"health.clean": "All four gates are green.",
|
|
115
146
|
"health.level.ok": "ok",
|
|
116
147
|
"health.level.warn": "warn",
|
|
148
|
+
"health.level.skipped": "Not checked",
|
|
117
149
|
"health.level.fail": "fail",
|
|
118
150
|
"health.gate.log-integrity": "Log integrity",
|
|
119
151
|
"health.gate.projection-cache": "Projection cache",
|
|
@@ -129,11 +161,19 @@ window.__ModuleLoader__.load({
|
|
|
129
161
|
"health.repairing": "Repairing…",
|
|
130
162
|
"health.before": "Before",
|
|
131
163
|
"health.after": "After",
|
|
164
|
+
"health.quarantined": "Projection cache quarantined (refolds after a DSH restart)",
|
|
132
165
|
"health.commands": "Commands",
|
|
133
166
|
"health.copy": "Copy",
|
|
134
167
|
"health.copied": "Copied",
|
|
135
168
|
"health.detail": "Details",
|
|
136
|
-
"health.back": "Back"
|
|
169
|
+
"health.back": "Back",
|
|
170
|
+
"health.cache.hint": "Cached result · {ago}",
|
|
171
|
+
"health.cache.refresh": "Refresh",
|
|
172
|
+
"health.cache.corpusChanged": "Corpus changed ({was} → {now}); refresh recommended",
|
|
173
|
+
"health.ago.justNow": "just now",
|
|
174
|
+
"health.ago.minutes": "{n} min ago",
|
|
175
|
+
"health.ago.hours": "{n} h ago",
|
|
176
|
+
"health.ago.days": "{n} d ago"
|
|
137
177
|
};
|
|
138
178
|
/** 字典查找(缺 key 时回退到内置 zh,再回退 key 本身)。 */
|
|
139
179
|
function translate(t, key, params) {
|
|
@@ -150,9 +190,18 @@ window.__ModuleLoader__.load({
|
|
|
150
190
|
//#endregion
|
|
151
191
|
//#region src/client/card.tsx
|
|
152
192
|
/**
|
|
153
|
-
*
|
|
193
|
+
* 设置卡(插件配置行):一个可折叠抽屉 —— 会话历史文件 / 健康检查。
|
|
194
|
+
*
|
|
195
|
+
* **chrome 必须自己渲染**:DSH 的「插件」设置分区(`dsh-client-ui-settings-plugins`)
|
|
196
|
+
* 只做一件事——按 settings 命名空间分发 `settings.plugin.item`:
|
|
197
|
+
*
|
|
198
|
+
* renderSlot('settings.plugin.item', {}, { entryKey: ns })
|
|
199
|
+
*
|
|
200
|
+
* 它**不提供任何外壳**。所以卡片的标题、说明、展开/收起全归插件自己所有
|
|
201
|
+
* (同分区里 search-index 等卡片都是这个形态)。早期版本只渲染了裸行,
|
|
202
|
+
* 结果既与分区内其它插件的抽屉形态不一致,又让 `card.title` / `card.desc`
|
|
203
|
+
* 变成定义了却没人渲染的死文案。
|
|
154
204
|
*
|
|
155
|
-
* 与 dsh-session-search-toggle 的 card 同范式:`Row`(标题+说明+控件)+ `Toggle`,
|
|
156
205
|
* 读写走 settingsScope 的显式 `set`(用户在控件上的直接操作,非隐式提交)。
|
|
157
206
|
*/
|
|
158
207
|
/** 一行设置项。 */
|
|
@@ -183,14 +232,52 @@ window.__ModuleLoader__.load({
|
|
|
183
232
|
className: "dss_toggleTrack"
|
|
184
233
|
}, (0, react.createElement)("span", { className: "dss_toggleKnob" }))]);
|
|
185
234
|
}
|
|
186
|
-
/**
|
|
235
|
+
/** 抽屉头:标题 + 说明 + chevron,整块可点。 */
|
|
236
|
+
function Header(props) {
|
|
237
|
+
return (0, react.createElement)("button", {
|
|
238
|
+
key: "head",
|
|
239
|
+
type: "button",
|
|
240
|
+
className: "dss_setHead",
|
|
241
|
+
"aria-expanded": props.open,
|
|
242
|
+
onClick: props.onToggle
|
|
243
|
+
}, [(0, react.createElement)("span", {
|
|
244
|
+
key: "text",
|
|
245
|
+
className: "dss_setHeadText"
|
|
246
|
+
}, [(0, react.createElement)("span", {
|
|
247
|
+
key: "t",
|
|
248
|
+
className: "dss_setCardTitle"
|
|
249
|
+
}, translate(props.t, "card.title")), (0, react.createElement)("span", {
|
|
250
|
+
key: "d",
|
|
251
|
+
className: "dss_setCardDesc"
|
|
252
|
+
}, translate(props.t, "card.desc"))]), (0, react.createElement)("svg", {
|
|
253
|
+
key: "chev",
|
|
254
|
+
className: "dss_setChev",
|
|
255
|
+
width: 16,
|
|
256
|
+
height: 16,
|
|
257
|
+
viewBox: "0 0 16 16",
|
|
258
|
+
"aria-hidden": true
|
|
259
|
+
}, (0, react.createElement)("path", {
|
|
260
|
+
d: "M4 6l4 4 4-4",
|
|
261
|
+
fill: "none",
|
|
262
|
+
stroke: "currentColor",
|
|
263
|
+
strokeWidth: 1.5,
|
|
264
|
+
strokeLinecap: "round",
|
|
265
|
+
strokeLinejoin: "round"
|
|
266
|
+
}))]);
|
|
267
|
+
}
|
|
268
|
+
/** 插件设置卡主体(抽屉)。默认收起,与「插件」分区内其它卡片一致。 */
|
|
187
269
|
function StewardSettingsCard({ t, scope }) {
|
|
188
270
|
const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
271
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
189
272
|
const value = snapshot.value ?? {};
|
|
190
273
|
const writable = snapshot.writable;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
274
|
+
const rows = [];
|
|
275
|
+
if (snapshot.status === "unavailable") rows.push((0, react.createElement)("div", {
|
|
276
|
+
key: "unavailable",
|
|
277
|
+
className: "dss_setDesc"
|
|
278
|
+
}, translate(t, "card.unavailable")));
|
|
279
|
+
else {
|
|
280
|
+
rows.push((0, react.createElement)(Row, {
|
|
194
281
|
key: "enabled",
|
|
195
282
|
title: translate(t, "card.enabled"),
|
|
196
283
|
desc: translate(t, "card.enabled.desc"),
|
|
@@ -201,8 +288,7 @@ window.__ModuleLoader__.load({
|
|
|
201
288
|
scope.set("enabled", checked);
|
|
202
289
|
}
|
|
203
290
|
})
|
|
204
|
-
}),
|
|
205
|
-
(0, react.createElement)(Row, {
|
|
291
|
+
}), (0, react.createElement)(Row, {
|
|
206
292
|
key: "history",
|
|
207
293
|
title: translate(t, "card.history"),
|
|
208
294
|
desc: translate(t, "card.history.desc"),
|
|
@@ -213,8 +299,7 @@ window.__ModuleLoader__.load({
|
|
|
213
299
|
scope.set("historyFiles", checked);
|
|
214
300
|
}
|
|
215
301
|
})
|
|
216
|
-
}),
|
|
217
|
-
(0, react.createElement)(Row, {
|
|
302
|
+
}), (0, react.createElement)(Row, {
|
|
218
303
|
key: "health",
|
|
219
304
|
title: translate(t, "card.health"),
|
|
220
305
|
desc: translate(t, "card.health.desc"),
|
|
@@ -225,13 +310,23 @@ window.__ModuleLoader__.load({
|
|
|
225
310
|
scope.set("healthCheck", checked);
|
|
226
311
|
}
|
|
227
312
|
})
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
return (0, react.createElement)("div", { className: "
|
|
313
|
+
}));
|
|
314
|
+
if (!writable) rows.push((0, react.createElement)("div", {
|
|
315
|
+
key: "ro",
|
|
316
|
+
className: "dss_setDesc"
|
|
317
|
+
}, translate(t, "card.readonly")));
|
|
318
|
+
}
|
|
319
|
+
return (0, react.createElement)("div", { className: `dss_setCard${open ? " dss_setCardOpen" : ""}` }, [(0, react.createElement)(Header, {
|
|
320
|
+
key: "head",
|
|
321
|
+
t,
|
|
322
|
+
open,
|
|
323
|
+
onToggle: () => {
|
|
324
|
+
setOpen((current) => !current);
|
|
325
|
+
}
|
|
326
|
+
}), open && (0, react.createElement)("div", {
|
|
327
|
+
key: "body",
|
|
328
|
+
className: "dss_setBody"
|
|
329
|
+
}, rows)]);
|
|
235
330
|
}
|
|
236
331
|
//#endregion
|
|
237
332
|
//#region src/client/host-api.ts
|
|
@@ -263,23 +358,51 @@ window.__ModuleLoader__.load({
|
|
|
263
358
|
//#endregion
|
|
264
359
|
//#region src/client/history-panel.tsx
|
|
265
360
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
361
|
+
* 养老院(会话历史文件)面板 —— 自 dsh-session-search-toggle `src/client/archive-panel.tsx`
|
|
362
|
+
* **迁入**并适配到本包的方法名。
|
|
363
|
+
*
|
|
364
|
+
* 与迁移前的关键差异(**两个操作刻意分开,不可互换**):
|
|
365
|
+
*
|
|
366
|
+
* | 操作 | 路由 | 改什么 | 可逆 |
|
|
367
|
+
* |---|---|---|---|
|
|
368
|
+
* | 取消归档状态 | `session-history-prune` | 只改归档数组 | ✅ 重启后会话回到侧边栏 |
|
|
369
|
+
* | 清理归档文件 | `session-history-purge` | **真删**转录目录 + 投影缓存 + 两处 id | ❌ 不可逆 |
|
|
268
370
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
371
|
+
* 「删除」一词在本面板**不存在** —— 旧文案承诺「删除」却只改了个数组,
|
|
372
|
+
* 是导致「点了确认没反应」那类误判的一部分。
|
|
373
|
+
* 体积**按行**显示(实测单条可从 0 到 23 MB,报一个总量没有意义),按钮上给所选小计。
|
|
272
374
|
*/
|
|
273
|
-
/**
|
|
375
|
+
/** 人类可读体积。 */
|
|
376
|
+
function fmtSize(bytes) {
|
|
377
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
378
|
+
const units = [
|
|
379
|
+
"B",
|
|
380
|
+
"KB",
|
|
381
|
+
"MB",
|
|
382
|
+
"GB",
|
|
383
|
+
"TB"
|
|
384
|
+
];
|
|
385
|
+
let value = bytes;
|
|
386
|
+
let unit = 0;
|
|
387
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
388
|
+
value /= 1024;
|
|
389
|
+
unit++;
|
|
390
|
+
}
|
|
391
|
+
return `${unit === 0 || value >= 100 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
392
|
+
}
|
|
393
|
+
/** 养老院面板。 */
|
|
274
394
|
function HistoryPanel({ t, onClose }) {
|
|
275
395
|
const [items, setItems] = (0, react.useState)(null);
|
|
276
396
|
const [source, setSource] = (0, react.useState)("");
|
|
277
397
|
const [degraded, setDegraded] = (0, react.useState)("");
|
|
398
|
+
const [pendingRestart, setPendingRestart] = (0, react.useState)(0);
|
|
278
399
|
const [error, setError] = (0, react.useState)(null);
|
|
279
400
|
const [attempt, setAttempt] = (0, react.useState)(0);
|
|
280
401
|
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
281
|
-
const [
|
|
402
|
+
const [busy, setBusy] = (0, react.useState)(null);
|
|
282
403
|
const [note, setNote] = (0, react.useState)(null);
|
|
404
|
+
/** 清理失败明细(逐条);空数组表示无失败。 */
|
|
405
|
+
const [failures, setFailures] = (0, react.useState)([]);
|
|
283
406
|
const [editing, setEditing] = (0, react.useState)(false);
|
|
284
407
|
(0, react.useEffect)(() => {
|
|
285
408
|
let cancelled = false;
|
|
@@ -290,6 +413,7 @@ window.__ModuleLoader__.load({
|
|
|
290
413
|
setItems(res.items);
|
|
291
414
|
setSource(typeof res.source === "string" ? res.source : "");
|
|
292
415
|
setDegraded(typeof res.degraded === "string" ? res.degraded : "");
|
|
416
|
+
setPendingRestart(typeof res.pendingRestart === "number" ? res.pendingRestart : 0);
|
|
293
417
|
} else setError(res.error ?? "读取归档列表失败");
|
|
294
418
|
});
|
|
295
419
|
return () => {
|
|
@@ -318,31 +442,69 @@ window.__ModuleLoader__.load({
|
|
|
318
442
|
setEditing((prev) => !prev);
|
|
319
443
|
setSelected(/* @__PURE__ */ new Set());
|
|
320
444
|
setNote(null);
|
|
445
|
+
setFailures([]);
|
|
321
446
|
};
|
|
322
447
|
const toggleAll = () => {
|
|
323
448
|
setSelected((prev) => prev.size === (items?.length ?? 0) ? /* @__PURE__ */ new Set() : new Set((items ?? []).map((item) => item.sessionId)));
|
|
324
449
|
};
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
450
|
+
const selectedRows = (items ?? []).filter((item) => selected.has(item.sessionId));
|
|
451
|
+
const selectedBytes = selectedRows.reduce((sum, item) => sum + item.bytes + item.cacheBytes, 0);
|
|
452
|
+
const totalBytes = (items ?? []).reduce((sum, item) => sum + item.bytes + item.cacheBytes, 0);
|
|
453
|
+
/** 确认弹窗里的逐条摘要(清理时带体积,取消归档时不带)。 */
|
|
454
|
+
const summarise = (rows, withSize) => {
|
|
455
|
+
const lines = rows.slice(0, 5).map((row) => {
|
|
456
|
+
const label = row.title || translate(t, "panel.untitled");
|
|
457
|
+
const size = fmtSize(row.bytes + row.cacheBytes);
|
|
458
|
+
return withSize ? `· ${label} — ${size}` : `· ${label}`;
|
|
459
|
+
});
|
|
460
|
+
if (rows.length > 5) lines.push(`… 共 ${rows.length} 个`);
|
|
461
|
+
return lines.join("\n");
|
|
462
|
+
};
|
|
463
|
+
const confirm = (message) => typeof window !== "undefined" && typeof window.confirm === "function" ? window.confirm(message) : false;
|
|
464
|
+
/** 提交一次操作:确认 → 调用 → 刷新列表(列表以存储文件为准,因此立刻可见变化)。 */
|
|
465
|
+
const run = (kind, method, confirmText) => {
|
|
466
|
+
const rows = selectedRows;
|
|
467
|
+
if (rows.length === 0) return;
|
|
468
|
+
if (!confirm(confirmText)) return;
|
|
469
|
+
setBusy(kind);
|
|
335
470
|
setNote(null);
|
|
336
|
-
|
|
471
|
+
setFailures([]);
|
|
472
|
+
callHostAny(method, { sessionIds: rows.map((row) => row.sessionId) }, 6e4).then((res) => {
|
|
337
473
|
if (res.ok === true) {
|
|
338
|
-
setNote(translate(t, "history.restartHint", {
|
|
339
|
-
removed: res.removed ??
|
|
474
|
+
if (kind === "unarchive") setNote(translate(t, "history.restartHint", {
|
|
475
|
+
removed: res.removed ?? rows.length,
|
|
340
476
|
remaining: res.remaining ?? "?"
|
|
341
477
|
}));
|
|
478
|
+
else {
|
|
479
|
+
const freed = fmtSize(res.freedBytes ?? 0);
|
|
480
|
+
const failed = res.failures?.length ?? 0;
|
|
481
|
+
setNote(failed === 0 ? translate(t, "history.purgeResult", {
|
|
482
|
+
purged: res.purged ?? rows.length,
|
|
483
|
+
size: freed
|
|
484
|
+
}) : translate(t, "history.purgePartial", {
|
|
485
|
+
purged: res.purged ?? rows.length,
|
|
486
|
+
size: freed,
|
|
487
|
+
failed
|
|
488
|
+
}));
|
|
489
|
+
setFailures(res.failures ?? []);
|
|
490
|
+
}
|
|
342
491
|
setSelected(/* @__PURE__ */ new Set());
|
|
343
492
|
setAttempt((n) => n + 1);
|
|
344
|
-
} else setNote(res.error ?? "
|
|
345
|
-
}).catch((err) => setNote(
|
|
493
|
+
} else setNote(res.error ?? "操作失败");
|
|
494
|
+
}).catch((err) => setNote(`操作失败:${String(err instanceof Error ? err.message : err)}`)).finally(() => setBusy(null));
|
|
495
|
+
};
|
|
496
|
+
const unarchiveSelected = () => {
|
|
497
|
+
run("unarchive", "session-history-prune", translate(t, "history.confirmUnarchive", {
|
|
498
|
+
n: selectedRows.length,
|
|
499
|
+
summary: summarise(selectedRows, false)
|
|
500
|
+
}));
|
|
501
|
+
};
|
|
502
|
+
const purgeSelected = () => {
|
|
503
|
+
run("purge", "session-history-purge", translate(t, "history.confirmPurge", {
|
|
504
|
+
n: selectedRows.length,
|
|
505
|
+
summary: summarise(selectedRows, true),
|
|
506
|
+
size: fmtSize(selectedBytes)
|
|
507
|
+
}));
|
|
346
508
|
};
|
|
347
509
|
const children = [];
|
|
348
510
|
if (error !== null) children.push((0, react.createElement)("div", {
|
|
@@ -371,18 +533,31 @@ window.__ModuleLoader__.load({
|
|
|
371
533
|
key: "manage",
|
|
372
534
|
className: "dss_btnRow",
|
|
373
535
|
style: { padding: "4px 10px 0" }
|
|
374
|
-
}, [
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
536
|
+
}, [
|
|
537
|
+
(0, react.createElement)("button", {
|
|
538
|
+
key: "all",
|
|
539
|
+
type: "button",
|
|
540
|
+
className: "dss_actBtn",
|
|
541
|
+
onClick: toggleAll
|
|
542
|
+
}, allSelected ? translate(t, "history.unselectAll") : translate(t, "history.selectAll")),
|
|
543
|
+
(0, react.createElement)("button", {
|
|
544
|
+
key: "unarchive",
|
|
545
|
+
type: "button",
|
|
546
|
+
className: "dss_actBtn",
|
|
547
|
+
disabled: busy !== null || selected.size === 0,
|
|
548
|
+
onClick: unarchiveSelected
|
|
549
|
+
}, busy === "unarchive" ? translate(t, "history.unarchiving") : translate(t, "history.unarchive", { n: selected.size })),
|
|
550
|
+
(0, react.createElement)("button", {
|
|
551
|
+
key: "purge",
|
|
552
|
+
type: "button",
|
|
553
|
+
className: "dss_actBtn dss_dangerBtn",
|
|
554
|
+
disabled: busy !== null || selected.size === 0,
|
|
555
|
+
onClick: purgeSelected
|
|
556
|
+
}, busy === "purge" ? translate(t, "history.purging") : translate(t, "history.purge", {
|
|
557
|
+
n: selected.size,
|
|
558
|
+
size: fmtSize(selectedBytes)
|
|
559
|
+
}))
|
|
560
|
+
]));
|
|
386
561
|
children.push((0, react.createElement)("ul", {
|
|
387
562
|
key: "list",
|
|
388
563
|
className: "dss_list",
|
|
@@ -412,6 +587,13 @@ window.__ModuleLoader__.load({
|
|
|
412
587
|
key: "tag",
|
|
413
588
|
className: "dss_tag"
|
|
414
589
|
}, fmtTime(item.updatedAt))]),
|
|
590
|
+
(0, react.createElement)("span", {
|
|
591
|
+
key: "size",
|
|
592
|
+
className: "dss_meta dss_size"
|
|
593
|
+
}, item.bytes + item.cacheBytes > 0 ? translate(t, "history.size", {
|
|
594
|
+
t: fmtSize(item.bytes),
|
|
595
|
+
c: fmtSize(item.cacheBytes)
|
|
596
|
+
}) : translate(t, "history.sizeUnknown")),
|
|
415
597
|
item.cwd !== "" && (0, react.createElement)("span", {
|
|
416
598
|
key: "c",
|
|
417
599
|
className: "dss_meta"
|
|
@@ -427,6 +609,13 @@ window.__ModuleLoader__.load({
|
|
|
427
609
|
key: "note",
|
|
428
610
|
className: "dss_status"
|
|
429
611
|
}, note),
|
|
612
|
+
failures.length > 0 && (0, react.createElement)("div", {
|
|
613
|
+
key: "fail",
|
|
614
|
+
className: "dss_status dss_error"
|
|
615
|
+
}, [(0, react.createElement)("div", { key: "h" }, translate(t, "history.purgeFailures")), ...failures.slice(0, 10).map((failure, index) => (0, react.createElement)("div", {
|
|
616
|
+
key: `f${index}`,
|
|
617
|
+
className: "dss_meta"
|
|
618
|
+
}, `${failure.sessionId.slice(0, 22)}… — ${failure.reason}`))]),
|
|
430
619
|
editing && (0, react.createElement)("div", {
|
|
431
620
|
key: "hint",
|
|
432
621
|
className: "dss_status"
|
|
@@ -435,10 +624,21 @@ window.__ModuleLoader__.load({
|
|
|
435
624
|
key: "degraded",
|
|
436
625
|
className: "dss_status dss_warnText"
|
|
437
626
|
}, degraded),
|
|
627
|
+
pendingRestart > 0 && (0, react.createElement)("div", {
|
|
628
|
+
key: "pending",
|
|
629
|
+
className: "dss_status dss_warnText"
|
|
630
|
+
}, translate(t, "history.pendingRestart", { n: pendingRestart })),
|
|
438
631
|
source !== "" && (0, react.createElement)("div", {
|
|
439
632
|
key: "source",
|
|
440
633
|
className: "dss_metaLine"
|
|
441
634
|
}, translate(t, source === "registry" ? "history.source.registry" : source === "storage-file" ? "history.source.storage-file" : "history.source.none")),
|
|
635
|
+
items !== null && error === null && (0, react.createElement)("div", {
|
|
636
|
+
key: "count",
|
|
637
|
+
className: "dss_metaLine"
|
|
638
|
+
}, translate(t, "history.count", {
|
|
639
|
+
n: items.length,
|
|
640
|
+
size: fmtSize(totalBytes)
|
|
641
|
+
})),
|
|
442
642
|
(0, react.createElement)("div", {
|
|
443
643
|
key: "headBtns",
|
|
444
644
|
className: "dss_btnRow"
|
|
@@ -483,8 +683,44 @@ window.__ModuleLoader__.load({
|
|
|
483
683
|
const LEVEL_LABEL = {
|
|
484
684
|
ok: "health.level.ok",
|
|
485
685
|
warn: "health.level.warn",
|
|
686
|
+
skipped: "health.level.skipped",
|
|
486
687
|
fail: "health.level.fail"
|
|
487
688
|
};
|
|
689
|
+
/**
|
|
690
|
+
* 单批扫描的会话数。
|
|
691
|
+
*
|
|
692
|
+
* 宿主侧 `scanSessions` 是同步循环,一次批太大就长时间占住事件循环、界面全无反馈。
|
|
693
|
+
* 小批次连续调用把控制权交回客户端:每批之间有真实的进度与计时,且宿主无需持有
|
|
694
|
+
* 跨请求状态(见 host/health/scan.ts 的 offset 语义)。
|
|
695
|
+
*/
|
|
696
|
+
const SCAN_BATCH = 5;
|
|
697
|
+
/**
|
|
698
|
+
* 把缓存生成时刻折成一个粗粒度的相对时间 key。
|
|
699
|
+
*
|
|
700
|
+
* 不引第三方 i18n/时间库:四档粒度足够表达「这是刚才的 / 是昨天的」,
|
|
701
|
+
* 而这正是用户判断「要不要刷新」所需的全部信息。
|
|
702
|
+
*/
|
|
703
|
+
function relativeAgo(generatedAt, now) {
|
|
704
|
+
const seconds = Math.max(0, Math.round((now - generatedAt) / 1e3));
|
|
705
|
+
if (seconds < 60) return {
|
|
706
|
+
key: "health.ago.justNow",
|
|
707
|
+
n: 0
|
|
708
|
+
};
|
|
709
|
+
const minutes = Math.floor(seconds / 60);
|
|
710
|
+
if (minutes < 60) return {
|
|
711
|
+
key: "health.ago.minutes",
|
|
712
|
+
n: minutes
|
|
713
|
+
};
|
|
714
|
+
const hours = Math.floor(minutes / 60);
|
|
715
|
+
if (hours < 24) return {
|
|
716
|
+
key: "health.ago.hours",
|
|
717
|
+
n: hours
|
|
718
|
+
};
|
|
719
|
+
return {
|
|
720
|
+
key: "health.ago.days",
|
|
721
|
+
n: Math.floor(hours / 24)
|
|
722
|
+
};
|
|
723
|
+
}
|
|
488
724
|
/** 体检面板。 */
|
|
489
725
|
function HealthPanel({ t, onClose }) {
|
|
490
726
|
const [scanning, setScanning] = (0, react.useState)(false);
|
|
@@ -494,6 +730,14 @@ window.__ModuleLoader__.load({
|
|
|
494
730
|
const [repairing, setRepairing] = (0, react.useState)(false);
|
|
495
731
|
const [discharge, setDischarge] = (0, react.useState)(null);
|
|
496
732
|
const [copied, setCopied] = (0, react.useState)(null);
|
|
733
|
+
/** 分批扫描进度:done = 已访问会话数,total = 语料总数(0 表示尚未拿到分母)。 */
|
|
734
|
+
const [progress, setProgress] = (0, react.useState)(null);
|
|
735
|
+
const [startedAt, setStartedAt] = (0, react.useState)(0);
|
|
736
|
+
const [elapsedMs, setElapsedMs] = (0, react.useState)(0);
|
|
737
|
+
/** 缓存命中时的元信息;非 null 即「当前列表来自缓存」,必须露出生成时间。 */
|
|
738
|
+
const [cacheInfo, setCacheInfo] = (0, react.useState)(null);
|
|
739
|
+
/** 相对时间的走字刻度:缓存提示里的「N 分钟前」需要自己更新。 */
|
|
740
|
+
const [, setClockTick] = (0, react.useState)(0);
|
|
497
741
|
(0, react.useEffect)(() => {
|
|
498
742
|
const onKey = (event) => {
|
|
499
743
|
if (event.key === "Escape") onClose();
|
|
@@ -503,25 +747,115 @@ window.__ModuleLoader__.load({
|
|
|
503
747
|
document.removeEventListener("keydown", onKey);
|
|
504
748
|
};
|
|
505
749
|
}, [onClose]);
|
|
506
|
-
|
|
750
|
+
(0, react.useEffect)(() => {
|
|
751
|
+
callHostAny("session-health-scan", { resume: true }, 3e4).then((res) => {
|
|
752
|
+
if (res.ok === true && res.cached === true) {
|
|
753
|
+
setFindings(res.findings ?? []);
|
|
754
|
+
setCacheInfo({
|
|
755
|
+
generatedAt: res.generatedAt ?? 0,
|
|
756
|
+
total: res.total ?? 0,
|
|
757
|
+
currentTotal: res.currentTotal ?? res.total ?? 0
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}, []);
|
|
762
|
+
(0, react.useEffect)(() => {
|
|
763
|
+
if (cacheInfo === null) return;
|
|
764
|
+
const timer = setInterval(() => {
|
|
765
|
+
setClockTick((tick) => tick + 1);
|
|
766
|
+
}, 3e4);
|
|
767
|
+
return () => {
|
|
768
|
+
clearInterval(timer);
|
|
769
|
+
};
|
|
770
|
+
}, [cacheInfo]);
|
|
771
|
+
(0, react.useEffect)(() => {
|
|
772
|
+
if (!scanning || startedAt === 0) return;
|
|
773
|
+
const timer = setInterval(() => {
|
|
774
|
+
setElapsedMs(Date.now() - startedAt);
|
|
775
|
+
}, 100);
|
|
776
|
+
return () => {
|
|
777
|
+
clearInterval(timer);
|
|
778
|
+
};
|
|
779
|
+
}, [scanning, startedAt]);
|
|
780
|
+
const runScan = (options) => {
|
|
781
|
+
const forceRefresh = options?.forceRefresh === true;
|
|
507
782
|
setScanning(true);
|
|
508
783
|
setError(null);
|
|
509
784
|
setDetail(null);
|
|
510
785
|
setDischarge(null);
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
786
|
+
setFindings(null);
|
|
787
|
+
setCacheInfo(null);
|
|
788
|
+
setProgress({
|
|
789
|
+
done: 0,
|
|
790
|
+
total: 0
|
|
791
|
+
});
|
|
792
|
+
setStartedAt(Date.now());
|
|
793
|
+
setElapsedMs(0);
|
|
794
|
+
(async () => {
|
|
795
|
+
const collected = [];
|
|
796
|
+
let offset = 0;
|
|
797
|
+
try {
|
|
798
|
+
for (;;) {
|
|
799
|
+
const res = await callHostAny("session-health-scan", {
|
|
800
|
+
limit: SCAN_BATCH,
|
|
801
|
+
offset,
|
|
802
|
+
onlyProblems: true,
|
|
803
|
+
...offset === 0 && !forceRefresh ? { resume: true } : {}
|
|
804
|
+
}, 12e4);
|
|
805
|
+
if (res.ok !== true) {
|
|
806
|
+
setError(res.error ?? "体检失败");
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (res.cached === true) {
|
|
810
|
+
setFindings(res.findings ?? []);
|
|
811
|
+
setCacheInfo({
|
|
812
|
+
generatedAt: res.generatedAt ?? 0,
|
|
813
|
+
total: res.total ?? 0,
|
|
814
|
+
currentTotal: res.currentTotal ?? res.total ?? 0
|
|
815
|
+
});
|
|
816
|
+
setProgress(null);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const total = res.total ?? 0;
|
|
820
|
+
const scanned = res.scanned ?? 0;
|
|
821
|
+
collected.push(...res.findings ?? []);
|
|
822
|
+
offset += scanned;
|
|
823
|
+
setProgress({
|
|
824
|
+
done: offset,
|
|
825
|
+
total
|
|
826
|
+
});
|
|
827
|
+
if (scanned === 0 || offset >= total) break;
|
|
828
|
+
}
|
|
829
|
+
setFindings(collected);
|
|
830
|
+
} catch (err) {
|
|
831
|
+
setError(String(err instanceof Error ? err.message : err));
|
|
832
|
+
} finally {
|
|
833
|
+
setScanning(false);
|
|
834
|
+
}
|
|
835
|
+
})();
|
|
836
|
+
};
|
|
837
|
+
/**
|
|
838
|
+
* 把单会话的最新报告同步进列表。
|
|
839
|
+
*
|
|
840
|
+
* 宿主侧缓存已就地回写(见 index.ts 的 `cache?.patch`),客户端列表必须同步,
|
|
841
|
+
* 否则退回列表时会看到已被处置的行还挂着旧档位。
|
|
842
|
+
*/
|
|
843
|
+
const reconcileRow = (next) => {
|
|
844
|
+
setFindings((prev) => {
|
|
845
|
+
if (prev === null) return prev;
|
|
846
|
+
const index = prev.findIndex((item) => item.sessionId === next.sessionId);
|
|
847
|
+
if (next.level === "ok") return index < 0 ? prev : prev.filter((item) => item.sessionId !== next.sessionId);
|
|
848
|
+
return index < 0 ? [...prev, next] : prev.map((item) => item.sessionId === next.sessionId ? next : item);
|
|
849
|
+
});
|
|
518
850
|
};
|
|
519
851
|
const openDetail = (sessionId) => {
|
|
520
852
|
setError(null);
|
|
521
853
|
setDischarge(null);
|
|
522
854
|
callHostAny("session-health-session", { sessionId }, 12e4).then((res) => {
|
|
523
|
-
if (res.ok === true)
|
|
524
|
-
|
|
855
|
+
if (res.ok === true) {
|
|
856
|
+
setDetail(res);
|
|
857
|
+
if (res.report !== void 0) reconcileRow(res.report);
|
|
858
|
+
} else setError(res.error ?? "读取体检详情失败");
|
|
525
859
|
}).catch((err) => setError(String(err instanceof Error ? err.message : err)));
|
|
526
860
|
};
|
|
527
861
|
const runRepair = (sessionId) => {
|
|
@@ -530,11 +864,14 @@ window.__ModuleLoader__.load({
|
|
|
530
864
|
callHostAny("session-health-repair", { sessionId }, 12e4).then((res) => {
|
|
531
865
|
if (res.ok === true) {
|
|
532
866
|
setDischarge(res);
|
|
533
|
-
if (res.after !== void 0)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
867
|
+
if (res.after !== void 0) {
|
|
868
|
+
setDetail({
|
|
869
|
+
ok: true,
|
|
870
|
+
report: res.after,
|
|
871
|
+
prescriptions: res.prescriptions
|
|
872
|
+
});
|
|
873
|
+
reconcileRow(res.after);
|
|
874
|
+
}
|
|
538
875
|
} else setError(res.error ?? "处置失败");
|
|
539
876
|
}).catch((err) => setError(String(err instanceof Error ? err.message : err))).finally(() => setRepairing(false));
|
|
540
877
|
};
|
|
@@ -559,6 +896,55 @@ window.__ModuleLoader__.load({
|
|
|
559
896
|
disabled: scanning,
|
|
560
897
|
onClick: runScan
|
|
561
898
|
}, scanning ? translate(t, "health.scanning") : translate(t, "health.scan"))]));
|
|
899
|
+
if (scanning && progress !== null) {
|
|
900
|
+
const total = progress.total;
|
|
901
|
+
const pct = total > 0 ? Math.min(100, Math.round(progress.done / total * 100)) : 0;
|
|
902
|
+
lines.push((0, react.createElement)("div", {
|
|
903
|
+
key: "progress",
|
|
904
|
+
className: "dss_progressRow"
|
|
905
|
+
}, [(0, react.createElement)("div", {
|
|
906
|
+
key: "track",
|
|
907
|
+
className: "dss_progressTrack"
|
|
908
|
+
}, (0, react.createElement)("div", {
|
|
909
|
+
key: "fill",
|
|
910
|
+
className: total > 0 ? "dss_progressFill" : "dss_progressFill dss_progressIndeterminate",
|
|
911
|
+
...total > 0 ? { style: { width: `${pct}%` } } : {}
|
|
912
|
+
})), (0, react.createElement)("span", {
|
|
913
|
+
key: "read",
|
|
914
|
+
className: "dss_meta"
|
|
915
|
+
}, translate(t, "health.progress", {
|
|
916
|
+
done: progress.done,
|
|
917
|
+
total: total > 0 ? total : "?",
|
|
918
|
+
sec: (elapsedMs / 1e3).toFixed(1)
|
|
919
|
+
}))]));
|
|
920
|
+
}
|
|
921
|
+
if (cacheInfo !== null && !scanning && detail === null && discharge === null) {
|
|
922
|
+
const ago = relativeAgo(cacheInfo.generatedAt, Date.now());
|
|
923
|
+
lines.push((0, react.createElement)("div", {
|
|
924
|
+
key: "cache",
|
|
925
|
+
className: "dss_cacheRow"
|
|
926
|
+
}, [
|
|
927
|
+
(0, react.createElement)("span", {
|
|
928
|
+
key: "h",
|
|
929
|
+
className: "dss_meta"
|
|
930
|
+
}, translate(t, "health.cache.hint", { ago: translate(t, ago.key, { n: ago.n }) })),
|
|
931
|
+
cacheInfo.currentTotal !== cacheInfo.total && (0, react.createElement)("span", {
|
|
932
|
+
key: "chg",
|
|
933
|
+
className: "dss_why"
|
|
934
|
+
}, translate(t, "health.cache.corpusChanged", {
|
|
935
|
+
was: cacheInfo.total,
|
|
936
|
+
now: cacheInfo.currentTotal
|
|
937
|
+
})),
|
|
938
|
+
(0, react.createElement)("button", {
|
|
939
|
+
key: "r",
|
|
940
|
+
type: "button",
|
|
941
|
+
className: "dss_actBtn",
|
|
942
|
+
onClick: () => {
|
|
943
|
+
runScan({ forceRefresh: true });
|
|
944
|
+
}
|
|
945
|
+
}, translate(t, "health.cache.refresh"))
|
|
946
|
+
]));
|
|
947
|
+
}
|
|
562
948
|
if (error !== null) lines.push((0, react.createElement)("div", {
|
|
563
949
|
key: "err",
|
|
564
950
|
className: "dss_error"
|
|
@@ -608,27 +994,34 @@ window.__ModuleLoader__.load({
|
|
|
608
994
|
key: "list",
|
|
609
995
|
className: "dss_list",
|
|
610
996
|
role: "list"
|
|
611
|
-
}, findings.map((item) =>
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
997
|
+
}, findings.map((item) => {
|
|
998
|
+
const reasons = item.gates.filter((gate) => gate.level !== "ok").map((gate) => `${translate(t, GATE_LABEL[gate.id])}·${translate(t, LEVEL_LABEL[gate.level])}`).join(" · ");
|
|
999
|
+
return (0, react.createElement)("li", {
|
|
1000
|
+
key: item.sessionId,
|
|
1001
|
+
className: "dss_row"
|
|
1002
|
+
}, [
|
|
1003
|
+
(0, react.createElement)("span", {
|
|
1004
|
+
key: "lv",
|
|
1005
|
+
className: `dss_levelPill dss_level_${item.level}`
|
|
1006
|
+
}, translate(t, LEVEL_LABEL[item.level])),
|
|
1007
|
+
(0, react.createElement)("span", {
|
|
1008
|
+
key: "id",
|
|
1009
|
+
className: "dss_meta dss_uuid"
|
|
1010
|
+
}, item.sessionId),
|
|
1011
|
+
reasons !== "" && (0, react.createElement)("span", {
|
|
1012
|
+
key: "why",
|
|
1013
|
+
className: "dss_why"
|
|
1014
|
+
}, reasons),
|
|
1015
|
+
(0, react.createElement)("button", {
|
|
1016
|
+
key: "d",
|
|
1017
|
+
type: "button",
|
|
1018
|
+
className: "dss_actBtn",
|
|
1019
|
+
onClick: () => {
|
|
1020
|
+
openDetail(item.sessionId);
|
|
1021
|
+
}
|
|
1022
|
+
}, translate(t, "health.detail"))
|
|
1023
|
+
]);
|
|
1024
|
+
})));
|
|
632
1025
|
}
|
|
633
1026
|
if (report !== void 0) {
|
|
634
1027
|
lines.push((0, react.createElement)("div", {
|
|
@@ -645,7 +1038,11 @@ window.__ModuleLoader__.load({
|
|
|
645
1038
|
onClick: () => {
|
|
646
1039
|
runRepair(report.sessionId);
|
|
647
1040
|
}
|
|
648
|
-
}, repairing ?
|
|
1041
|
+
}, [repairing ? (0, react.createElement)("span", {
|
|
1042
|
+
key: "sp",
|
|
1043
|
+
className: "dss_spinner",
|
|
1044
|
+
"aria-hidden": "true"
|
|
1045
|
+
}) : null, (0, react.createElement)("span", { key: "tx" }, repairing ? translate(t, "health.repairing") : translate(t, "health.repair"))])]));
|
|
649
1046
|
const prescriptions = detail?.prescriptions ?? discharge?.prescriptions ?? [];
|
|
650
1047
|
if (prescriptions.length > 0) lines.push((0, react.createElement)("div", {
|
|
651
1048
|
key: "cmds",
|
|
@@ -686,10 +1083,14 @@ window.__ModuleLoader__.load({
|
|
|
686
1083
|
className: `dss_levelPill dss_level_${discharge.after?.level ?? "warn"}`
|
|
687
1084
|
}, `${translate(t, "health.after")}: ${translate(t, LEVEL_LABEL[discharge.after?.level ?? "warn"])}`)
|
|
688
1085
|
]));
|
|
1086
|
+
if (discharge.explanation !== void 0 && discharge.explanation !== "") lines.push((0, react.createElement)("div", {
|
|
1087
|
+
key: "verdict",
|
|
1088
|
+
className: `dss_verdict dss_verdict_${discharge.verdict ?? "failed"}`
|
|
1089
|
+
}, discharge.explanation));
|
|
689
1090
|
if (discharge.repair?.ok === true) lines.push((0, react.createElement)("div", {
|
|
690
1091
|
key: "repairNote",
|
|
691
1092
|
className: "dss_status"
|
|
692
|
-
},
|
|
1093
|
+
}, `${translate(t, "health.quarantined")} → ${discharge.repair.to ?? ""}`));
|
|
693
1094
|
else if (discharge.repair?.error !== void 0) lines.push((0, react.createElement)("div", {
|
|
694
1095
|
key: "repairErr",
|
|
695
1096
|
className: "dss_status"
|
|
@@ -700,9 +1101,9 @@ window.__ModuleLoader__.load({
|
|
|
700
1101
|
//#endregion
|
|
701
1102
|
//#region src/client/panel.tsx
|
|
702
1103
|
/**
|
|
703
|
-
*
|
|
1104
|
+
* 会话管家面板壳:一个侧边栏入口,两个页签(养老院 / 体检)。
|
|
704
1105
|
*
|
|
705
|
-
* 页签可见性由开关决定:`historyFiles=false`
|
|
1106
|
+
* 页签可见性由开关决定:`historyFiles=false` 时「养老院」消失,`healthCheck=false`
|
|
706
1107
|
* 时「体检」消失;两者都关时进入面板直接显示空壳提示(客户端也会隐藏入口)。
|
|
707
1108
|
*/
|
|
708
1109
|
/** 页签 id(导出供测试断言)。 */
|
|
@@ -785,18 +1186,23 @@ window.__ModuleLoader__.load({
|
|
|
785
1186
|
}, translate(t, "panel.close"))
|
|
786
1187
|
]), body])]), document.body);
|
|
787
1188
|
}
|
|
788
|
-
/**
|
|
1189
|
+
/** 侧边栏脚部入口:宽栏带文案成行控件,收起轨道退化为 36x36 图标钮。 */
|
|
789
1190
|
function StewardFooter(props) {
|
|
1191
|
+
const wide = props.wide === true;
|
|
790
1192
|
return (0, react.createElement)("button", {
|
|
791
1193
|
type: "button",
|
|
792
|
-
className: "dss_footerEntry",
|
|
793
|
-
title: props.title
|
|
794
|
-
"aria-label": props.title
|
|
1194
|
+
className: wide ? "dss_footerEntry" : "dss_footerEntry dss_footerEntryRail",
|
|
1195
|
+
title: props.title,
|
|
1196
|
+
"aria-label": props.title,
|
|
795
1197
|
onClick: props.onClick
|
|
796
|
-
}, (0, react.createElement)("span", {
|
|
1198
|
+
}, [(0, react.createElement)("span", {
|
|
1199
|
+
key: "icon",
|
|
797
1200
|
className: "dss_footerIcon",
|
|
798
1201
|
"aria-hidden": true
|
|
799
|
-
}, "🧭"))
|
|
1202
|
+
}, "🧭"), wide && props.label !== void 0 ? (0, react.createElement)("span", {
|
|
1203
|
+
key: "label",
|
|
1204
|
+
className: "dss_footerLabel"
|
|
1205
|
+
}, props.label) : null]);
|
|
800
1206
|
}
|
|
801
1207
|
//#endregion
|
|
802
1208
|
//#region src/client/index.ts
|
|
@@ -812,10 +1218,13 @@ window.__ModuleLoader__.load({
|
|
|
812
1218
|
const style = document.createElement("style");
|
|
813
1219
|
style.id = id;
|
|
814
1220
|
style.textContent = `
|
|
815
|
-
.dss_entryWrap{display:inline-flex;align-items:center}
|
|
816
|
-
.
|
|
1221
|
+
.dss_entryWrap{flex:0 1 auto;display:inline-flex;align-items:center;min-width:0}
|
|
1222
|
+
.dss_entryWrapWide{margin-left:6px}
|
|
1223
|
+
.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}
|
|
817
1224
|
.dss_footerEntry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
818
|
-
.
|
|
1225
|
+
.dss_footerEntryRail{width:28px;height:28px;padding:0;gap:0;border-radius:50%}
|
|
1226
|
+
.dss_footerIcon{flex:none;font-size:15px;line-height:1}
|
|
1227
|
+
.dss_footerLabel{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
819
1228
|
.dss_backdrop{position:fixed;inset:0;background:rgba(15,20,30,.42);z-index:1000}
|
|
820
1229
|
.dss_panel{position:fixed;z-index:1001;left:50%;top:8vh;transform:translateX(-50%);width:min(760px,92vw);max-height:78vh;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}
|
|
821
1230
|
.dss_dialogHead{display:flex;align-items:center;gap:10px;min-height:34px}
|
|
@@ -838,6 +1247,8 @@ window.__ModuleLoader__.load({
|
|
|
838
1247
|
.dss_meta{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
|
|
839
1248
|
.dss_metaLine{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af)}
|
|
840
1249
|
.dss_uuid{font-family:var(--ds-font-family-code,monospace)}
|
|
1250
|
+
/* 按行的磁盘占用:右对齐、不参与标题压缩,方便纵向比对「哪条最占地方」。 */
|
|
1251
|
+
.dss_size{flex:none;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
841
1252
|
.dss_check{display:inline-flex;align-items:center}
|
|
842
1253
|
.dss_status{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
|
|
843
1254
|
.dss_warnText{color:var(--dsw-alias-state-warning-primary,#d97706)}
|
|
@@ -851,12 +1262,36 @@ window.__ModuleLoader__.load({
|
|
|
851
1262
|
.dss_level_ok{background:rgba(22,163,74,.12);color:#15803d}
|
|
852
1263
|
.dss_level_warn{background:rgba(217,119,6,.14);color:#b45309}
|
|
853
1264
|
.dss_level_fail{background:rgba(220,38,38,.14);color:#b91c1c}
|
|
1265
|
+
.dss_level_skipped{background:rgba(107,114,128,.14);color:#4b5563}
|
|
1266
|
+
.dss_progressRow{display:flex;align-items:center;gap:8px;margin-top:2px}
|
|
1267
|
+
.dss_progressTrack{flex:1;height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#eef0f3);overflow:hidden}
|
|
1268
|
+
.dss_progressFill{height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary,#3d5be0);transition:width .2s ease}
|
|
1269
|
+
.dss_progressIndeterminate{width:35%;animation:dssSlide 1.1s ease-in-out infinite}
|
|
1270
|
+
.dss_why{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex:1;min-width:0}
|
|
1271
|
+
.dss_cacheRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px}
|
|
1272
|
+
.dss_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}
|
|
1273
|
+
@keyframes dssSpin{to{transform:rotate(360deg)}}
|
|
1274
|
+
@keyframes dssSlide{0%{margin-left:-35%}100%{margin-left:100%}}
|
|
1275
|
+
@media (prefers-reduced-motion:reduce){.dss_spinner{animation:none}.dss_progressIndeterminate{animation:none;width:100%;opacity:.5}.dss_setChev{transition:none}.dss_setCard{transition:none}}
|
|
1276
|
+
.dss_verdict{font-size:12px;line-height:18px;border-radius:8px;padding:6px 10px;margin-top:6px;border:1px solid transparent}
|
|
1277
|
+
.dss_verdict_repaired{background:rgba(22,163,74,.10);color:#15803d;border-color:rgba(22,163,74,.28)}
|
|
1278
|
+
.dss_verdict_repaired-with-residual{background:rgba(217,119,6,.10);color:#b45309;border-color:rgba(217,119,6,.28)}
|
|
1279
|
+
.dss_verdict_nothing-to-do{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
|
|
1280
|
+
.dss_verdict_not-applicable{background:rgba(107,114,128,.10);color:#4b5563;border-color:rgba(107,114,128,.28)}
|
|
1281
|
+
.dss_verdict_failed{background:rgba(220,38,38,.10);color:#b91c1c;border-color:rgba(220,38,38,.28)}
|
|
854
1282
|
.dss_phaseRow{display:flex;gap:8px;align-items:center}
|
|
855
1283
|
.dss_phase{font-weight:600;font-size:12px}
|
|
856
1284
|
.dss_cmdBlock{display:flex;flex-direction:column;gap:4px;border:1px dashed var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:6px 8px}
|
|
857
1285
|
.dss_cmdLine{display:flex;gap:6px;align-items:center}
|
|
858
1286
|
.dss_cmd{flex:1 1 auto;font-family:var(--ds-font-family-code,monospace);font-size:11px;white-space:pre-wrap;word-break:break-all}
|
|
859
|
-
.
|
|
1287
|
+
.dss_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}
|
|
1288
|
+
.dss_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}
|
|
1289
|
+
.dss_setHeadText{display:flex;flex-direction:column;gap:2px;flex:1 1 0%;min-width:0}
|
|
1290
|
+
.dss_setCardTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#111827)}
|
|
1291
|
+
.dss_setCardDesc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9ca3af)}
|
|
1292
|
+
.dss_setChev{color:var(--dsw-alias-label-tertiary,#9ca3af);flex:0 0 auto;transition:transform .16s}
|
|
1293
|
+
.dss_setCardOpen .dss_setChev{transform:rotate(180deg)}
|
|
1294
|
+
.dss_setBody{display:flex;flex-direction:column;gap:2px;padding:0 16px 12px}
|
|
860
1295
|
.dss_setRow{display:flex;align-items:center;gap:10px;padding:6px 0}
|
|
861
1296
|
.dss_setText{display:flex;flex-direction:column;flex:1 1 auto;min-width:0}
|
|
862
1297
|
.dss_setTitle{font-size:13px;color:var(--dsw-alias-label-primary,#111827)}
|
|
@@ -922,10 +1357,15 @@ window.__ModuleLoader__.load({
|
|
|
922
1357
|
};
|
|
923
1358
|
const historyFiles = config.historyFiles !== false;
|
|
924
1359
|
const healthCheck = config.healthCheck !== false;
|
|
925
|
-
|
|
926
|
-
|
|
1360
|
+
const visible = config.enabled !== false && (historyFiles || healthCheck);
|
|
1361
|
+
const wide = props.wide === true;
|
|
1362
|
+
const label = translate(void 0, "panel.title");
|
|
1363
|
+
if (!visible) return (0, react.createElement)("span", { className: "dss_entryWrap" });
|
|
1364
|
+
return (0, react.createElement)("span", { className: wide ? "dss_entryWrap dss_entryWrapWide" : "dss_entryWrap" }, [(0, react.createElement)(StewardFooter, {
|
|
927
1365
|
key: "btn",
|
|
928
|
-
title:
|
|
1366
|
+
title: label,
|
|
1367
|
+
label,
|
|
1368
|
+
wide,
|
|
929
1369
|
onClick: () => {
|
|
930
1370
|
setOpen(!open);
|
|
931
1371
|
}
|