dsh-pocket 1.0.27 → 1.0.29
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.en.md +10 -2
- package/README.md +10 -2
- package/client/client.js +1 -1
- package/client/index.jsx +1 -1
- package/lib/tunnel.mjs +141 -22
- package/lib/web-rpc.js +12 -3
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -90,10 +90,10 @@ On the same page click "**Enable anywhere**" → wait for the tunnel (first run
|
|
|
90
90
|
| `dsh: command not found` / "DSH is not defined" | dsh CLI missing: `npm install -g @deepseek-ai/dsh`, or prefix commands with `npx @deepseek-ai/dsh` |
|
|
91
91
|
| `ERR_PNPM_ADDING_TO_ROOT` | pnpm 9 workspace-root restriction: append `-w` (`--workspace-root`) to install/update commands |
|
|
92
92
|
| Nothing changed after install/update | **You must restart `dsh web`**; the running process still loads the old code |
|
|
93
|
-
| `listen EADDRINUSE ... :3081` | A stale dsh-pocket process holds the port: `lsof -ti :3081 \| xargs kill -9`, then retry |
|
|
93
|
+
| `listen EADDRINUSE ... :3081` | A stale dsh-pocket process holds the port: macOS/Linux `lsof -ti :3081 \| xargs kill -9`; Windows `netstat -ano \| findstr :3081` (find the LISTENING PID) → `taskkill /PID <PID> /F`, then retry |
|
|
94
94
|
| Version stuck below 1.x | `^0.x` ranges never jump to 1.x: update with `--latest` (`dsh plugin --profile web update dsh-pocket --latest -w`) |
|
|
95
95
|
| Public `error 1033` | See "Public tunnel troubleshooting" below — usually a local proxy/VPN (Clash etc. TUN mode) killing the tunnel |
|
|
96
|
-
| After "Restart dsh web", the page says the process is running in the background | The new process from in-page self-restart is a detached background process (not attached to your terminal) — that's the standard way to apply updates in-page; stop it
|
|
96
|
+
| After "Restart dsh web", the page says the process is running in the background | The new process from in-page self-restart is a detached background process (not attached to your terminal) — that's the standard way to apply updates in-page; stop it: macOS/Linux `lsof -ti :3080 \| xargs kill -9`; Windows `netstat -ano \| findstr :3080` → `taskkill /PID <PID> /F` (logs under `$DSH_HOME` as `dsh-pocket-restart-*.log`) |
|
|
97
97
|
|
|
98
98
|
## ⚠️ Public tunnel troubleshooting (read first)
|
|
99
99
|
|
|
@@ -121,6 +121,14 @@ Such tools take over all traffic and often cut cloudflared's tunnel-edge connect
|
|
|
121
121
|
|
|
122
122
|
**Other causes**: corporate firewalls / campus networks blocking outbound — ask IT to allow it, or use a hotspot.
|
|
123
123
|
|
|
124
|
+
**First run: "Downloading cloudflared" fails or hangs**: cloudflared (~20MB) downloads from GitHub releases, which is often unreachable from mainland China. The plugin tries several GitHub acceleration mirrors in order; if all fail, the settings page shows a hint. Alternatives (any one):
|
|
125
|
+
1. Install the `cloudflared` command and retry (the plugin then uses the PATH binary, no download):
|
|
126
|
+
- macOS: `brew install cloudflared`; Linux: `sudo apt install cloudflared` or from the official site
|
|
127
|
+
- Windows: `winget install cloudflared` or from the official site
|
|
128
|
+
- Any platform: `npm i -g cloudflared`
|
|
129
|
+
2. Enable a proxy (system proxy / Clash etc.) and click "Enable anywhere" again
|
|
130
|
+
3. Manually download the binary into `$DSH_HOME/dsh-pocket/bin/` (`$DSH_HOME` is usually `~/.dsh`, on Windows `%USERPROFILE%\.dsh`)
|
|
131
|
+
|
|
124
132
|
## 🗂 Architecture (single package)
|
|
125
133
|
|
|
126
134
|
| File | Purpose |
|
package/README.md
CHANGED
|
@@ -90,10 +90,10 @@ npx @deepseek-ai/dsh web
|
|
|
90
90
|
| `dsh: command not found` / 提示 DSH 未定义 | dsh CLI 没装:`npm install -g @deepseek-ai/dsh`,或命令前加 `npx @deepseek-ai/dsh` |
|
|
91
91
|
| `ERR_PNPM_ADDING_TO_ROOT` | pnpm 9 对 workspace 根的限制:安装/更新命令**末尾加 `-w`**(`--workspace-root`) |
|
|
92
92
|
| 装完/更新了但界面没变化 | **必须重启 `dsh web`** 才生效;运行中的进程仍加载旧代码 |
|
|
93
|
-
| `listen EADDRINUSE ... :3081` | 旧 dsh-pocket
|
|
93
|
+
| `listen EADDRINUSE ... :3081` | 旧 dsh-pocket 进程还占着端口:macOS/Linux `lsof -ti :3081 \| xargs kill -9`;Windows `netstat -ano \| findstr :3081`(找 LISTENING 的 PID)→ `taskkill /PID <PID> /F`,后重试 |
|
|
94
94
|
| 版本停在 0.x 升不上去 | `^0.x` 范围不允许升到 1.x:更新用 `--latest`(`dsh plugin --profile web update dsh-pocket --latest -w`) |
|
|
95
95
|
| 公网 `error 1033` | 见下方「公网隧道常见问题」——多半是本机代理/VPN(Clash 等 TUN 模式)掐断了隧道 |
|
|
96
|
-
| 点「重启 dsh web」后页面提示进程在后台运行 | 自重启的新进程是 detached
|
|
96
|
+
| 点「重启 dsh web」后页面提示进程在后台运行 | 自重启的新进程是 detached 后台进程(不挂终端),是页内更新的标准做法;停止它:macOS/Linux `lsof -ti :3080 \| xargs kill -9`;Windows `netstat -ano \| findstr :3080` → `taskkill /PID <PID> /F`(日志在 `$DSH_HOME` 下 `dsh-pocket-restart-*.log`) |
|
|
97
97
|
|
|
98
98
|
## ⚠️ 公网隧道常见问题(必读)
|
|
99
99
|
|
|
@@ -122,6 +122,14 @@ npx @deepseek-ai/dsh web
|
|
|
122
122
|
|
|
123
123
|
**其他可能**:企业防火墙/校园网拦截出站;此时请让 IT 放行或改用热点。
|
|
124
124
|
|
|
125
|
+
**首次开启时「下载 cloudflared」失败/卡住**:cloudflared 从 GitHub 发布页下载(约 20MB),国内直连经常失败。插件会自动依次尝试多个 GitHub 加速镜像;全部失败时设置页会给出提示。备选方案(任选其一):
|
|
126
|
+
1. 手动装好命令行 cloudflared 后重试(装好后 dsh-pocket 直接用 PATH 里的,不再下载):
|
|
127
|
+
- macOS:`brew install cloudflared`;Linux:`sudo apt install cloudflared` 或官网下载
|
|
128
|
+
- Windows:`winget install cloudflared` 或官网下载
|
|
129
|
+
- 任何平台:`npm i -g cloudflared`
|
|
130
|
+
2. 挂代理(系统代理/Clash 等)后重新点「开启公网访问」
|
|
131
|
+
3. 手动下载二进制放到 `$DSH_HOME/dsh-pocket/bin/` 目录(`$DSH_HOME` 一般是 `~/.dsh`,Windows 是 `%USERPROFILE%\.dsh`)
|
|
132
|
+
|
|
125
133
|
## 🗂 架构(单包)
|
|
126
134
|
|
|
127
135
|
| 文件 | 说明 |
|
package/client/client.js
CHANGED
|
@@ -1439,7 +1439,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1439
1439
|
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F504} \u5DF2\u91CD\u542F | Restarted"),
|
|
1440
1440
|
(0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, "\u77E5\u9053\u4E86 | OK")
|
|
1441
1441
|
),
|
|
1442
|
-
(0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" }, `\u8FDB\u7A0B\u5728\u540E\u53F0\u8FD0\u884C\uFF08\u4E0D\u6302\u7EC8\u7AEF\uFF09\u3002\u5982\u9700\u505C\u6B62\
|
|
1442
|
+
(0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" }, `\u8FDB\u7A0B\u5728\u540E\u53F0\u8FD0\u884C\uFF08\u4E0D\u6302\u7EC8\u7AEF\uFF09\u3002\u5982\u9700\u505C\u6B62\uFF1A${status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9`}`)
|
|
1443
1443
|
) : null,
|
|
1444
1444
|
// 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
|
|
1445
1445
|
updateInfo ? (0, import_react2.createElement)(
|
package/client/index.jsx
CHANGED
|
@@ -168,7 +168,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
168
168
|
h('div', { style: { fontWeight: 600, fontSize: 13 } }, '🔄 已重启 | Restarted'),
|
|
169
169
|
h('button', { style: styles.btn, onClick: () => setRestartNotice(false) }, '知道了 | OK'),
|
|
170
170
|
),
|
|
171
|
-
h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' },
|
|
171
|
+
h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' }, `进程在后台运行(不挂终端)。如需停止:${status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9`}`),
|
|
172
172
|
) : null,
|
|
173
173
|
|
|
174
174
|
// 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
|
package/lib/tunnel.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 无密码模式:URL 即钥匙(dsh web 能执行代码,请勿把二维码/URL 发给别人)。
|
|
5
5
|
|
|
6
6
|
import { spawn, execSync } from 'node:child_process';
|
|
7
|
-
import { mkdir, access, chmod, rm } from 'node:fs/promises';
|
|
7
|
+
import { mkdir, access, chmod, rm, stat, rename, cp } from 'node:fs/promises';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { join, dirname } from 'node:path';
|
|
10
10
|
import { pipeline } from 'node:stream/promises';
|
|
@@ -20,30 +20,147 @@ function platformBinary() {
|
|
|
20
20
|
return { os, a, ext: os === 'windows' ? '.exe' : '' };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* cloudflared 下载源。
|
|
25
|
+
* 优先:清华 TUNA 镜像的 Homebrew bottle(国内 CDN,实测 ~3MB/s)——仅 macOS/Linux
|
|
26
|
+
* 且有对应 bottle 时可用(Windows 无 Homebrew,自动跳过)。
|
|
27
|
+
* 兜底:官方 GitHub + 国内加速源(ghproxy.net / gh.ddlc.top / gh-proxy.com,2026-08
|
|
28
|
+
* 实测可达)。npmmirror(淘宝)没有 cloudflared 镜像(已实测 404)。
|
|
29
|
+
*/
|
|
30
|
+
const CLOUDFLARED_MIRRORS = [
|
|
31
|
+
(asset) => `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
|
|
32
|
+
(asset) => `https://ghproxy.net/https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
|
|
33
|
+
(asset) => `https://gh.ddlc.top/https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
|
|
34
|
+
(asset) => `https://gh-proxy.com/https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const TUNA_BOTTLES = 'https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/';
|
|
38
|
+
|
|
39
|
+
function hostOf(url) {
|
|
40
|
+
try { return new URL(url).host; } catch { return url; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 清华 TUNA 镜像的 cloudflared Homebrew bottle URL(国内 CDN,实测 ~3MB/s)。
|
|
45
|
+
* 只覆盖 macOS/Linux(Windows 无 Homebrew bottle,返回 null)。
|
|
46
|
+
* 匹配按 CPU 架构取清华目录里版本号最新的 bottle——Homebrew 构建时部署目标
|
|
47
|
+
* 设得较老、向后兼容,所以旧系统(如 Ventura)也能用新一点的 bottle。
|
|
48
|
+
* 抓目录失败/无匹配 → null(调用方回退 GitHub/加速源,不影响可用性)。
|
|
49
|
+
*/
|
|
50
|
+
async function tsinghuaBottleUrl({ os, a }) {
|
|
51
|
+
if (os !== 'darwin' && os !== 'linux') return null;
|
|
52
|
+
let res;
|
|
53
|
+
try {
|
|
54
|
+
res = await fetch(TUNA_BOTTLES, { signal: AbortSignal.timeout(20_000) });
|
|
55
|
+
} catch { return null; }
|
|
56
|
+
if (!res.ok) return null;
|
|
57
|
+
let html;
|
|
58
|
+
try { html = await res.text(); } catch { return null; }
|
|
59
|
+
// macOS: arm64_<代号> 或 <代号>(Intel 无前缀),代号白名单排除 linux;Linux: arm64_linux / x86_64_linux
|
|
60
|
+
const MACOS_CODES = 'monterey|ventura|sonoma|sequoia|tahoe';
|
|
61
|
+
const pattern = os === 'darwin'
|
|
62
|
+
? new RegExp(`cloudflared-([0-9.]+)\\.${a === 'arm64' ? 'arm64_' : ''}(${MACOS_CODES})\\.bottle\\.tar\\.gz`, 'g')
|
|
63
|
+
: new RegExp(`cloudflared-([0-9.]+)\\.${a === 'arm64' ? 'arm64' : 'x86_64'}_linux\\.bottle\\.tar\\.gz`, 'g');
|
|
64
|
+
let best = null;
|
|
65
|
+
let bestV = '';
|
|
66
|
+
for (const m of html.matchAll(pattern)) {
|
|
67
|
+
if (m[1] > bestV) { bestV = m[1]; best = m[0]; }
|
|
68
|
+
}
|
|
69
|
+
return best ? `${TUNA_BOTTLES}${best}` : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
23
72
|
async function downloadCloudflared(binPath, signal) {
|
|
24
73
|
const { os, a, ext } = platformBinary();
|
|
25
|
-
// cloudflared 新版发布资产是 .tgz 压缩包(内含 cloudflared 二进制)
|
|
26
|
-
const url = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-${os}-${a}.tgz`;
|
|
27
|
-
console.log(`⬇️ 正在下载 cloudflared(首次运行,之后跳过)…`);
|
|
28
|
-
// 同时响应外部 abort(用户点停止)与 180s 兜底超时
|
|
29
|
-
const fetchSignal = signal
|
|
30
|
-
? AbortSignal.any([signal, AbortSignal.timeout(180_000)])
|
|
31
|
-
: AbortSignal.timeout(180_000);
|
|
32
|
-
const res = await fetch(url, { signal: fetchSignal });
|
|
33
|
-
if (!res.ok) throw new Error(`cloudflared 下载失败(HTTP ${res.status})`);
|
|
34
74
|
const dir = dirname(binPath);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
const tmpFile = join(dir, `cloudflared.download`);
|
|
76
|
+
const isWindows = os === 'windows';
|
|
77
|
+
// 发布资产:Windows 是 .exe(下载即二进制),macOS/Linux 是 .tgz(需解压)
|
|
78
|
+
const asset = isWindows ? `cloudflared-windows-${a}.exe` : `cloudflared-${os}-${a}.tgz`;
|
|
79
|
+
const fetchSignal = signal
|
|
80
|
+
? AbortSignal.any([signal, AbortSignal.timeout(120_000)])
|
|
81
|
+
: AbortSignal.timeout(120_000);
|
|
82
|
+
|
|
83
|
+
// 构建有序源列表:[{url, host}];清华(如有,仅 macOS/Linux)排第一,再官方 + 加速源
|
|
84
|
+
const sources = [];
|
|
85
|
+
if (!isWindows) {
|
|
86
|
+
const tua = await tsinghuaBottleUrl({ os, a }).catch(() => null);
|
|
87
|
+
if (tua) sources.push({ url: tua, host: 'mirrors.tuna.tsinghua.edu.cn' });
|
|
88
|
+
}
|
|
89
|
+
for (const m of CLOUDFLARED_MIRRORS) sources.push({ url: m(asset), host: hostOf(m(asset)) });
|
|
90
|
+
|
|
91
|
+
let lastErr = null;
|
|
92
|
+
for (let i = 0; i < sources.length; i++) {
|
|
93
|
+
const { url, host } = sources[i];
|
|
94
|
+
console.log(`⬇️ 下载 cloudflared(${i + 1}/${sources.length}:${host})…`);
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(url, { signal: fetchSignal });
|
|
97
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
98
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(tmpFile));
|
|
99
|
+
// 简单校验:空文件/极小文件视为下载失败(可能是镜像返回了错误页)
|
|
100
|
+
const st = await stat(tmpFile);
|
|
101
|
+
if (st.size < 1024 * 1024) throw new Error(`文件异常小(${st.size} 字节),疑似镜像错误页`);
|
|
102
|
+
lastErr = null;
|
|
103
|
+
break; // 下载成功
|
|
104
|
+
} catch (err) {
|
|
105
|
+
lastErr = err;
|
|
106
|
+
await rm(tmpFile, { force: true }).catch(() => {}); // 清掉半截文件
|
|
107
|
+
console.warn(` ⚠️ 源 ${i + 1} 失败:${err?.message ?? err},尝试下一个…`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (lastErr) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`cloudflared 下载失败:所有源都不通(最后错误:${lastErr?.message ?? lastErr})。`
|
|
113
|
+
+ (isWindows
|
|
114
|
+
? `Windows 可手动安装后重试:winget install cloudflared;或下载 ${asset} 放到 ${dir} 目录 | download failed — try: winget install cloudflared, or put the exe into ${dir}`
|
|
115
|
+
: `可手动安装后重试:npm i -g cloudflared(装好命令行 cloudflared 即可,无需下载);或开启代理/换网络后重试 | all mirrors failed — install cloudflared manually: npm i -g cloudflared, then retry`),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let extracted = join(dir, `cloudflared${ext}`);
|
|
120
|
+
if (isWindows) {
|
|
121
|
+
// Windows:exe 直接就是二进制,无需解压
|
|
122
|
+
await rename(tmpFile, extracted).catch(async () => {
|
|
123
|
+
await cp(tmpFile, extracted).catch(() => {});
|
|
124
|
+
});
|
|
125
|
+
} else {
|
|
126
|
+
// 解压到独立临时子目录(bottle 解压产物会占用 cacheDir/cloudflared 这个名字,
|
|
127
|
+
// 直接解压到 dir 会让目标路径变成目录,rename 失败)
|
|
128
|
+
const extractDir = join(dir, `.extract-${process.pid}-${Date.now()}`);
|
|
129
|
+
await mkdir(extractDir, { recursive: true });
|
|
130
|
+
try {
|
|
131
|
+
await new Promise((resolve, reject) => {
|
|
132
|
+
const child = spawn('tar', ['-xzf', tmpFile, '-C', extractDir], { stdio: 'ignore' });
|
|
133
|
+
child.once('exit', (code) => code === 0 ? resolve() : reject(new Error(`cloudflared 解压失败(code=${code})`)));
|
|
134
|
+
child.once('error', reject);
|
|
135
|
+
});
|
|
136
|
+
// 找真实的二进制**文件**(排除目录):
|
|
137
|
+
// - GitHub tgz:extractDir/cloudflared
|
|
138
|
+
// - Homebrew bottle(清华):extractDir/cloudflared/<版本>/bin/cloudflared
|
|
139
|
+
const { readdir } = await import('node:fs/promises');
|
|
140
|
+
let found = null;
|
|
141
|
+
const direct = join(extractDir, `cloudflared${ext}`);
|
|
142
|
+
try { if ((await stat(direct)).isFile()) found = direct; } catch { /* 不存在 */ }
|
|
143
|
+
if (!found) {
|
|
144
|
+
const verDir = join(extractDir, 'cloudflared');
|
|
145
|
+
try {
|
|
146
|
+
const vers = await readdir(verDir);
|
|
147
|
+
for (const v of vers) {
|
|
148
|
+
const bin = join(verDir, v, 'bin', `cloudflared${ext}`);
|
|
149
|
+
try { if ((await stat(bin)).isFile()) { found = bin; break; } } catch { /* 继续 */ }
|
|
150
|
+
}
|
|
151
|
+
} catch { /* 无此目录 */ }
|
|
152
|
+
}
|
|
153
|
+
if (!found) throw new Error('cloudflared 解压成功但未找到二进制 | binary not found after extract');
|
|
154
|
+
if (found !== extracted) {
|
|
155
|
+
await rename(found, extracted).catch(async () => { await cp(found, extracted).catch(() => {}); });
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
await rm(extractDir, { recursive: true, force: true }).catch(() => {});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!isWindows) await chmod(extracted, 0o755);
|
|
162
|
+
// 解压/搬移完成就删掉临时下载文件,避免长期占用缓存目录
|
|
163
|
+
await rm(tmpFile, { force: true }).catch(() => {});
|
|
47
164
|
return extracted;
|
|
48
165
|
}
|
|
49
166
|
|
|
@@ -65,6 +182,8 @@ let downloading = null;
|
|
|
65
182
|
* 优先:PATH 已有 → 直接用;否则用持久缓存($DSH_HOME/dsh-pocket/cloudflared),
|
|
66
183
|
* 只有缓存缺失才下载——避免每次开启公网都重新下 20MB。
|
|
67
184
|
*/
|
|
185
|
+
export { tsinghuaBottleUrl };
|
|
186
|
+
|
|
68
187
|
export async function resolveCloudflared({ home, onPhase = () => {}, signal } = {}) {
|
|
69
188
|
if (cloudflaredOnPath()) return 'cloudflared';
|
|
70
189
|
const dshHome = home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
package/lib/web-rpc.js
CHANGED
|
@@ -16,6 +16,14 @@ function fail(code, message) {
|
|
|
16
16
|
return { ok: false, error: { code: 'bad-request', message, details: { issues: [{ message }] } } };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** 各平台停止 dsh web 进程的命令(Windows 没有 lsof/kill)。 */
|
|
20
|
+
export function killHint(port) {
|
|
21
|
+
if (process.platform === 'win32') {
|
|
22
|
+
return `netstat -ano | findstr :${port}(找 LISTENING 的 PID)→ taskkill /PID <PID> /F`;
|
|
23
|
+
}
|
|
24
|
+
return `lsof -ti :${port} | xargs kill -9`;
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
/** 注册 /dsh-pocket 逻辑通道(仅本机 loopback 可调)。 */
|
|
20
28
|
export function installPocketRpc(ctx, { service, log = console, runUpdate = null, restart = null, restartNotice = null }) {
|
|
21
29
|
if (!ctx?.connection?.rpc?.handle) {
|
|
@@ -25,11 +33,12 @@ export function installPocketRpc(ctx, { service, log = console, runUpdate = null
|
|
|
25
33
|
return ctx.connection.rpc.handle(POCKET_RPC_CHANNEL, async (endpoint, payload = {}, signal) => {
|
|
26
34
|
if (signal?.aborted) return fail('cancelled', 'The request was cancelled.');
|
|
27
35
|
|
|
28
|
-
// status 响应:redact 后的服务状态 + 重启提示(重启后 30
|
|
36
|
+
// status 响应:redact 后的服务状态 + 重启提示(重启后 30 分钟内有效)+ 停止命令(按平台)
|
|
29
37
|
const statusPayload = async () => {
|
|
30
38
|
let notice = null;
|
|
31
39
|
try { notice = (await restartNotice?.()) ?? null; } catch { notice = null; }
|
|
32
|
-
|
|
40
|
+
const s = await service.status();
|
|
41
|
+
return ok({ ...redactStatus(s), restartNotice: notice, killHint: killHint(s.dshPort ?? 3080) });
|
|
33
42
|
};
|
|
34
43
|
|
|
35
44
|
try {
|
|
@@ -65,7 +74,7 @@ export function installPocketRpc(ctx, { service, log = console, runUpdate = null
|
|
|
65
74
|
return fail('bad-request', `重启失败:${result?.error ?? '未知'} | restart failed`);
|
|
66
75
|
}
|
|
67
76
|
const dshPort = service.dshPort ?? 3080;
|
|
68
|
-
return ok({ ...result, hint:
|
|
77
|
+
return ok({ ...result, hint: `重启后进程在后台运行;如需停止:${killHint(dshPort)}` });
|
|
69
78
|
}
|
|
70
79
|
return fail('bad-request', `Unknown endpoint: ${endpoint}`);
|
|
71
80
|
} catch (err) {
|
package/package.json
CHANGED