dsh-stop-service 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/lib/client.js +173 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,15 @@ A stop-service button for DeepSeek Harness (DSH) Web: one confirmed click gracef
|
|
|
4
4
|
|
|
5
5
|
## 功能 / Features
|
|
6
6
|
|
|
7
|
-
- **实时服务信息**:进程 PID、启动时间、运行时长、活跃会话数、内存占用、CPU 占用、Node / DSH 宿主 / 插件版本、平台——每 5
|
|
7
|
+
- **实时服务信息**:进程 PID、启动时间、运行时长、活跃会话数、内存占用、CPU 占用、Node / DSH 宿主 / 插件版本、平台——每 5 秒自动刷新,附内存/CPU 趋势迷你图;CPU ≥ 80% 或内存 ≥ 1024 MB 时对应行高亮警示
|
|
8
|
+
- **更新提醒**:面板顶部对比 npm registry 最新版,DSH 宿主或插件有新版时提示并附升级命令(浏览器端直查 registry,宿主不外联)
|
|
8
9
|
- **复制诊断信息**:一键复制版本与运行状态摘要,提 issue 时直接粘贴
|
|
9
10
|
- 带**确认弹窗**的「终止服务」按钮:确认框显示将断开的活跃会话数;宿主进程收到 SIGTERM 优雅退出——会话落盘、端口释放,效果与在终端执行 `kill <pid>` 完全一致
|
|
10
11
|
- **服务回归自动刷新**:停止后持续探测,终端里重启完成时页面自动刷新接回
|
|
11
12
|
- 界面适配 `--dsw-*` 主题变量,跟随皮肤切换;中英双语文案
|
|
12
13
|
|
|
13
|
-
- Live service info: PID, start time, uptime, active sessions, memory, CPU, Node/DSH/plugin versions, platform — auto-refreshed every 5s, with
|
|
14
|
+
- Live service info: PID, start time, uptime, active sessions, memory, CPU, Node/DSH/plugin versions, platform — auto-refreshed every 5s, with memory/CPU sparklines; rows highlight when CPU ≥ 80% or memory ≥ 1024 MB.
|
|
15
|
+
- Update banner: compares against npm dist-tags from the browser (the host never phones out) and shows the upgrade command when a newer DSH host or plugin exists.
|
|
14
16
|
- Copy-diagnostics button for issue reports.
|
|
15
17
|
- Confirm-guarded stop button that names the live session count; SIGTERM graceful shutdown, identical to `kill <pid>` from a terminal.
|
|
16
18
|
- Auto-reload once the restarted host comes back.
|
|
@@ -23,17 +25,19 @@ A stop-service button for DeepSeek Harness (DSH) Web: one confirmed click gracef
|
|
|
23
25
|
|
|
24
26
|
## 安装 / Install
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
> 本插件**未发布到 npm**——`npm install dsh-stop-service` 与 `dsh plugin add dsh-stop-service` 均不可用,请使用下方 Git 安装方式。
|
|
28
|
-
> This plugin is **NOT published on npm** — the npm-based install commands will not work. Use the Git-based methods below.
|
|
28
|
+
方式一 / Option 1 — npm 直装(推荐 / recommended):
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
```sh
|
|
31
|
+
dsh plugin --profile web add dsh-stop-service
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
方式二 / Option 2 — GitHub 直装:
|
|
31
35
|
|
|
32
36
|
```sh
|
|
33
37
|
dsh plugin --profile web add github:KhaosGx/dsh-stop-service
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
方式三 / Option 3 — 克隆后本地 link(适合参与开发 / for development):
|
|
37
41
|
|
|
38
42
|
```sh
|
|
39
43
|
git clone https://github.com/KhaosGx/dsh-stop-service.git
|
package/lib/client.js
CHANGED
|
@@ -62,9 +62,39 @@ window.__ModuleLoader__.load({
|
|
|
62
62
|
return `${sec}${t("secUnit")}`;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
65
|
+
/** Normalize values into a 100x28 viewBox polyline. */
|
|
66
|
+
function sparklinePoints(values) {
|
|
67
|
+
const min = Math.min(...values);
|
|
68
|
+
const max = Math.max(...values);
|
|
69
|
+
const span = max - min || 1;
|
|
70
|
+
const step = 100 / (values.length - 1);
|
|
71
|
+
return values
|
|
72
|
+
.map((v, i) => `${(i * step).toFixed(1)},${(25 - ((v - min) / span) * 22).toFixed(1)}`)
|
|
73
|
+
.join(" ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One labeled sparkline; renders nothing before two samples exist. */
|
|
77
|
+
const TrendChart = ({ label, values }) => values.length >= 2
|
|
78
|
+
? jsx("div", { style: { marginTop: 10 }, children: [
|
|
79
|
+
jsx("div", { style: { fontSize: 11, opacity: 0.55, marginBottom: 4 }, children: label }),
|
|
80
|
+
jsx("svg", {
|
|
81
|
+
width: "100%",
|
|
82
|
+
height: 28,
|
|
83
|
+
viewBox: "0 0 100 28",
|
|
84
|
+
preserveAspectRatio: "none",
|
|
85
|
+
style: { display: "block" },
|
|
86
|
+
children: jsx("polyline", {
|
|
87
|
+
points: sparklinePoints(values),
|
|
88
|
+
fill: "none",
|
|
89
|
+
stroke: "var(--dsw-accent, #4a90d9)",
|
|
90
|
+
strokeWidth: 1.4,
|
|
91
|
+
vectorEffect: "non-scaling-stroke"
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
] })
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
const InfoCard = ({ t, info, history, cpuHistory }) => {
|
|
68
98
|
const [copied, setCopied] = react.useState(false);
|
|
69
99
|
|
|
70
100
|
const copyDiagnostics = async () => {
|
|
@@ -87,40 +117,24 @@ window.__ModuleLoader__.load({
|
|
|
87
117
|
};
|
|
88
118
|
|
|
89
119
|
react.useEffect(() => {
|
|
90
|
-
|
|
91
|
-
const load = async () => {
|
|
92
|
-
try {
|
|
93
|
-
const res = await fetch("/api/dsh-stop-service/info");
|
|
94
|
-
const data = await res.json();
|
|
95
|
-
if (alive && data?.ok) {
|
|
96
|
-
setInfo(data);
|
|
97
|
-
if (typeof data.memoryRssMb === "number") {
|
|
98
|
-
setHistory((prev) => [...prev, data.memoryRssMb].slice(-36));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} catch {
|
|
102
|
-
// Host unreachable (e.g. stopping); keep the last snapshot.
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
load();
|
|
106
|
-
const timer = window.setInterval(load, 5000);
|
|
107
|
-
return () => {
|
|
108
|
-
alive = false;
|
|
109
|
-
window.clearInterval(timer);
|
|
110
|
-
};
|
|
120
|
+
// Polling lives in StopServiceTab; this card only renders.
|
|
111
121
|
}, []);
|
|
112
122
|
|
|
113
123
|
if (info === null) {
|
|
114
124
|
return jsx("p", { style: { fontSize: 13, opacity: 0.6 }, children: t("loading") });
|
|
115
125
|
}
|
|
116
126
|
|
|
127
|
+
/** Display thresholds for the warning highlight. */
|
|
128
|
+
const CPU_WARN_PERCENT = 80;
|
|
129
|
+
const MEM_WARN_MB = 1024;
|
|
130
|
+
|
|
117
131
|
const rows = [
|
|
118
132
|
[t("infoPid"), String(info.pid)],
|
|
119
133
|
[t("infoStartedAt"), new Date(info.startedAt).toLocaleString()],
|
|
120
134
|
[t("infoUptime"), formatUptime(info.uptimeMs, t)],
|
|
121
135
|
[t("infoSessions"), typeof info.sessionCount === "number" ? String(info.sessionCount) : "-"],
|
|
122
|
-
[t("infoMemory"), `${info.memoryRssMb} MB
|
|
123
|
-
[t("infoCpu"), typeof info.cpuPercent === "number" ? `${info.cpuPercent}%` : "-"],
|
|
136
|
+
[t("infoMemory"), `${info.memoryRssMb} MB`, info.memoryRssMb >= MEM_WARN_MB],
|
|
137
|
+
[t("infoCpu"), typeof info.cpuPercent === "number" ? `${info.cpuPercent}%` : "-", typeof info.cpuPercent === "number" && info.cpuPercent >= CPU_WARN_PERCENT],
|
|
124
138
|
[t("infoNode"), info.node ?? "-"],
|
|
125
139
|
[t("infoHostVersion"), info.hostVersion ?? "-"],
|
|
126
140
|
[t("infoPluginVersion"), info.pluginVersion ?? "-"],
|
|
@@ -155,38 +169,25 @@ window.__ModuleLoader__.load({
|
|
|
155
169
|
})
|
|
156
170
|
]
|
|
157
171
|
}),
|
|
158
|
-
...rows.map(([label, value]) => jsx("div", { style: rowStyle, children: [
|
|
172
|
+
...rows.map(([label, value, warn]) => jsx("div", { style: rowStyle, children: [
|
|
159
173
|
jsx("span", { style: labelStyle, children: label }),
|
|
160
|
-
jsx("span", {
|
|
174
|
+
jsx("span", {
|
|
175
|
+
style: warn === true
|
|
176
|
+
? { ...valueStyle, color: "var(--dsw-danger, #d33)", fontWeight: 600 }
|
|
177
|
+
: valueStyle,
|
|
178
|
+
children: value
|
|
179
|
+
})
|
|
161
180
|
] }, label)),
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
children: jsx("polyline", {
|
|
173
|
-
points: (() => {
|
|
174
|
-
const min = Math.min(...history);
|
|
175
|
-
const max = Math.max(...history);
|
|
176
|
-
const span = max - min || 1;
|
|
177
|
-
const step = 100 / (history.length - 1);
|
|
178
|
-
return history
|
|
179
|
-
.map((v, i) => `${(i * step).toFixed(1)},${(25 - ((v - min) / span) * 22).toFixed(1)}`)
|
|
180
|
-
.join(" ");
|
|
181
|
-
})(),
|
|
182
|
-
fill: "none",
|
|
183
|
-
stroke: "var(--dsw-accent, #4a90d9)",
|
|
184
|
-
strokeWidth: 1.4,
|
|
185
|
-
vectorEffect: "non-scaling-stroke"
|
|
186
|
-
})
|
|
187
|
-
})
|
|
188
|
-
] })
|
|
189
|
-
: null,
|
|
181
|
+
jsx(TrendChart, {
|
|
182
|
+
key: "mem-chart",
|
|
183
|
+
label: `${t("memTrend")} (${history.length * 5}s)`,
|
|
184
|
+
values: history
|
|
185
|
+
}),
|
|
186
|
+
jsx(TrendChart, {
|
|
187
|
+
key: "cpu-chart",
|
|
188
|
+
label: `${t("cpuTrend")} (${cpuHistory.length * 5}s)`,
|
|
189
|
+
values: cpuHistory
|
|
190
|
+
}),
|
|
190
191
|
]
|
|
191
192
|
});
|
|
192
193
|
};
|
|
@@ -282,11 +283,120 @@ window.__ModuleLoader__.load({
|
|
|
282
283
|
});
|
|
283
284
|
};
|
|
284
285
|
|
|
286
|
+
/** Minimal semver compare (numeric fields; release outranks prerelease; unknown → 0). */
|
|
287
|
+
function compareVersion(a, b) {
|
|
288
|
+
const pattern = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
289
|
+
const pa = pattern.exec(String(a ?? ""));
|
|
290
|
+
const pb = pattern.exec(String(b ?? ""));
|
|
291
|
+
if (pa === null || pb === null) return 0;
|
|
292
|
+
for (let i = 1; i <= 3; i += 1) {
|
|
293
|
+
const diff = Number(pa[i]) - Number(pb[i]);
|
|
294
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
295
|
+
}
|
|
296
|
+
if (pa[4] === pb[4]) return 0;
|
|
297
|
+
if (pa[4] === undefined) return 1;
|
|
298
|
+
if (pb[4] === undefined) return -1;
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Update banner: compares local host/plugin versions (from /info) against
|
|
304
|
+
* the npm registry's public dist-tags, queried directly from the browser.
|
|
305
|
+
* Offline-tolerant — the banner simply stays hidden.
|
|
306
|
+
*/
|
|
307
|
+
const UpdateBanner = ({ t, info }) => {
|
|
308
|
+
const [lines, setLines] = react.useState(null);
|
|
309
|
+
|
|
310
|
+
react.useEffect(() => {
|
|
311
|
+
if (info === null) return;
|
|
312
|
+
let alive = true;
|
|
313
|
+
(async () => {
|
|
314
|
+
let hostLatest = null;
|
|
315
|
+
let pluginLatest = null;
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetch("https://registry.npmjs.org/-/package/@deepseek-ai/dsh/dist-tags");
|
|
318
|
+
if (res.ok) {
|
|
319
|
+
const tags = await res.json();
|
|
320
|
+
if (typeof tags?.latest === "string") hostLatest = tags.latest;
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
// Registry unreachable; hide the banner.
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const res = await fetch("https://registry.npmjs.org/-/package/dsh-stop-service/dist-tags");
|
|
327
|
+
if (res.ok) {
|
|
328
|
+
const tags = await res.json();
|
|
329
|
+
if (typeof tags?.latest === "string") pluginLatest = tags.latest;
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
// Registry unreachable; hide the banner.
|
|
333
|
+
}
|
|
334
|
+
if (!alive) return;
|
|
335
|
+
const found = [];
|
|
336
|
+
if (hostLatest !== null && info.hostVersion && compareVersion(hostLatest, info.hostVersion) > 0) {
|
|
337
|
+
found.push(t("updHost").replace("{v}", hostLatest).replace("{c}", info.hostVersion));
|
|
338
|
+
}
|
|
339
|
+
if (pluginLatest !== null && info.pluginVersion && compareVersion(pluginLatest, info.pluginVersion) > 0) {
|
|
340
|
+
found.push(t("updPlugin").replace("{v}", pluginLatest).replace("{c}", info.pluginVersion));
|
|
341
|
+
}
|
|
342
|
+
if (found.length > 0) setLines(found);
|
|
343
|
+
})();
|
|
344
|
+
return () => {
|
|
345
|
+
alive = false;
|
|
346
|
+
};
|
|
347
|
+
}, [info === null ? null : info.hostVersion, info === null ? null : info.pluginVersion]);
|
|
348
|
+
|
|
349
|
+
if (lines === null) return null;
|
|
350
|
+
return jsx("div", {
|
|
351
|
+
style: {
|
|
352
|
+
border: "1px solid var(--dsw-accent, #4a90d9)",
|
|
353
|
+
borderRadius: 10,
|
|
354
|
+
padding: "10px 14px",
|
|
355
|
+
marginBottom: 16,
|
|
356
|
+
fontSize: 13,
|
|
357
|
+
lineHeight: 1.7,
|
|
358
|
+
},
|
|
359
|
+
children: lines.map((line) => jsx("p", { style: { margin: 0 }, children: line }, line))
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
|
|
285
363
|
const StopServiceTab = ({ t }) => {
|
|
364
|
+
const [info, setInfo] = react.useState(null);
|
|
365
|
+
const [history, setHistory] = react.useState([]);
|
|
366
|
+
const [cpuHistory, setCpuHistory] = react.useState([]);
|
|
367
|
+
|
|
368
|
+
react.useEffect(() => {
|
|
369
|
+
let alive = true;
|
|
370
|
+
const load = async () => {
|
|
371
|
+
try {
|
|
372
|
+
const res = await fetch("/api/dsh-stop-service/info");
|
|
373
|
+
const data = await res.json();
|
|
374
|
+
if (alive && data?.ok) {
|
|
375
|
+
setInfo(data);
|
|
376
|
+
if (typeof data.memoryRssMb === "number") {
|
|
377
|
+
setHistory((prev) => [...prev, data.memoryRssMb].slice(-36));
|
|
378
|
+
}
|
|
379
|
+
if (typeof data.cpuPercent === "number") {
|
|
380
|
+
setCpuHistory((prev) => [...prev, data.cpuPercent].slice(-36));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
// Host unreachable (e.g. stopping); keep the last snapshot.
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
load();
|
|
388
|
+
const timer = window.setInterval(load, 5000);
|
|
389
|
+
return () => {
|
|
390
|
+
alive = false;
|
|
391
|
+
window.clearInterval(timer);
|
|
392
|
+
};
|
|
393
|
+
}, []);
|
|
394
|
+
|
|
286
395
|
return jsx("div", {
|
|
287
396
|
style: { padding: "24px 8px", maxWidth: 560 },
|
|
288
397
|
children: [
|
|
289
|
-
jsx(
|
|
398
|
+
jsx(UpdateBanner, { key: "updates", t: t, info: info }),
|
|
399
|
+
jsx(InfoCard, { key: "info", t: t, info: info, history: history, cpuHistory: cpuHistory }),
|
|
290
400
|
jsx(StopCard, { key: "stop", t: t }),
|
|
291
401
|
]
|
|
292
402
|
});
|
|
@@ -311,6 +421,7 @@ window.__ModuleLoader__.load({
|
|
|
311
421
|
infoMemory: "内存占用 (RSS)",
|
|
312
422
|
infoCpu: "CPU 占用",
|
|
313
423
|
memTrend: "内存趋势",
|
|
424
|
+
cpuTrend: "CPU 趋势",
|
|
314
425
|
infoNode: "Node 版本",
|
|
315
426
|
infoHostVersion: "DSH 宿主版本",
|
|
316
427
|
infoPluginVersion: "插件版本",
|
|
@@ -320,6 +431,8 @@ window.__ModuleLoader__.load({
|
|
|
320
431
|
minUnit: "分",
|
|
321
432
|
secUnit: "秒",
|
|
322
433
|
title: "终止服务",
|
|
434
|
+
updHost: "DSH 宿主有新版本 {v} 可用(当前 {c}),可在终端升级:npm install -g @deepseek-ai/dsh@{v}",
|
|
435
|
+
updPlugin: "插件有新版本 {v} 可用(当前 {c}):dsh plugin --profile web add dsh-stop-service@{v}",
|
|
323
436
|
desc: "优雅停止当前 DeepSeek Harness 宿主进程(SIGTERM,与终端 kill 相同):会话落盘、端口释放,不会留下后台孤儿进程。",
|
|
324
437
|
hint: "停止后需要重启:在终端运行 npx @deepseek-ai/dsh web,服务回归后本页自动刷新。",
|
|
325
438
|
stop: "终止服务",
|
|
@@ -341,6 +454,7 @@ window.__ModuleLoader__.load({
|
|
|
341
454
|
infoMemory: "Memory (RSS)",
|
|
342
455
|
infoCpu: "CPU",
|
|
343
456
|
memTrend: "Memory trend",
|
|
457
|
+
cpuTrend: "CPU trend",
|
|
344
458
|
infoNode: "Node",
|
|
345
459
|
infoHostVersion: "DSH host",
|
|
346
460
|
infoPluginVersion: "Plugin",
|
|
@@ -350,6 +464,8 @@ window.__ModuleLoader__.load({
|
|
|
350
464
|
minUnit: "m ",
|
|
351
465
|
secUnit: "s",
|
|
352
466
|
title: "Stop Service",
|
|
467
|
+
updHost: "A newer DSH host {v} is available (current {c}) — upgrade from a terminal: npm install -g @deepseek-ai/dsh@{v}",
|
|
468
|
+
updPlugin: "A newer plugin {v} is available (current {c}) — dsh plugin --profile web add dsh-stop-service@{v}",
|
|
353
469
|
desc: "Gracefully stops the current DeepSeek Harness host process (SIGTERM, same as a terminal kill): sessions flush and the port is released — no orphaned background process.",
|
|
354
470
|
hint: "To start it again, run npx @deepseek-ai/dsh web in a terminal; this page reloads automatically once it is back.",
|
|
355
471
|
stop: "Stop Service",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-stop-service",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"description": "A stop-service button for DeepSeek Harness Web: one confirmed click gracefully terminates the running dsh host (SIGTERM) — no orphaned background processes",
|