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.
- package/client/client.js +315 -303
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +35 -1
- package/lib/types/http/routes.d.ts +5 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +12 -2
- package/src/client/components/modals/modals.tsx +3 -2
- package/src/client/locales.ts +4 -0
- package/src/client/types.ts +1 -1
- package/src/server/http/routes.ts +36 -1
package/lib/http/routes.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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('
|
|
507
|
-
|
|
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,
|
package/src/client/locales.ts
CHANGED
|
@@ -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',
|
package/src/client/types.ts
CHANGED
|
@@ -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
|
-
|
|
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+)$/)
|