dsh-pocket 1.0.12 → 1.0.14
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 +11 -12
- package/README.md +11 -12
- package/client/client.js +15 -1
- package/client/index.jsx +12 -1
- package/lib/index.js +28 -3
- package/lib/restart.js +53 -22
- package/lib/web-rpc.js +12 -5
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">DSH Pocket</h1>
|
|
6
6
|
|
|
7
|
+
<p align="center"><a href="README.en.md">English</a> | <a href="README.md">中文</a></p>
|
|
8
|
+
|
|
7
9
|
<p align="center">
|
|
8
10
|
<a href="https://www.npmjs.com/package/dsh-pocket"><img alt="npm" src="https://img.shields.io/npm/v/dsh-pocket?color=4d6bfe&label=npm"></a>
|
|
9
11
|
<a href="https://www.npmjs.com/package/dsh-pocket"><img alt="downloads" src="https://img.shields.io/npm/dm/dsh-pocket?color=4d6bfe"></a>
|
|
@@ -13,8 +15,6 @@
|
|
|
13
15
|
</p>
|
|
14
16
|
|
|
15
17
|
> Put **DeepSeek Harness in your pocket**: one package, one settings tab — scan a QR code and your phone shows exactly what's on your computer screen, live, from anywhere.
|
|
16
|
-
>
|
|
17
|
-
> [English](README.en.md) | [中文](README.md)
|
|
18
18
|
|
|
19
19
|
## What is this
|
|
20
20
|
|
|
@@ -26,8 +26,6 @@
|
|
|
26
26
|
|
|
27
27
|
That's what DSH Pocket does: **install it, scan a QR code, and your phone shows and controls the DeepSeek Harness UI in real time — from anywhere.**
|
|
28
28
|
|
|
29
|
-
> One-liner: `dsh web` only allows local access by default; DSH Pocket puts a proxy in front of it that safely bridges your phone (see [How it works](#-how-it-works)).
|
|
30
|
-
|
|
31
29
|
What it looks like — the phone shows the exact same UI as your computer, live:
|
|
32
30
|
|
|
33
31
|
<p align="center">
|
|
@@ -79,10 +77,6 @@ On the same page click "**Enable anywhere**" → wait for the tunnel (first run
|
|
|
79
77
|
|
|
80
78
|
> Upgrading: `dsh plugin --profile web update dsh-pocket --latest -w` (`--latest` is required across major versions — a `^0.x` range won't auto-jump to 1.x).
|
|
81
79
|
|
|
82
|
-
## 🧭 How it works
|
|
83
|
-
|
|
84
|
-
DSH's `/api` browser-trust fence only accepts loopback or `--trusted-host` authorities (and the official build refuses `0.0.0.0` binding to avoid exposing remote code execution to the network). Pocket rewrites every inbound request's `Host` / `Origin` to `127.0.0.1:<dsh-port>`, so the fence always sees loopback — **no dsh configuration changes needed**. The proxy auto-starts with the plugin; the public tunnel is on demand (cloudflared forced to HTTP/2 over TCP 443 to bypass blocked QUIC/UDP).
|
|
85
|
-
|
|
86
80
|
## ⚠️ Security (read first)
|
|
87
81
|
|
|
88
82
|
- **DSH can execute code on your computer.** The QR code / URL is the key — **never share it with anyone**.
|
|
@@ -101,6 +95,7 @@ DSH's `/api` browser-trust fence only accepts loopback or `--trusted-host` autho
|
|
|
101
95
|
| 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`) |
|
|
102
96
|
| No push on iOS Safari | Safari Web Push requires **"Add to Home Screen"** first, then open from the home-screen icon (Chrome/Android don't need this) |
|
|
103
97
|
| Public `error 1033` | See "Public tunnel troubleshooting" below — usually a local proxy/VPN (Clash etc. TUN mode) killing the tunnel |
|
|
98
|
+
| 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 with `lsof -ti :3080 \| xargs kill -9` (logs under `$DSH_HOME` as `dsh-pocket-restart-*.log`) |
|
|
104
99
|
|
|
105
100
|
## ⚠️ Public tunnel troubleshooting (read first)
|
|
106
101
|
|
|
@@ -110,17 +105,21 @@ DSH's `/api` browser-trust fence only accepts loopback or `--trusted-host` autho
|
|
|
110
105
|
Such tools take over all traffic and often cut cloudflared's tunnel-edge connections
|
|
111
106
|
(`*.argotunnel.com`, Cloudflare edge IPs), so the tunnel registers but the data plane never connects.
|
|
112
107
|
|
|
113
|
-
**Fix (
|
|
108
|
+
**Fix (try in order, lightest first)**:
|
|
114
109
|
|
|
115
|
-
1.
|
|
110
|
+
1. First **just turn off the proxy's TUN mode** — no need to quit the proxy; this is enough in most cases:
|
|
111
|
+
- Clash: turn off the "**TUN mode**" toggle in Settings (or right-click the menu-bar icon → uncheck TUN mode)
|
|
112
|
+
- Surge: turn off "**Enhanced mode**"; v2ray/sing-box: turn off "**virtual NIC / route takeover**"
|
|
113
|
+
- Then go back to the settings page and click "Enable anywhere" again
|
|
114
|
+
2. If that's not enough, temporarily **fully quit the proxy** (not just close the window: quit Clash from the menu-bar icon; if a
|
|
116
115
|
background service is installed, stop it in the service manager and confirm with `ps aux | grep clash`), then retry.
|
|
117
|
-
|
|
116
|
+
3. Add **DIRECT rules** to the proxy for the tunnel domains and Cloudflare edge (Clash example):
|
|
118
117
|
```yaml
|
|
119
118
|
- DOMAIN-SUFFIX,argotunnel.com,DIRECT
|
|
120
119
|
- DOMAIN-SUFFIX,trycloudflare.com,DIRECT
|
|
121
120
|
- IP-CIDR,198.41.192.0/24,DIRECT,no-resolve
|
|
122
121
|
```
|
|
123
|
-
|
|
122
|
+
4. If the network really can't reach the tunnel, use **LAN mode**: turn on the phone hotspot → connect the computer to it → scan the LAN QR. Same experience, from anywhere.
|
|
124
123
|
|
|
125
124
|
**Other causes**: corporate firewalls / campus networks blocking outbound — ask IT to allow it, or use a hotspot.
|
|
126
125
|
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">DSH Pocket</h1>
|
|
6
6
|
|
|
7
|
+
<p align="center"><a href="README.en.md">English</a> | <a href="README.md">中文</a></p>
|
|
8
|
+
|
|
7
9
|
<p align="center">
|
|
8
10
|
<a href="https://www.npmjs.com/package/dsh-pocket"><img alt="npm" src="https://img.shields.io/npm/v/dsh-pocket?color=4d6bfe&label=npm"></a>
|
|
9
11
|
<a href="https://www.npmjs.com/package/dsh-pocket"><img alt="downloads" src="https://img.shields.io/npm/dm/dsh-pocket?color=4d6bfe"></a>
|
|
@@ -13,8 +15,6 @@
|
|
|
13
15
|
</p>
|
|
14
16
|
|
|
15
17
|
> 把 **DeepSeek Harness 装进你的口袋**:一个包、一个设置页,手机扫二维码就实时看到电脑上的同一个界面——人在外面也能用。
|
|
16
|
-
>
|
|
17
|
-
> [English](README.en.md) | [中文](README.md)
|
|
18
18
|
|
|
19
19
|
## 这是什么
|
|
20
20
|
|
|
@@ -26,8 +26,6 @@
|
|
|
26
26
|
|
|
27
27
|
DSH Pocket 就是干这个的:**装上它,手机扫个码,就能实时看到并操控电脑上的 DeepSeek Harness 界面**——人在外面也能用。
|
|
28
28
|
|
|
29
|
-
> 原理一句话:`dsh web` 默认只允许本机访问,DSH Pocket 在它前面加一层代理,把手机访问安全地接入(详见[「原理」](#-原理))。
|
|
30
|
-
|
|
31
29
|
实际效果——手机上的界面就是电脑上的界面,实时同步:
|
|
32
30
|
|
|
33
31
|
<p align="center">
|
|
@@ -79,10 +77,6 @@ npx @deepseek-ai/dsh web
|
|
|
79
77
|
|
|
80
78
|
> 更新到新版本:`dsh plugin --profile web update dsh-pocket --latest -w`(跨大版本时 `--latest` 是必须的,`^0.x` 范围不会自动升到 1.x)。
|
|
81
79
|
|
|
82
|
-
## 🧭 原理
|
|
83
|
-
|
|
84
|
-
DSH 的 `/api` 浏览器信任栅栏只认 loopback 或 `--trusted-host`(官方还禁了 `0.0.0.0` 绑定,防止把远程执行代码暴露给网络)。Pocket 把入站请求的 `Host` / `Origin` 统一改写成 `127.0.0.1:<dsh端口>`,栅栏永远看到 loopback——**不需要改 dsh 的任何配置**。代理随插件自动启动,公网隧道按需开启(cloudflared 强制走 HTTP/2/TCP 443,绕开被网络屏蔽的 QUIC/UDP)。
|
|
85
|
-
|
|
86
80
|
## ⚠️ 安全(必读)
|
|
87
81
|
|
|
88
82
|
- **DSH 能执行你电脑上的代码**。二维码/URL 就是钥匙,**请勿把二维码或 URL 发给任何人**
|
|
@@ -101,6 +95,7 @@ DSH 的 `/api` 浏览器信任栅栏只认 loopback 或 `--trusted-host`(官
|
|
|
101
95
|
| 版本停在 0.x 升不上去 | `^0.x` 范围不允许升到 1.x:更新用 `--latest`(`dsh plugin --profile web update dsh-pocket --latest -w`) |
|
|
102
96
|
| 手机 iOS 收不到推送 | Safari 的 Web Push 要求**先把网页「添加到主屏幕」**,从主屏幕图标打开后才生效(Chrome/Android 无此要求) |
|
|
103
97
|
| 公网 `error 1033` | 见下方「公网隧道常见问题」——多半是本机代理/VPN(Clash 等 TUN 模式)掐断了隧道 |
|
|
98
|
+
| 点「重启 dsh web」后页面提示进程在后台运行 | 自重启的新进程是 detached 后台进程(不挂终端),是页内更新的标准做法;停止它:`lsof -ti :3080 \| xargs kill -9`(日志在 `$DSH_HOME` 下 `dsh-pocket-restart-*.log`) |
|
|
104
99
|
|
|
105
100
|
## ⚠️ 公网隧道常见问题(必读)
|
|
106
101
|
|
|
@@ -110,17 +105,21 @@ DSH 的 `/api` 浏览器信任栅栏只认 loopback 或 `--trusted-host`(官
|
|
|
110
105
|
这类工具会接管全部流量,并常常把 cloudflared 的隧道边缘连接
|
|
111
106
|
(`*.argotunnel.com`、Cloudflare 边缘 IP)掐断,导致隧道注册成功但数据面连不上。
|
|
112
107
|
|
|
113
|
-
|
|
108
|
+
**解决(从轻到重,按顺序试)**:
|
|
114
109
|
|
|
115
|
-
1.
|
|
110
|
+
1. 先**只关闭代理的 TUN 模式**,不用退出代理软件——多数情况这一步就够:
|
|
111
|
+
- Clash:设置里关掉「**TUN 模式**」开关(或右键菜单栏图标 → 取消勾选 TUN 模式)
|
|
112
|
+
- Surge:关「**增强模式**」;v2ray/sing-box:关「**虚拟网卡/路由接管**」
|
|
113
|
+
- 然后回设置页重新点「开启公网访问」
|
|
114
|
+
2. 仍不行就**彻底退出代理软件**(不只是关界面:Clash 要右键菜单栏图标 → 退出;若装有
|
|
116
115
|
后台服务还要在服务管理器里停掉,`ps aux | grep clash` 确认进程消失),再重试
|
|
117
|
-
|
|
116
|
+
3. 给代理加**直连规则**,放行隧道域名与 Cloudflare 边缘(Clash 规则示例):
|
|
118
117
|
```yaml
|
|
119
118
|
- DOMAIN-SUFFIX,argotunnel.com,DIRECT
|
|
120
119
|
- DOMAIN-SUFFIX,trycloudflare.com,DIRECT
|
|
121
120
|
- IP-CIDR,198.41.192.0/24,DIRECT,no-resolve
|
|
122
121
|
```
|
|
123
|
-
|
|
122
|
+
4. 网络实在不通时,改用**局域网模式**:手机开热点 → 电脑连手机热点 → 扫局域网码,
|
|
124
123
|
效果完全一样(人在外面也能用)
|
|
125
124
|
|
|
126
125
|
**其他可能**:企业防火墙/校园网拦截出站;此时请让 IT 放行或改用热点。
|
package/client/client.js
CHANGED
|
@@ -1306,6 +1306,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1306
1306
|
const [pushEnabled, setPushEnabled] = (0, import_react2.useState)(true);
|
|
1307
1307
|
const [pushState, setPushState] = (0, import_react2.useState)("checking");
|
|
1308
1308
|
const [tunnelState, setTunnelState] = (0, import_react2.useState)(null);
|
|
1309
|
+
const [restartNotice, setRestartNotice] = (0, import_react2.useState)(false);
|
|
1309
1310
|
const [updateInfo, setUpdateInfo] = (0, import_react2.useState)(null);
|
|
1310
1311
|
const call = async (endpoint, payload) => {
|
|
1311
1312
|
const res = await rpcCall(endpoint, payload);
|
|
@@ -1317,6 +1318,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1317
1318
|
const s = await call(POCKET_ENDPOINTS.status, {});
|
|
1318
1319
|
setStatus(s);
|
|
1319
1320
|
setTunnelState(s.tunnelState ?? null);
|
|
1321
|
+
if (s.restartNotice) setRestartNotice(true);
|
|
1320
1322
|
} catch {
|
|
1321
1323
|
}
|
|
1322
1324
|
};
|
|
@@ -1428,10 +1430,22 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1428
1430
|
"\u5F00\u53D1\u8005\uFF1A\u7A0B\u5E8F\u5458\u5C11\u5317\u6668"
|
|
1429
1431
|
)
|
|
1430
1432
|
),
|
|
1433
|
+
// 重启后提示(进程在后台运行,停止方法)
|
|
1434
|
+
restartNotice ? (0, import_react2.createElement)(
|
|
1435
|
+
"div",
|
|
1436
|
+
{ style: { ...styles.block, border: "1px solid var(--dsw-alias-brand-primary,#4f6ef7)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" } },
|
|
1437
|
+
(0, import_react2.createElement)(
|
|
1438
|
+
"div",
|
|
1439
|
+
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
|
|
1440
|
+
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F504} \u5DF2\u91CD\u542F | Restarted"),
|
|
1441
|
+
(0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, "\u77E5\u9053\u4E86 | OK")
|
|
1442
|
+
),
|
|
1443
|
+
(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\uFF1Alsof -ti :3080 | xargs kill -9")
|
|
1444
|
+
) : null,
|
|
1431
1445
|
// 更新提示
|
|
1432
1446
|
updateInfo ? (0, import_react2.createElement)(
|
|
1433
1447
|
"div",
|
|
1434
|
-
{ style: { ...styles.block, border: "1px solid var(--dsw-alias-state-warn-primary,#b45309)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" }
|
|
1448
|
+
{ style: { ...styles.block, border: "1px solid var(--dsw-alias-state-warn-primary,#b45309)" }, borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" },
|
|
1435
1449
|
(0, import_react2.createElement)(
|
|
1436
1450
|
"div",
|
|
1437
1451
|
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
|
package/client/index.jsx
CHANGED
|
@@ -61,6 +61,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
61
61
|
const [pushEnabled, setPushEnabled] = useState(true); // 宿主开关
|
|
62
62
|
const [pushState, setPushState] = useState('checking'); // checking|on|unsupported|insecure|off
|
|
63
63
|
const [tunnelState, setTunnelState] = useState(null); // 隧道进度 {phase, detail, startedAt}
|
|
64
|
+
const [restartNotice, setRestartNotice] = useState(false); // 重启后提示
|
|
64
65
|
const [updateInfo, setUpdateInfo] = useState(null); // { current, latest, updating, result } | null
|
|
65
66
|
|
|
66
67
|
const call = async (endpoint, payload) => {
|
|
@@ -74,6 +75,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
74
75
|
const s = await call(POCKET_ENDPOINTS.status, {});
|
|
75
76
|
setStatus(s);
|
|
76
77
|
setTunnelState(s.tunnelState ?? null);
|
|
78
|
+
if (s.restartNotice) setRestartNotice(true);
|
|
77
79
|
} catch { /* 忽略瞬时失败 */ }
|
|
78
80
|
};
|
|
79
81
|
|
|
@@ -186,8 +188,17 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
186
188
|
'开发者:程序员少北晨'),
|
|
187
189
|
),
|
|
188
190
|
|
|
191
|
+
// 重启后提示(进程在后台运行,停止方法)
|
|
192
|
+
restartNotice ? h('div', { style: { ...styles.block, border: '1px solid var(--dsw-alias-brand-primary,#4f6ef7)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' } },
|
|
193
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
|
|
194
|
+
h('div', { style: { fontWeight: 600, fontSize: 13 } }, '🔄 已重启 | Restarted'),
|
|
195
|
+
h('button', { style: styles.btn, onClick: () => setRestartNotice(false) }, '知道了 | OK'),
|
|
196
|
+
),
|
|
197
|
+
h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' }, '进程在后台运行(不挂终端)。如需停止:lsof -ti :3080 | xargs kill -9'),
|
|
198
|
+
) : null,
|
|
199
|
+
|
|
189
200
|
// 更新提示
|
|
190
|
-
updateInfo ? h('div', { style: { ...styles.block, border: '1px solid var(--dsw-alias-state-warn-primary,#b45309)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' }
|
|
201
|
+
updateInfo ? h('div', { style: { ...styles.block, border: '1px solid var(--dsw-alias-state-warn-primary,#b45309)' }, borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' },
|
|
191
202
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
|
|
192
203
|
h('div', { style: { fontWeight: 600, fontSize: 13 } }, `📦 新版本 v${updateInfo.latest} | Update available`),
|
|
193
204
|
updateInfo.result !== 'ok'
|
package/lib/index.js
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
// - 更新提示:有新版本时显示一键更新按钮(dsh plugin update --latest)
|
|
8
8
|
// 手机看到的界面 = 电脑上的 dsh web,实时同步(WebSocket 透传)。
|
|
9
9
|
|
|
10
|
-
import { readFile } from 'node:fs/promises';
|
|
11
10
|
import { fileURLToPath } from 'node:url';
|
|
12
11
|
import { spawn } from 'node:child_process';
|
|
13
12
|
import { createRequire } from 'node:module';
|
|
13
|
+
import { writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
14
16
|
|
|
15
17
|
import { createPocketService } from './service.mjs';
|
|
16
18
|
import { installPocketRpc } from './web-rpc.js';
|
|
@@ -30,6 +32,28 @@ function currentVersion() {
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
const restartNoticeRel = join('dsh-pocket', 'restarted.json');
|
|
36
|
+
function restartNoticePath() {
|
|
37
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), restartNoticeRel);
|
|
38
|
+
}
|
|
39
|
+
async function readRestartNotice() {
|
|
40
|
+
try {
|
|
41
|
+
const raw = JSON.parse(await readFile(restartNoticePath(), 'utf8'));
|
|
42
|
+
if (!raw?.at) return null;
|
|
43
|
+
if (Date.now() - raw.at > 30 * 60 * 1000) return null; // 30 分钟后过期
|
|
44
|
+
return raw;
|
|
45
|
+
} catch { return null; }
|
|
46
|
+
}
|
|
47
|
+
function writeRestartNotice() {
|
|
48
|
+
return mkdir(dirname(restartNoticePath()), { recursive: true })
|
|
49
|
+
.then(() => writeFile(restartNoticePath(), JSON.stringify({ at: Date.now(), pid: process.pid }), 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
/** 自重启(先落 notice,让重启后的页面提示停止方法)。 */
|
|
52
|
+
function pocketRestart() {
|
|
53
|
+
writeRestartNotice().catch(() => {});
|
|
54
|
+
return restartHost({ internals });
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
/** 执行更新:dsh plugin --profile <p> update dsh-pocket --latest -w(超时保护)。 */
|
|
34
58
|
function performUpdate(profile, { timeoutMs = 180_000 } = {}) {
|
|
35
59
|
return new Promise((resolve) => {
|
|
@@ -84,7 +108,8 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
84
108
|
service,
|
|
85
109
|
push: internals.push ?? { vapidPublicKey: () => '', count: () => 0, subscribe: async () => false, unsubscribe: async () => false, isEnabled: () => true, setEnabled: async () => true },
|
|
86
110
|
runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate },
|
|
87
|
-
restart: internals.restart ??
|
|
111
|
+
restart: internals.restart ?? pocketRestart,
|
|
112
|
+
restartNotice: internals.restartNotice ?? readRestartNotice,
|
|
88
113
|
log: logger,
|
|
89
114
|
});
|
|
90
115
|
disposers.push(disposeRpc);
|
|
@@ -137,4 +162,4 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
137
162
|
}, 'dsh-pocket: stop proxy, tunnel and push');
|
|
138
163
|
}
|
|
139
164
|
|
|
140
|
-
export { name, inject };
|
|
165
|
+
export { name, inject, readRestartNotice };
|
package/lib/restart.js
CHANGED
|
@@ -1,45 +1,76 @@
|
|
|
1
|
-
// dsh-pocket 自重启:重新拉起启动本宿主的确切 dsh
|
|
2
|
-
//
|
|
3
|
-
// self-restart 方案,见 https://github.com/dsh-market/dsh-market lib/restart.js)。
|
|
1
|
+
// dsh-pocket 自重启:重新拉起启动本宿主的确切 dsh 调用(detached 交接),
|
|
2
|
+
// 让更新后的插件代码生效——用户无需离开界面手动重启。
|
|
4
3
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// 方案借鉴 dshmarket 的 self-restart(lib/restart.js,MIT):不直接拉起新
|
|
5
|
+
// 进程,而是先拉一个 detached 的 node 辅助进程,等旧进程退出、端口释放
|
|
6
|
+
// (1.5s)后再拉起新 dsh,并把新进程输出写入临时日志——避免端口竞争
|
|
7
|
+
// (EADDRINUSE)导致新进程静默崩溃。
|
|
8
|
+
//
|
|
9
|
+
// 注意:新进程 detached,不挂终端——停止方式:lsof -ti :3080 | xargs kill -9。
|
|
8
10
|
|
|
9
11
|
import { spawn } from 'node:child_process';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
10
14
|
|
|
11
|
-
/**
|
|
12
|
-
* 重建启动调用(与当前宿主相同的命令)。
|
|
13
|
-
* @returns {{file:string, args:string[]}}
|
|
14
|
-
*/
|
|
15
|
+
/** 重建启动调用(与当前宿主相同的命令)。 */
|
|
15
16
|
export function restartLaunch() {
|
|
16
17
|
return {
|
|
17
18
|
file: process.argv[0], // node
|
|
18
19
|
args: [process.argv[1], ...process.argv.slice(2)], // <bin.js> + web [flags]
|
|
20
|
+
cwd: process.cwd(),
|
|
19
21
|
};
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/** 辅助进程代码:等待 1.5s(旧进程释放端口)→ 拉起新 dsh → 输出写日志。 */
|
|
25
|
+
function helperCode(launch, logOut, logErr) {
|
|
26
|
+
const spawn = "const { spawn } = require('node:child_process')";
|
|
27
|
+
const fs = "const fs = require('node:fs')";
|
|
28
|
+
const file = `const file = ${JSON.stringify(launch.file)}`;
|
|
29
|
+
const args = `const args = ${JSON.stringify(launch.args)}`;
|
|
30
|
+
const cwd = `const cwd = ${JSON.stringify(launch.cwd)}`;
|
|
31
|
+
const o = `const logOut = ${JSON.stringify(logOut)}`;
|
|
32
|
+
const e = `const logErr = ${JSON.stringify(logErr)}`;
|
|
33
|
+
const body = [
|
|
34
|
+
'setTimeout(() => {',
|
|
35
|
+
' try {',
|
|
36
|
+
' const out = fs.openSync(logOut, "a")',
|
|
37
|
+
' const err = fs.openSync(logErr, "a")',
|
|
38
|
+
' const child = spawn(file, args, { cwd, detached: true, stdio: ["ignore", out, err], env: process.env })',
|
|
39
|
+
' child.unref()',
|
|
40
|
+
' } catch {}',
|
|
41
|
+
'}, 1500)',
|
|
42
|
+
].join('\n');
|
|
43
|
+
return [spawn, fs, file, args, cwd, o, e, body].join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
22
46
|
/**
|
|
23
|
-
* 拉起替代宿主(detached
|
|
47
|
+
* 拉起替代宿主(detached 辅助进程交接),随后结束当前进程。
|
|
24
48
|
* @param {object} opts
|
|
25
|
-
* @param {number} [opts.
|
|
26
|
-
* @param {object} [opts.internals] 测试注入:
|
|
27
|
-
* @returns {{
|
|
49
|
+
* @param {number} [opts.handoffMs] 等待旧进程释放端口的时长(默认 1500ms)
|
|
50
|
+
* @param {object} [opts.internals] 测试注入:spawn / kill
|
|
51
|
+
* @returns {{helperPid:number|null, logOut:string, logErr:string}}
|
|
28
52
|
*/
|
|
29
|
-
export function restartHost({
|
|
53
|
+
export function restartHost({ handoffMs = 1500, internals = {} } = {}) {
|
|
30
54
|
const spawnFn = internals.spawn ?? spawn;
|
|
31
|
-
const
|
|
32
|
-
const
|
|
55
|
+
const killFn = internals.kill ?? ((pid) => process.kill(pid, 'SIGTERM'));
|
|
56
|
+
const launch = restartLaunch();
|
|
57
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
58
|
+
const logOut = join(tmpdir(), `dsh-pocket-restart-${stamp}.out.log`);
|
|
59
|
+
const logErr = join(tmpdir(), `dsh-pocket-restart-${stamp}.err.log`);
|
|
60
|
+
|
|
61
|
+
let helperPid = null;
|
|
33
62
|
try {
|
|
34
|
-
const
|
|
63
|
+
const helper = spawnFn(process.execPath, ['-e', helperCode(launch, logOut, logErr)], {
|
|
35
64
|
detached: true,
|
|
36
65
|
stdio: 'ignore',
|
|
37
66
|
env: process.env,
|
|
38
67
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
helper.unref?.();
|
|
69
|
+
helperPid = helper.pid ?? null;
|
|
70
|
+
// 短暂等待后结束当前进程(释放端口);由辅助进程在 1.5s 后拉起新宿主
|
|
71
|
+
setTimeout(() => { try { killFn(process.pid); } catch { /* 忽略 */ } }, 500);
|
|
42
72
|
} catch (err) {
|
|
43
|
-
return {
|
|
73
|
+
return { helperPid: null, logOut, logErr, error: err?.message ?? String(err) };
|
|
44
74
|
}
|
|
75
|
+
return { helperPid, logOut, logErr };
|
|
45
76
|
}
|
package/lib/web-rpc.js
CHANGED
|
@@ -17,7 +17,7 @@ function fail(code, message) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** 注册 /dsh-pocket 逻辑通道(仅本机 loopback 可调)。 */
|
|
20
|
-
export function installPocketRpc(ctx, { service, push, log = console, runUpdate = null, restart = null }) {
|
|
20
|
+
export function installPocketRpc(ctx, { service, push, log = console, runUpdate = null, restart = null, restartNotice = null }) {
|
|
21
21
|
if (!ctx?.connection?.rpc?.handle) {
|
|
22
22
|
log.warn?.('dsh-pocket: DSH Host Connection RPC unavailable — settings tab disabled | 无 Connection RPC,设置页不可用');
|
|
23
23
|
return () => {};
|
|
@@ -25,17 +25,24 @@ export function installPocketRpc(ctx, { service, push, log = console, runUpdate
|
|
|
25
25
|
return ctx.connection.rpc.handle(POCKET_RPC_CHANNEL, async (endpoint, payload = {}, signal) => {
|
|
26
26
|
if (signal?.aborted) return fail('cancelled', 'The request was cancelled.');
|
|
27
27
|
|
|
28
|
+
// status 响应:redact 后的服务状态 + 重启提示(重启后 30 分钟内有效)
|
|
29
|
+
const statusPayload = async () => {
|
|
30
|
+
let notice = null;
|
|
31
|
+
try { notice = (await restartNotice?.()) ?? null; } catch { notice = null; }
|
|
32
|
+
return ok({ ...redactStatus(await service.status()), restartNotice: notice });
|
|
33
|
+
};
|
|
34
|
+
|
|
28
35
|
try {
|
|
29
36
|
if (endpoint === POCKET_ENDPOINTS.status) {
|
|
30
|
-
return
|
|
37
|
+
return await statusPayload();
|
|
31
38
|
}
|
|
32
39
|
if (endpoint === POCKET_ENDPOINTS.tunnelStart) {
|
|
33
40
|
await service.startTunnel();
|
|
34
|
-
return
|
|
41
|
+
return await statusPayload();
|
|
35
42
|
}
|
|
36
43
|
if (endpoint === POCKET_ENDPOINTS.tunnelStop) {
|
|
37
44
|
service.stopTunnel();
|
|
38
|
-
return
|
|
45
|
+
return await statusPayload();
|
|
39
46
|
}
|
|
40
47
|
if (endpoint === POCKET_ENDPOINTS.pushVapidKey) {
|
|
41
48
|
return ok({ publicKey: push.vapidPublicKey() });
|
|
@@ -66,7 +73,7 @@ export function installPocketRpc(ctx, { service, push, log = console, runUpdate
|
|
|
66
73
|
if (endpoint === POCKET_ENDPOINTS.restart) {
|
|
67
74
|
if (!restart) return fail('bad-request', '重启不可用 | restart unavailable');
|
|
68
75
|
const result = restart();
|
|
69
|
-
return ok(result);
|
|
76
|
+
return ok({ ...result, hint: '重启后进程在后台运行;如需停止:lsof -ti :3080 | xargs kill -9' });
|
|
70
77
|
}
|
|
71
78
|
return fail('bad-request', `Unknown endpoint: ${endpoint}`);
|
|
72
79
|
} catch (err) {
|
package/package.json
CHANGED