dsh-stop-service 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +11 -7
  2. package/lib/client.js +190 -78
  3. 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 a memory sparkline.
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
- > [!IMPORTANT]
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
- 方式一 / Option 1 — GitHub 直装(实测于 dsh 0.1.2-rc.1 / verified on dsh 0.1.2-rc.1):
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
- 方式二 / Option 2 — 克隆后本地 link(适合参与开发 / for development):
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
- const InfoCard = ({ t }) => {
66
- const [info, setInfo] = react.useState(null);
67
- const [history, setHistory] = react.useState([]);
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
- let alive = true;
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", { style: valueStyle, children: value })
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
- history.length >= 2
163
- ? jsx("div", { key: "sparkline", style: { marginTop: 10 }, children: [
164
- jsx("div", { key: "label", style: { fontSize: 11, opacity: 0.55, marginBottom: 4 }, children: `${t("memTrend")} (${history.length * 5}s)` }),
165
- jsx("svg", {
166
- key: "svg",
167
- width: "100%",
168
- height: 28,
169
- viewBox: "0 0 100 28",
170
- preserveAspectRatio: "none",
171
- style: { display: "block" },
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
  };
@@ -246,31 +247,31 @@ window.__ModuleLoader__.load({
246
247
  }
247
248
 
248
249
  return jsx("div", {
249
- style: cardStyle,
250
+ style: {
251
+ ...cardStyle,
252
+ padding: "12px 16px",
253
+ display: "flex",
254
+ justifyContent: "space-between",
255
+ alignItems: "center",
256
+ gap: 16,
257
+ marginBottom: 20,
258
+ flexWrap: "wrap",
259
+ },
250
260
  children: [
251
- jsx("h3", {
252
- key: "title",
253
- style: { margin: "0 0 8px", fontSize: 15 },
254
- children: t("title")
255
- }),
256
- jsx("p", {
261
+ jsx("span", {
257
262
  key: "desc",
258
- style: { margin: "0 0 4px", fontSize: 13, opacity: 0.85, lineHeight: 1.7 },
259
- children: t("desc")
260
- }),
261
- jsx("p", {
262
- key: "hint",
263
- style: { margin: "0 0 16px", fontSize: 12, opacity: 0.6, lineHeight: 1.7 },
264
- children: t("hint")
263
+ style: { fontSize: 12.5, opacity: 0.8, lineHeight: 1.6 },
264
+ children: t("stopRowDesc")
265
265
  }),
266
266
  jsx("button", {
267
267
  key: "button",
268
268
  onClick: onStop,
269
269
  disabled: state === "stopping",
270
270
  style: {
271
- padding: "8px 18px",
271
+ padding: "7px 16px",
272
272
  fontSize: 13,
273
273
  borderRadius: 8,
274
+ flexShrink: 0,
274
275
  border: "1px solid var(--dsw-danger, #d33)",
275
276
  background: state === "stopping" ? "transparent" : "var(--dsw-danger, #d33)",
276
277
  color: state === "stopping" ? "var(--dsw-danger, #d33)" : "var(--dsw-danger-text, #fff)",
@@ -282,12 +283,121 @@ 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(InfoCard, { key: "info", t: t }),
398
+ jsx(UpdateBanner, { key: "updates", t: t, info: info }),
290
399
  jsx(StopCard, { key: "stop", t: t }),
400
+ jsx(InfoCard, { key: "info", t: t, info: info, history: history, cpuHistory: cpuHistory }),
291
401
  ]
292
402
  });
293
403
  };
@@ -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: "插件版本",
@@ -319,9 +430,9 @@ window.__ModuleLoader__.load({
319
430
  hourUnit: "小时",
320
431
  minUnit: "分",
321
432
  secUnit: "秒",
322
- title: "终止服务",
323
- desc: "优雅停止当前 DeepSeek Harness 宿主进程(SIGTERM,与终端 kill 相同):会话落盘、端口释放,不会留下后台孤儿进程。",
324
- hint: "停止后需要重启:在终端运行 npx @deepseek-ai/dsh web,服务回归后本页自动刷新。",
433
+ stopRowDesc: "优雅停止宿主进程(SIGTERM,与终端 kill 等效),不留孤儿进程。",
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}",
325
436
  stop: "终止服务",
326
437
  stopping: "正在停止…",
327
438
  confirm: "确定要终止当前 DeepSeek Harness 服务吗?所有会话将断开(已保存的会话不受影响)。",
@@ -341,6 +452,7 @@ window.__ModuleLoader__.load({
341
452
  infoMemory: "Memory (RSS)",
342
453
  infoCpu: "CPU",
343
454
  memTrend: "Memory trend",
455
+ cpuTrend: "CPU trend",
344
456
  infoNode: "Node",
345
457
  infoHostVersion: "DSH host",
346
458
  infoPluginVersion: "Plugin",
@@ -349,9 +461,9 @@ window.__ModuleLoader__.load({
349
461
  hourUnit: "h ",
350
462
  minUnit: "m ",
351
463
  secUnit: "s",
352
- title: "Stop Service",
353
- 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
- hint: "To start it again, run npx @deepseek-ai/dsh web in a terminal; this page reloads automatically once it is back.",
464
+ stopRowDesc: "Gracefully stop the host process (SIGTERM, like a terminal kill) — no orphaned processes.",
465
+ updHost: "A newer DSH host {v} is available (current {c}) upgrade from a terminal: npm install -g @deepseek-ai/dsh@{v}",
466
+ updPlugin: "A newer plugin {v} is available (current {c}) dsh plugin --profile web add dsh-stop-service@{v}",
355
467
  stop: "Stop Service",
356
468
  stopping: "Stopping…",
357
469
  confirm: "Stop the current DeepSeek Harness service? All sessions disconnect (saved sessions are unaffected).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-stop-service",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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",