dsh-plugin 1.6.1 → 1.6.2

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.
@@ -26,6 +26,7 @@ import { appendLog, clearLog, readLog, logFilePath, defaultLogFilePath, customLo
26
26
  * 不用 `/bin/sh -c`:Windows 无 /bin/sh,且 lsof/nohup 仅 POSIX 存在。
27
27
  * 由 `spawn(process.execPath, ['-e', SCRIPT, port], { detached, stdio:'ignore' })` 孵化,
28
28
  * 宿主进程被 kill 后仍能完成「停旧 → 等端口释放 → 拉起新 dsh web」。
29
+ * 端口探测不到监听进程时不再假装重启(记一行日志后退出,见脚本内注释)。
29
30
  */
30
31
  const HOST_RESTART_SCRIPT = String.raw `const C = require('node:child_process')
31
32
  const F = require('node:fs')
@@ -52,7 +53,16 @@ function findPids() {
52
53
  return String(out.stdout || '').split(/\s+/).filter(Boolean)
53
54
  }
54
55
 
55
- for (const pid of findPids()) {
56
+ const targets = findPids()
57
+ // 一个监听进程都没找到 = 端口探测本身就失败了(netstat 输出格式 / 本地化差异),
58
+ // 旧宿主仍占着端口。此时再拉一个 dsh web 只会在日志里以 EADDRINUSE 静默退出,
59
+ // 用户却看到「重启完成」——不制造这种假象,留一行可诊断的记录后退出。
60
+ if (targets.length === 0) {
61
+ logLine('restart aborted: no listener found on port ' + port)
62
+ process.exit(0)
63
+ }
64
+
65
+ for (const pid of targets) {
56
66
  try {
57
67
  if (win) C.spawnSync('taskkill', ['/pid', pid, '/t', '/f'])
58
68
  else process.kill(Number(pid), 'SIGTERM')
@@ -65,6 +75,14 @@ let tries = 0
65
75
  setTimeout(waitForStop, 250)
66
76
  })()
67
77
 
78
+ function logLine(msg) {
79
+ try {
80
+ const dir = P.join(O.homedir(), '.dsh', 'logs')
81
+ F.mkdirSync(dir, { recursive: true })
82
+ F.appendFileSync(P.join(dir, 'dsh-web-' + port + '.log'), '[' + new Date().toISOString() + '] ' + msg + '\n')
83
+ } catch {}
84
+ }
85
+
68
86
  function startHost() {
69
87
  const dir = P.join(O.homedir(), '.dsh', 'logs')
70
88
  F.mkdirSync(dir, { recursive: true })
@@ -76,6 +94,13 @@ function startHost() {
76
94
  })
77
95
  child.unref()
78
96
  }`;
97
+ /**
98
+ * 宿主是否由桌面应用壳托管(`ELECTRON_RUN_AS_NODE=1`:壳用 Electron 二进制以 Node 模式跑宿主)。
99
+ * 桌面端的宿主归应用壳管,插件重启不了它,也不该去 kill —— 详见 /restart 路由。
100
+ */
101
+ export function isDesktopHost() {
102
+ return process.env.ELECTRON_RUN_AS_NODE === '1';
103
+ }
79
104
  const PROFILE_RE = /^[A-Za-z0-9_-]+$/;
80
105
  const BODY_LIMIT_BYTES = 4 * 1024;
81
106
  const COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
@@ -1133,6 +1158,15 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
1133
1158
  handler: (request, response) => {
1134
1159
  if (!requireTrustedPost(request, response))
1135
1160
  return;
1161
+ // 桌面端:宿主由应用壳托管,插件重启不了它。Windows 上 `taskkill /T` 会把发起
1162
+ // 重启的助手一起结束(助手是宿主的子进程),壳再把这次外部 kill 判成宿主崩溃
1163
+ // 弹「应用无法启动或已意外停止」;退一步说,脱离壳拉起的 `dsh web` 也服务不了
1164
+ // 桌面 UI(缺壳注入的 desktop:boot)。故不 kill、不拉新宿主,由客户端改提示
1165
+ // 用户从菜单/托盘退出后重新打开。
1166
+ if (isDesktopHost()) {
1167
+ sendJson(response, 200, { ok: false, desktop: true });
1168
+ return;
1169
+ }
1136
1170
  // 当前宿主监听端口来自请求 Host 头(localhost:7923),解析失败回退 7923
1137
1171
  const host = request.headers.host ?? '';
1138
1172
  const portMatch = host.match(/:(\d+)$/);
@@ -1,5 +1,10 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { readProfileArg, type LoaderHandle } from '../services/install/install.ts';
3
+ /**
4
+ * 宿主是否由桌面应用壳托管(`ELECTRON_RUN_AS_NODE=1`:壳用 Electron 二进制以 Node 模式跑宿主)。
5
+ * 桌面端的宿主归应用壳管,插件重启不了它,也不该去 kill —— 详见 /restart 路由。
6
+ */
7
+ export declare function isDesktopHost(): boolean;
3
8
  export interface WebRoute {
4
9
  kind: 'exact';
5
10
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 10000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):10000+ 人工精选社区插件,每日更新。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -311,16 +311,26 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
311
311
  if (!ok) setToast({ id: Date.now(), kind: 'revealFail' })
312
312
  }
313
313
 
314
- /** 「立即重启」:POST 同源 /restart,宿主进程自杀重启;随后轮询服务恢复后整页刷新。 */
314
+ /** 「立即重启」:POST 同源 /restart,宿主进程自杀重启;随后轮询服务恢复后整页刷新。
315
+ * 桌面端宿主归应用壳管(服务端回 `desktop: true` 且不重启):不轮询,改提示退出重开。 */
315
316
  const requestRestart = async () => {
316
317
  if (restarting) return
317
318
  setRestarting(true)
319
+ let desktop = false
318
320
  try {
319
321
  // 响应可能在宿主进程被 kill 前返回,也可能直接断连 —— 两种都属正常
320
- await fetch('/dsh-plugin-hub/restart', { method: 'POST' })
322
+ const res = await fetch('/dsh-plugin-hub/restart', { method: 'POST' })
323
+ const data = await res.json() as { desktop?: boolean }
324
+ desktop = data.desktop === true
321
325
  } catch {
322
326
  /* 服务已终止,无需处理 */
323
327
  }
328
+ if (desktop) {
329
+ // 宿主不会重启,轮询等不到新服务:直接给出正确口径
330
+ setRestarting(false)
331
+ setToast({ id: Date.now(), kind: 'restartDesktop' })
332
+ return
333
+ }
324
334
  // 轮询同源端点,服务恢复后整页 reload(让新挂载的 bundle 完全初始化)
325
335
  let attempts = 0
326
336
  const poll = async () => {
@@ -503,8 +503,9 @@ export function Toast({ toast, t }: { toast: ToastState; t: Translate }) {
503
503
  : toast.kind === 'fail' ? t('installFail')
504
504
  : toast.kind === 'removed' ? t('uninstallDone')
505
505
  : toast.kind === 'revealFail' ? t('openFolderFail')
506
- : t('uninstallFail')
507
- const fail = toast.kind === 'fail' || toast.kind === 'removeFail' || toast.kind === 'revealFail'
506
+ : toast.kind === 'restartDesktop' ? t('toastRestartDesktop')
507
+ : t('uninstallFail')
508
+ const fail = toast.kind === 'fail' || toast.kind === 'removeFail' || toast.kind === 'revealFail' || toast.kind === 'restartDesktop'
508
509
  return h('div', {
509
510
  key: toast.id,
510
511
  className: fail ? `${styles.toast} ${styles.toastFail}` : styles.toast,
@@ -268,6 +268,8 @@ export const zh = {
268
268
  restartLater: '稍后重启',
269
269
  restarting: '正在重启…',
270
270
  restartHint: '重启会中断正在进行的安装/卸载任务,其进度将丢失。部分插件(如插件市场、顶层 bundle)需重启后才会生效。',
271
+ // 桌面端宿主归应用壳管、插件重启不了它:点「立即重启」时改提示用户退出重开
272
+ toastRestartDesktop: '桌面端的宿主由应用管理,请在应用菜单(或托盘)选择退出后重新打开,重启后生效',
271
273
  done: '完成',
272
274
  // 待重启:装完没重启的插件常驻提醒,直到宿主真正重启
273
275
  restartPendingHint: '安装成功,重启后生效',
@@ -650,6 +652,8 @@ export const en = {
650
652
  restartLater: 'Later',
651
653
  restarting: 'Restarting…',
652
654
  restartHint: 'Restarting interrupts any in-progress install / uninstall task — its progress will be lost. Some plugins (such as plugin markets / top-level bundles) only take effect after a restart.',
655
+ // Desktop: the app shell owns the host, so plugins cannot restart it — tell the user to quit and reopen
656
+ toastRestartDesktop: 'In the desktop app the host is managed by the app: quit from the app menu (or tray) and open it again — the change takes effect after that restart',
653
657
  done: 'Done',
654
658
  restartPendingHint: 'Installed — takes effect after restart',
655
659
  restartPendingHintUninstall: 'Removed — cleaned up after restart',
@@ -77,7 +77,7 @@ export interface TaskState {
77
77
  lines: string[]
78
78
  }
79
79
 
80
- export type ToastKind = 'copied' | 'errCopied' | 'done' | 'fail' | 'removed' | 'removeFail' | 'revealFail'
80
+ export type ToastKind = 'copied' | 'errCopied' | 'done' | 'fail' | 'removed' | 'removeFail' | 'revealFail' | 'restartDesktop'
81
81
 
82
82
  export interface ToastState {
83
83
  id: number
@@ -28,6 +28,7 @@ import { appendLog, clearLog, readLog, logFilePath, defaultLogFilePath, customLo
28
28
  * 不用 `/bin/sh -c`:Windows 无 /bin/sh,且 lsof/nohup 仅 POSIX 存在。
29
29
  * 由 `spawn(process.execPath, ['-e', SCRIPT, port], { detached, stdio:'ignore' })` 孵化,
30
30
  * 宿主进程被 kill 后仍能完成「停旧 → 等端口释放 → 拉起新 dsh web」。
31
+ * 端口探测不到监听进程时不再假装重启(记一行日志后退出,见脚本内注释)。
31
32
  */
32
33
  const HOST_RESTART_SCRIPT = String.raw`const C = require('node:child_process')
33
34
  const F = require('node:fs')
@@ -54,7 +55,16 @@ function findPids() {
54
55
  return String(out.stdout || '').split(/\s+/).filter(Boolean)
55
56
  }
56
57
 
57
- for (const pid of findPids()) {
58
+ const targets = findPids()
59
+ // 一个监听进程都没找到 = 端口探测本身就失败了(netstat 输出格式 / 本地化差异),
60
+ // 旧宿主仍占着端口。此时再拉一个 dsh web 只会在日志里以 EADDRINUSE 静默退出,
61
+ // 用户却看到「重启完成」——不制造这种假象,留一行可诊断的记录后退出。
62
+ if (targets.length === 0) {
63
+ logLine('restart aborted: no listener found on port ' + port)
64
+ process.exit(0)
65
+ }
66
+
67
+ for (const pid of targets) {
58
68
  try {
59
69
  if (win) C.spawnSync('taskkill', ['/pid', pid, '/t', '/f'])
60
70
  else process.kill(Number(pid), 'SIGTERM')
@@ -67,6 +77,14 @@ let tries = 0
67
77
  setTimeout(waitForStop, 250)
68
78
  })()
69
79
 
80
+ function logLine(msg) {
81
+ try {
82
+ const dir = P.join(O.homedir(), '.dsh', 'logs')
83
+ F.mkdirSync(dir, { recursive: true })
84
+ F.appendFileSync(P.join(dir, 'dsh-web-' + port + '.log'), '[' + new Date().toISOString() + '] ' + msg + '\n')
85
+ } catch {}
86
+ }
87
+
70
88
  function startHost() {
71
89
  const dir = P.join(O.homedir(), '.dsh', 'logs')
72
90
  F.mkdirSync(dir, { recursive: true })
@@ -79,6 +97,14 @@ function startHost() {
79
97
  child.unref()
80
98
  }`
81
99
 
100
+ /**
101
+ * 宿主是否由桌面应用壳托管(`ELECTRON_RUN_AS_NODE=1`:壳用 Electron 二进制以 Node 模式跑宿主)。
102
+ * 桌面端的宿主归应用壳管,插件重启不了它,也不该去 kill —— 详见 /restart 路由。
103
+ */
104
+ export function isDesktopHost(): boolean {
105
+ return process.env.ELECTRON_RUN_AS_NODE === '1'
106
+ }
107
+
82
108
  export interface WebRoute {
83
109
  kind: 'exact'
84
110
  path: string
@@ -1109,6 +1135,15 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
1109
1135
  path: '/dsh-plugin-hub/restart',
1110
1136
  handler: (request, response) => {
1111
1137
  if (!requireTrustedPost(request, response)) return
1138
+ // 桌面端:宿主由应用壳托管,插件重启不了它。Windows 上 `taskkill /T` 会把发起
1139
+ // 重启的助手一起结束(助手是宿主的子进程),壳再把这次外部 kill 判成宿主崩溃
1140
+ // 弹「应用无法启动或已意外停止」;退一步说,脱离壳拉起的 `dsh web` 也服务不了
1141
+ // 桌面 UI(缺壳注入的 desktop:boot)。故不 kill、不拉新宿主,由客户端改提示
1142
+ // 用户从菜单/托盘退出后重新打开。
1143
+ if (isDesktopHost()) {
1144
+ sendJson(response, 200, { ok: false, desktop: true })
1145
+ return
1146
+ }
1112
1147
  // 当前宿主监听端口来自请求 Host 头(localhost:7923),解析失败回退 7923
1113
1148
  const host = request.headers.host ?? ''
1114
1149
  const portMatch = host.match(/:(\d+)$/)