dsh-pocket 1.0.23 → 1.0.25
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 +49 -12
- package/client/index.jsx +41 -13
- package/lib/index.js +12 -6
- package/lib/service.mjs +16 -2
- package/lib/tunnel.mjs +33 -6
- package/lib/web-rpc.js +11 -1
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -55,7 +55,21 @@ function compareVersions(a, b) {
|
|
|
55
55
|
if (!aPre && !bPre) return 0;
|
|
56
56
|
if (!aPre) return 1;
|
|
57
57
|
if (!bPre) return -1;
|
|
58
|
-
|
|
58
|
+
const aParts = aPre.slice(1).split(".");
|
|
59
|
+
const bParts = bPre.slice(1).split(".");
|
|
60
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
61
|
+
for (let i = 0; i < len; i++) {
|
|
62
|
+
const ax = aParts[i] ?? "";
|
|
63
|
+
const bx = bParts[i] ?? "";
|
|
64
|
+
if (ax === bx) continue;
|
|
65
|
+
const aNum = /^\d+$/.test(ax);
|
|
66
|
+
const bNum = /^\d+$/.test(bx);
|
|
67
|
+
if (aNum && bNum) return Number(ax) - Number(bx);
|
|
68
|
+
if (aNum) return 1;
|
|
69
|
+
if (bNum) return -1;
|
|
70
|
+
return ax < bx ? -1 : 1;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
59
73
|
}
|
|
60
74
|
function redactStatus(s) {
|
|
61
75
|
return {
|
|
@@ -1287,7 +1301,10 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1287
1301
|
const s = await call(POCKET_ENDPOINTS.status, {});
|
|
1288
1302
|
setStatus(s);
|
|
1289
1303
|
setTunnelState(s.tunnelState ?? null);
|
|
1290
|
-
if (s.restartNotice)
|
|
1304
|
+
if (s.restartNotice) {
|
|
1305
|
+
setRestartNotice(true);
|
|
1306
|
+
setUpdateInfo(null);
|
|
1307
|
+
}
|
|
1291
1308
|
} catch {
|
|
1292
1309
|
}
|
|
1293
1310
|
};
|
|
@@ -1319,10 +1336,14 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1319
1336
|
const restartPocket = async () => {
|
|
1320
1337
|
setUpdateInfo((u) => ({ ...u, restarting: true }));
|
|
1321
1338
|
try {
|
|
1322
|
-
await
|
|
1339
|
+
await Promise.race([
|
|
1340
|
+
call(POCKET_ENDPOINTS.restart, {}),
|
|
1341
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("restart requested (no reply within 3s)")), 3e3))
|
|
1342
|
+
]);
|
|
1343
|
+
setUpdateInfo((u) => ({ ...u, restarting: true, result: "ok" }));
|
|
1323
1344
|
} catch (err) {
|
|
1324
1345
|
const msg = String(err?.message ?? "");
|
|
1325
|
-
if (/connection|socket|fetch|network|abort|cancelled|ECONN|disconnect|closed/i.test(msg)) {
|
|
1346
|
+
if (/connection|socket|fetch|network|abort|cancelled|ECONN|disconnect|closed|timeout/i.test(msg)) {
|
|
1326
1347
|
setUpdateInfo((u) => ({ ...u, restarting: true, result: "ok" }));
|
|
1327
1348
|
return;
|
|
1328
1349
|
}
|
|
@@ -1333,7 +1354,13 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1333
1354
|
setUpdateInfo((u) => ({ ...u, updating: true, result: null }));
|
|
1334
1355
|
try {
|
|
1335
1356
|
const r = await call(POCKET_ENDPOINTS.update, {});
|
|
1336
|
-
setUpdateInfo((u) => ({
|
|
1357
|
+
setUpdateInfo((u) => ({
|
|
1358
|
+
...u,
|
|
1359
|
+
updating: false,
|
|
1360
|
+
result: r.ok ? "ok" : "fail",
|
|
1361
|
+
autoRestart: r.autoRestart === true,
|
|
1362
|
+
output: r.output ?? r.error
|
|
1363
|
+
}));
|
|
1337
1364
|
} catch (err) {
|
|
1338
1365
|
setUpdateInfo((u) => ({ ...u, updating: false, result: "fail", output: err.message }));
|
|
1339
1366
|
}
|
|
@@ -1390,9 +1417,9 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1390
1417
|
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F504} \u5DF2\u91CD\u542F | Restarted"),
|
|
1391
1418
|
(0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, "\u77E5\u9053\u4E86 | OK")
|
|
1392
1419
|
),
|
|
1393
|
-
(0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" },
|
|
1420
|
+
(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 :${status?.dshPort ?? 3080} | xargs kill -9`)
|
|
1394
1421
|
) : null,
|
|
1395
|
-
//
|
|
1422
|
+
// 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
|
|
1396
1423
|
updateInfo ? (0, import_react2.createElement)(
|
|
1397
1424
|
"div",
|
|
1398
1425
|
{ style: { ...styles.block, borderLeft: "4px solid var(--dsw-alias-state-warn-primary,#b45309)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" } },
|
|
@@ -1402,14 +1429,14 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1402
1429
|
(0, import_react2.createElement)(
|
|
1403
1430
|
"div",
|
|
1404
1431
|
{ style: { fontWeight: 600, fontSize: 13 } },
|
|
1405
|
-
updateInfo.updated ? `\u2705 \u5DF2\u66F4\u65B0 v${updateInfo.current}\uFF0C\u91CD\u542F\u751F\u6548 | Updated \u2014 restart to apply` : `\u{1F4E6} \u65B0\u7248\u672C v${updateInfo.latest} | Update available`
|
|
1432
|
+
updateInfo.updated ? `\u2705 \u5DF2\u66F4\u65B0 v${updateInfo.current}\uFF0C\u91CD\u542F\u751F\u6548 | Updated \u2014 restart to apply` : updateInfo.result === "ok" ? updateInfo.autoRestart ? `\u2705 \u5DF2\u66F4\u65B0 v${updateInfo.latest}\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u542F\u2026 | updated \u2014 restarting\u2026` : `\u2705 \u5DF2\u66F4\u65B0 v${updateInfo.latest} | Updated` : `\u{1F4E6} \u65B0\u7248\u672C v${updateInfo.latest} | Update available`
|
|
1406
1433
|
),
|
|
1407
|
-
updateInfo.result !== "ok" ? (0, import_react2.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? "\u66F4\u65B0\u4E2D\u2026" : `\u66F4\u65B0\u5230 v${updateInfo.latest} | Update`) : (0, import_react2.createElement)("button", { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? "\u91CD\u542F\u4E2D\u2026" : "\u{1F504} \u91CD\u542F dsh web \u751F\u6548 | Restart now")
|
|
1434
|
+
updateInfo.result !== "ok" ? (0, import_react2.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? "\u66F4\u65B0\u4E2D\u2026" : `\u66F4\u65B0\u5230 v${updateInfo.latest} | Update`) : updateInfo.autoRestart ? (0, import_react2.createElement)("button", { style: styles.btn, disabled: true }, "\u6B63\u5728\u91CD\u542F\u751F\u6548\u2026 | restarting\u2026") : (0, import_react2.createElement)("button", { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? "\u91CD\u542F\u4E2D\u2026" : "\u{1F504} \u91CD\u542F dsh web \u751F\u6548 | Restart now")
|
|
1408
1435
|
),
|
|
1409
1436
|
(0, import_react2.createElement)(
|
|
1410
1437
|
"div",
|
|
1411
1438
|
{ style: styles.muted, marginTop: 4 },
|
|
1412
|
-
updateInfo.result === "ok" ? "\u2705 \u5DF2\u66F4\u65B0\uFF0C\u91CD\u542F dsh web \u751F\u6548 | updated \u2014 restart dsh web" : updateInfo.result === "fail" ? `\u274C \u5931\u8D25\uFF1A${updateInfo.output || "\u672A\u77E5"}\uFF08\u624B\u52A8\u66F4\u65B0\uFF1Adsh plugin --profile web update dsh-pocket --latest -w\uFF09` : `\u5F53\u524D v${updateInfo.current} \u2192 \u6700\u65B0 v${updateInfo.latest}`
|
|
1439
|
+
updateInfo.result === "ok" ? updateInfo.autoRestart ? "\u2705 \u5DF2\u66F4\u65B0\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u542F\u751F\u6548\uFF0C\u8BF7\u7A0D\u5019\u5237\u65B0 | updated \u2014 restarting automatically, refresh shortly" : "\u2705 \u5DF2\u66F4\u65B0\uFF0C\u91CD\u542F dsh web \u751F\u6548 | updated \u2014 restart dsh web" : updateInfo.result === "fail" ? `\u274C \u5931\u8D25\uFF1A${updateInfo.output || "\u672A\u77E5"}\uFF08\u624B\u52A8\u66F4\u65B0\uFF1Adsh plugin --profile web update dsh-pocket --latest -w\uFF09` : `\u5F53\u524D v${updateInfo.current} \u2192 \u6700\u65B0 v${updateInfo.latest}`
|
|
1413
1440
|
)
|
|
1414
1441
|
) : null,
|
|
1415
1442
|
// 局域网
|
|
@@ -1435,7 +1462,8 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1435
1462
|
null,
|
|
1436
1463
|
(0, import_react2.createElement)("img", { src: status.tunnelQr, alt: "Tunnel QR", style: styles.qr }),
|
|
1437
1464
|
(0, import_react2.createElement)("div", { style: styles.code }, tunnelUrl),
|
|
1438
|
-
(0, import_react2.createElement)("div", { style: styles.muted }, "\u4EFB\u4F55\u7F51\u7EDC\u626B\u7801\u5373\u7528\uFF08URL \u6BCF\u6B21\u91CD\u542F\
|
|
1465
|
+
(0, import_react2.createElement)("div", { style: styles.muted }, "\u4EFB\u4F55\u7F51\u7EDC\u626B\u7801\u5373\u7528\uFF08URL \u6BCF\u6B21\u91CD\u542F\u81EA\u52A8\u6362\u65B0\uFF09"),
|
|
1466
|
+
(0, import_react2.createElement)("div", { style: styles.warn, marginTop: 4 }, "\u{1F511} \u94FE\u63A5\u5DF2\u6CC4\u9732\uFF1F\u91CD\u542F dsh web\u2014\u2014URL \u7ACB\u5373\u6362\u65B0\uFF0C\u65E7\u94FE\u63A5\u4F5C\u5E9F\uFF0C\u65E0\u5B89\u5168\u98CE\u9669 | URL leaked? Restart dsh web \u2014 the URL rotates and the old one dies"),
|
|
1439
1467
|
(0, import_react2.createElement)("button", { style: styles.btn, onClick: stopTunnel }, "\u5173\u95ED\u516C\u7F51 | Stop")
|
|
1440
1468
|
) : (0, import_react2.createElement)(
|
|
1441
1469
|
"div",
|
|
@@ -1445,7 +1473,16 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1445
1473
|
"div",
|
|
1446
1474
|
{ style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
|
|
1447
1475
|
`\u23F3 ${tunnelStateDetail}\uFF08\u5DF2\u7B49\u5F85 ${Math.floor((Date.now() - (tunnelStateStarted || Date.now())) / 1e3)} \u79D2\uFF09\u2026`
|
|
1448
|
-
) : (0, import_react2.createElement)(
|
|
1476
|
+
) : tunnelPhase === "error" ? (0, import_react2.createElement)(
|
|
1477
|
+
"div",
|
|
1478
|
+
{ style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } },
|
|
1479
|
+
`\u274C \u5F00\u542F\u5931\u8D25\uFF1A${tunnelStateDetail || "\u672A\u77E5\u9519\u8BEF | failed"}\uFF08\u53EF\u91CD\u8BD5\uFF1B\u82E5\u662F\u4EE3\u7406/VPN \u95EE\u9898\u89C1 README \u6392\u969C\uFF09`
|
|
1480
|
+
) : (0, import_react2.createElement)(
|
|
1481
|
+
"div",
|
|
1482
|
+
null,
|
|
1483
|
+
(0, import_react2.createElement)("div", { style: styles.warn, marginTop: 8 }, "\u26A0\uFE0F DSH \u80FD\u6267\u884C\u7535\u8111\u4EE3\u7801\uFF1A\u4E8C\u7EF4\u7801/URL \u5C31\u662F\u94A5\u5319\uFF0C\u8BF7\u52FF\u53D1\u7ED9\u522B\u4EBA | the QR/URL is the key \u2014 never share it"),
|
|
1484
|
+
(0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4 }, "\u4E0D\u614E\u6CC4\u9732\u4E86\uFF1F\u91CD\u542F dsh web\uFF0CURL \u81EA\u52A8\u6362\u65B0\u3001\u65E7\u94FE\u63A5\u7ACB\u5373\u5931\u6548 | Leaked it? Restart dsh web \u2014 the URL rotates and the old one dies instantly")
|
|
1485
|
+
)
|
|
1449
1486
|
)
|
|
1450
1487
|
),
|
|
1451
1488
|
error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${error}`) : null,
|
package/client/index.jsx
CHANGED
|
@@ -45,7 +45,11 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
45
45
|
const s = await call(POCKET_ENDPOINTS.status, {});
|
|
46
46
|
setStatus(s);
|
|
47
47
|
setTunnelState(s.tunnelState ?? null);
|
|
48
|
-
if (s.restartNotice)
|
|
48
|
+
if (s.restartNotice) {
|
|
49
|
+
// 新进程确认起来了:显示一次「已重启」,并清掉旧的更新横幅(单状态,不并存)
|
|
50
|
+
setRestartNotice(true);
|
|
51
|
+
setUpdateInfo(null);
|
|
52
|
+
}
|
|
49
53
|
} catch { /* 忽略瞬时失败 */ }
|
|
50
54
|
};
|
|
51
55
|
|
|
@@ -80,11 +84,16 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
80
84
|
const restartPocket = async () => {
|
|
81
85
|
setUpdateInfo((u) => ({ ...u, restarting: true }));
|
|
82
86
|
try {
|
|
83
|
-
|
|
87
|
+
// 宿主 500ms 后自杀,RPC 响应可能来不及送达 → 3 秒超时兜底,别让按钮永远卡「重启中…」
|
|
88
|
+
await Promise.race([
|
|
89
|
+
call(POCKET_ENDPOINTS.restart, {}),
|
|
90
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('restart requested (no reply within 3s)')), 3000)),
|
|
91
|
+
]);
|
|
92
|
+
setUpdateInfo((u) => ({ ...u, restarting: true, result: 'ok' }));
|
|
84
93
|
} catch (err) {
|
|
85
|
-
//
|
|
94
|
+
// 网络断连/超时同样视为「已请求重启」——旧进程即将退出,等新进程起来后刷新即可
|
|
86
95
|
const msg = String(err?.message ?? '');
|
|
87
|
-
if (/connection|socket|fetch|network|abort|cancelled|ECONN|disconnect|closed/i.test(msg)) {
|
|
96
|
+
if (/connection|socket|fetch|network|abort|cancelled|ECONN|disconnect|closed|timeout/i.test(msg)) {
|
|
88
97
|
setUpdateInfo((u) => ({ ...u, restarting: true, result: 'ok' }));
|
|
89
98
|
return;
|
|
90
99
|
}
|
|
@@ -92,12 +101,18 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
92
101
|
}
|
|
93
102
|
};
|
|
94
103
|
|
|
95
|
-
// 一键更新:调宿主 dsh plugin update
|
|
104
|
+
// 一键更新:调宿主 dsh plugin update(成功后宿主自动重启生效,用户只点一次)
|
|
96
105
|
const runUpdate = async () => {
|
|
97
106
|
setUpdateInfo((u) => ({ ...u, updating: true, result: null }));
|
|
98
107
|
try {
|
|
99
108
|
const r = await call(POCKET_ENDPOINTS.update, {});
|
|
100
|
-
setUpdateInfo((u) => ({
|
|
109
|
+
setUpdateInfo((u) => ({
|
|
110
|
+
...u,
|
|
111
|
+
updating: false,
|
|
112
|
+
result: r.ok ? 'ok' : 'fail',
|
|
113
|
+
autoRestart: r.autoRestart === true,
|
|
114
|
+
output: r.output ?? r.error,
|
|
115
|
+
}));
|
|
101
116
|
} catch (err) {
|
|
102
117
|
setUpdateInfo((u) => ({ ...u, updating: false, result: 'fail', output: err.message }));
|
|
103
118
|
}
|
|
@@ -143,22 +158,28 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
143
158
|
h('div', { style: { fontWeight: 600, fontSize: 13 } }, '🔄 已重启 | Restarted'),
|
|
144
159
|
h('button', { style: styles.btn, onClick: () => setRestartNotice(false) }, '知道了 | OK'),
|
|
145
160
|
),
|
|
146
|
-
h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' },
|
|
161
|
+
h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' }, `进程在后台运行(不挂终端)。如需停止:lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9`),
|
|
147
162
|
) : null,
|
|
148
163
|
|
|
149
|
-
//
|
|
164
|
+
// 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
|
|
150
165
|
updateInfo ? h('div', { style: { ...styles.block, borderLeft: '4px solid var(--dsw-alias-state-warn-primary,#b45309)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' } },
|
|
151
166
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
|
|
152
167
|
h('div', { style: { fontWeight: 600, fontSize: 13 } },
|
|
153
168
|
updateInfo.updated
|
|
154
169
|
? `✅ 已更新 v${updateInfo.current},重启生效 | Updated — restart to apply`
|
|
155
|
-
:
|
|
170
|
+
: updateInfo.result === 'ok'
|
|
171
|
+
? (updateInfo.autoRestart ? `✅ 已更新 v${updateInfo.latest},正在自动重启… | updated — restarting…` : `✅ 已更新 v${updateInfo.latest} | Updated`)
|
|
172
|
+
: `📦 新版本 v${updateInfo.latest} | Update available`),
|
|
156
173
|
updateInfo.result !== 'ok'
|
|
157
174
|
? h('button', { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? '更新中…' : `更新到 v${updateInfo.latest} | Update`)
|
|
158
|
-
:
|
|
175
|
+
: updateInfo.autoRestart
|
|
176
|
+
? h('button', { style: styles.btn, disabled: true }, '正在重启生效… | restarting…')
|
|
177
|
+
: h('button', { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? '重启中…' : '🔄 重启 dsh web 生效 | Restart now'),
|
|
159
178
|
),
|
|
160
179
|
h('div', { style: styles.muted, marginTop: 4 },
|
|
161
|
-
updateInfo.result === 'ok'
|
|
180
|
+
updateInfo.result === 'ok'
|
|
181
|
+
? (updateInfo.autoRestart ? '✅ 已更新,正在自动重启生效,请稍候刷新 | updated — restarting automatically, refresh shortly'
|
|
182
|
+
: '✅ 已更新,重启 dsh web 生效 | updated — restart dsh web')
|
|
162
183
|
: updateInfo.result === 'fail' ? `❌ 失败:${updateInfo.output || '未知'}(手动更新:dsh plugin --profile web update dsh-pocket --latest -w)`
|
|
163
184
|
: `当前 v${updateInfo.current} → 最新 v${updateInfo.latest}`),
|
|
164
185
|
) : null,
|
|
@@ -182,7 +203,8 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
182
203
|
? h('div', null,
|
|
183
204
|
h('img', { src: status.tunnelQr, alt: 'Tunnel QR', style: styles.qr }),
|
|
184
205
|
h('div', { style: styles.code }, tunnelUrl),
|
|
185
|
-
h('div', { style: styles.muted }, '任何网络扫码即用(URL
|
|
206
|
+
h('div', { style: styles.muted }, '任何网络扫码即用(URL 每次重启自动换新)'),
|
|
207
|
+
h('div', { style: styles.warn, marginTop: 4 }, '🔑 链接已泄露?重启 dsh web——URL 立即换新,旧链接作废,无安全风险 | URL leaked? Restart dsh web — the URL rotates and the old one dies'),
|
|
186
208
|
h('button', { style: styles.btn, onClick: stopTunnel }, '关闭公网 | Stop'),
|
|
187
209
|
)
|
|
188
210
|
: h('div', null,
|
|
@@ -190,7 +212,13 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
190
212
|
tunnelStarting
|
|
191
213
|
? h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } },
|
|
192
214
|
`⏳ ${tunnelStateDetail}(已等待 ${Math.floor((Date.now() - (tunnelStateStarted || Date.now())) / 1000)} 秒)…`)
|
|
193
|
-
:
|
|
215
|
+
: tunnelPhase === 'error'
|
|
216
|
+
? h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' } },
|
|
217
|
+
`❌ 开启失败:${tunnelStateDetail || '未知错误 | failed'}(可重试;若是代理/VPN 问题见 README 排障)`)
|
|
218
|
+
: h('div', null,
|
|
219
|
+
h('div', { style: styles.warn, marginTop: 8 }, '⚠️ DSH 能执行电脑代码:二维码/URL 就是钥匙,请勿发给别人 | the QR/URL is the key — never share it'),
|
|
220
|
+
h('div', { style: styles.muted, marginTop: 4 }, '不慎泄露了?重启 dsh web,URL 自动换新、旧链接立即失效 | Leaked it? Restart dsh web — the URL rotates and the old one dies instantly'),
|
|
221
|
+
),
|
|
194
222
|
),
|
|
195
223
|
),
|
|
196
224
|
|
package/lib/index.js
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { spawn } from 'node:child_process';
|
|
14
|
-
import { createRequire } from 'node:module';
|
|
15
14
|
import { readFileSync } from 'node:fs';
|
|
16
15
|
import { writeFile, readFile, mkdir, rm } from 'node:fs/promises';
|
|
17
16
|
import { join, dirname } from 'node:path';
|
|
@@ -23,7 +22,6 @@ import { restartHost } from './restart.js';
|
|
|
23
22
|
|
|
24
23
|
const name = 'dsh-pocket';
|
|
25
24
|
const inject = ['connection', 'webServer'];
|
|
26
|
-
const require = createRequire(import.meta.url);
|
|
27
25
|
|
|
28
26
|
const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
29
27
|
|
|
@@ -69,10 +67,18 @@ async function consumeRestartNotice() {
|
|
|
69
67
|
}
|
|
70
68
|
return notice;
|
|
71
69
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
70
|
+
/**
|
|
71
|
+
* 自重启。
|
|
72
|
+
* 顺序很重要:先拉起 helper(失败就如实返回,不写标记、不停隧道)→ 停公网隧道
|
|
73
|
+
* (否则孤儿 cloudflared 让旧公网 URL 永活,与「重启即换 URL 作废」的宣传矛盾)→
|
|
74
|
+
* 写重启标记(新进程据此显示一次「已重启」横幅)。
|
|
75
|
+
*/
|
|
76
|
+
function pocketRestart(service) {
|
|
77
|
+
const result = restartHost();
|
|
78
|
+
if (!result || result.helperPid == null) return result; // helper 都没 spawn 出来 → 失败
|
|
79
|
+
try { service?.stopTunnel(); } catch { /* 忽略 */ }
|
|
74
80
|
writeRestartNotice().catch(() => {});
|
|
75
|
-
return
|
|
81
|
+
return result;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
/** 执行更新:dsh plugin --profile <p> update dsh-pocket --latest -w(超时保护)。 */
|
|
@@ -116,7 +122,7 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
116
122
|
const disposeRpc = installPocketRpc(ctx, {
|
|
117
123
|
service,
|
|
118
124
|
runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate, loadedVersion: () => loadedVersion },
|
|
119
|
-
restart: internals.restart ?? pocketRestart,
|
|
125
|
+
restart: internals.restart ?? (() => pocketRestart(service)),
|
|
120
126
|
restartNotice: internals.restartNotice ?? consumeRestartNotice,
|
|
121
127
|
log: logger,
|
|
122
128
|
});
|
package/lib/service.mjs
CHANGED
|
@@ -59,13 +59,18 @@ export function createPocketService({
|
|
|
59
59
|
async function qrCached(text) {
|
|
60
60
|
if (!text) return null;
|
|
61
61
|
if (!qrCache.has(text)) {
|
|
62
|
-
if (qrCache.size
|
|
62
|
+
if (qrCache.size >= 8) {
|
|
63
|
+
// 只淘汰最旧一条(隧道 URL 每次重启换新),别殃及稳定的 LAN 二维码
|
|
64
|
+
const oldest = qrCache.keys().next().value;
|
|
65
|
+
qrCache.delete(oldest);
|
|
66
|
+
}
|
|
63
67
|
qrCache.set(text, encodeQr(text).catch(() => null));
|
|
64
68
|
}
|
|
65
69
|
return qrCache.get(text);
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
return {
|
|
73
|
+
dshPort,
|
|
69
74
|
/** 启动局域网代理(幂等)。 */
|
|
70
75
|
async startProxy() {
|
|
71
76
|
if (proxy) return proxy;
|
|
@@ -98,6 +103,12 @@ export function createPocketService({
|
|
|
98
103
|
// 归一化:startTunnel 契约返回 {url, kill}(字符串也兼容)
|
|
99
104
|
tunnel = typeof result === 'string' ? { url: result, kill: () => {} } : result;
|
|
100
105
|
tunnelState.phase = 'ready';
|
|
106
|
+
// M1:隧道进程运行中死亡(崩溃/被杀)→ 状态打回,别让 UI 永远显示"可用"
|
|
107
|
+
tunnel.onExit?.((code) => {
|
|
108
|
+
if (controller.signal.aborted) return; // 主动停止(stopTunnel)不算故障
|
|
109
|
+
tunnelState.phase = 'error';
|
|
110
|
+
tunnelState.detail = `隧道进程退出(code=${code})| tunnel process exited`;
|
|
111
|
+
});
|
|
101
112
|
return tunnel.url;
|
|
102
113
|
} catch (err) {
|
|
103
114
|
// stopTunnel 触发的 abort 不算错误:保持 idle,别把状态刷成 error
|
|
@@ -108,9 +119,12 @@ export function createPocketService({
|
|
|
108
119
|
tunnelState.startedAt = null; // 失败后清掉计时,避免 UI 误显"启动中"
|
|
109
120
|
throw err;
|
|
110
121
|
} finally {
|
|
111
|
-
|
|
122
|
+
// 只清自己的引用:stopTunnel 后立即 startTunnel 可能已建了新的 in-flight
|
|
123
|
+
// (tunnelPromise=B),A 的 finally 不能把 B 清掉,否则第三次调用会并发 spawn
|
|
124
|
+
if (tunnelPromise === p) tunnelPromise = null;
|
|
112
125
|
}
|
|
113
126
|
})();
|
|
127
|
+
const p = tunnelPromise;
|
|
114
128
|
return tunnelPromise;
|
|
115
129
|
},
|
|
116
130
|
|
package/lib/tunnel.mjs
CHANGED
|
@@ -20,12 +20,16 @@ function platformBinary() {
|
|
|
20
20
|
return { os, a, ext: os === 'windows' ? '.exe' : '' };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
async function downloadCloudflared(binPath) {
|
|
23
|
+
async function downloadCloudflared(binPath, signal) {
|
|
24
24
|
const { os, a, ext } = platformBinary();
|
|
25
25
|
// cloudflared 新版发布资产是 .tgz 压缩包(内含 cloudflared 二进制)
|
|
26
26
|
const url = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-${os}-${a}.tgz`;
|
|
27
27
|
console.log(`⬇️ 正在下载 cloudflared(首次运行,之后跳过)…`);
|
|
28
|
-
|
|
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 });
|
|
29
33
|
if (!res.ok) throw new Error(`cloudflared 下载失败(HTTP ${res.status})`);
|
|
30
34
|
const dir = dirname(binPath);
|
|
31
35
|
const tgz = join(dir, `cloudflared.tgz`);
|
|
@@ -61,7 +65,7 @@ let downloading = null;
|
|
|
61
65
|
* 优先:PATH 已有 → 直接用;否则用持久缓存($DSH_HOME/dsh-pocket/cloudflared),
|
|
62
66
|
* 只有缓存缺失才下载——避免每次开启公网都重新下 20MB。
|
|
63
67
|
*/
|
|
64
|
-
export async function resolveCloudflared({ home, onPhase = () => {} } = {}) {
|
|
68
|
+
export async function resolveCloudflared({ home, onPhase = () => {}, signal } = {}) {
|
|
65
69
|
if (cloudflaredOnPath()) return 'cloudflared';
|
|
66
70
|
const dshHome = home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
67
71
|
const cacheDir = join(dshHome, 'dsh-pocket', 'bin');
|
|
@@ -73,7 +77,7 @@ export async function resolveCloudflared({ home, onPhase = () => {} } = {}) {
|
|
|
73
77
|
onPhase('downloading');
|
|
74
78
|
await mkdir(cacheDir, { recursive: true });
|
|
75
79
|
if (!downloading) {
|
|
76
|
-
downloading = downloadCloudflared(bin).finally(() => { downloading = null; });
|
|
80
|
+
downloading = downloadCloudflared(bin, signal).finally(() => { downloading = null; });
|
|
77
81
|
}
|
|
78
82
|
return downloading;
|
|
79
83
|
}
|
|
@@ -88,7 +92,7 @@ export async function resolveCloudflared({ home, onPhase = () => {} } = {}) {
|
|
|
88
92
|
* @returns {Promise<{url:string, kill:()=>void}>}
|
|
89
93
|
*/
|
|
90
94
|
export async function startQuickTunnel({ port, home, signal, onPhase = () => {} }) {
|
|
91
|
-
const bin = await resolveCloudflared({ home, onPhase });
|
|
95
|
+
const bin = await resolveCloudflared({ home, onPhase, signal });
|
|
92
96
|
onPhase('starting');
|
|
93
97
|
// 强制 HTTP/2(TCP 443)而不是默认的 QUIC(UDP 7844):
|
|
94
98
|
// 国内网络/部分企业网常屏蔽 UDP 7844,导致 tunnel 报 error 1033(Tunnel error);
|
|
@@ -96,8 +100,16 @@ export async function startQuickTunnel({ port, home, signal, onPhase = () => {}
|
|
|
96
100
|
const child = spawn(bin, ['tunnel', '--url', `http://127.0.0.1:${port}`, '--protocol', 'http2', '--no-autoupdate'], {
|
|
97
101
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
98
102
|
});
|
|
103
|
+
// H1:spawn 失败(缓存二进制损坏等)必须接住,否则 uncaughtException 崩宿主
|
|
104
|
+
child.on('error', (err) => {
|
|
105
|
+
cleanup?.();
|
|
106
|
+
onPhase?.('error');
|
|
107
|
+
rejectErr?.(new Error(`cloudflared 启动失败:${err?.message ?? err}(可删除 $DSH_HOME/dsh-pocket/bin 缓存后重试)`));
|
|
108
|
+
});
|
|
99
109
|
onPhase('registering');
|
|
100
110
|
|
|
111
|
+
let cleanup = null;
|
|
112
|
+
let rejectErr = null;
|
|
101
113
|
const url = await new Promise((resolve, reject) => {
|
|
102
114
|
let buf = '';
|
|
103
115
|
const onData = (chunk) => {
|
|
@@ -113,12 +125,15 @@ export async function startQuickTunnel({ port, home, signal, onPhase = () => {}
|
|
|
113
125
|
cleanup();
|
|
114
126
|
reject(new Error(`cloudflared 退出(code=${code})`));
|
|
115
127
|
};
|
|
116
|
-
|
|
128
|
+
cleanup = () => {
|
|
117
129
|
child.stdout.off('data', onData);
|
|
118
130
|
child.stderr.off('data', onData);
|
|
119
131
|
child.off('exit', onExit);
|
|
120
132
|
clearTimeout(timer);
|
|
121
133
|
signal?.removeEventListener('abort', onAbort);
|
|
134
|
+
// M4:摘掉监听后管道不再消费 → 64KB 缓冲填满会阻塞 cloudflared → 继续吞掉输出
|
|
135
|
+
child.stdout.resume();
|
|
136
|
+
child.stderr.resume();
|
|
122
137
|
};
|
|
123
138
|
const onAbort = () => {
|
|
124
139
|
cleanup();
|
|
@@ -138,6 +153,13 @@ export async function startQuickTunnel({ port, home, signal, onPhase = () => {}
|
|
|
138
153
|
child.stderr.on('data', onData);
|
|
139
154
|
child.once('exit', onExit);
|
|
140
155
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
156
|
+
rejectErr = reject;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// M1:隧道进程运行中死亡(崩溃/被杀)→ 通知监听方(service 据此把状态从 ready 打回)
|
|
160
|
+
const exitListeners = new Set();
|
|
161
|
+
child.on('exit', (code) => {
|
|
162
|
+
for (const cb of exitListeners) cb(code);
|
|
141
163
|
});
|
|
142
164
|
|
|
143
165
|
return {
|
|
@@ -145,5 +167,10 @@ export async function startQuickTunnel({ port, home, signal, onPhase = () => {}
|
|
|
145
167
|
kill: () => {
|
|
146
168
|
try { child.kill(); } catch { /* 忽略 */ }
|
|
147
169
|
},
|
|
170
|
+
/** 注册「进程已退出」回调,返回取消函数。 */
|
|
171
|
+
onExit: (cb) => {
|
|
172
|
+
exitListeners.add(cb);
|
|
173
|
+
return () => exitListeners.delete(cb);
|
|
174
|
+
},
|
|
148
175
|
};
|
|
149
176
|
}
|
package/lib/web-rpc.js
CHANGED
|
@@ -50,12 +50,22 @@ export function installPocketRpc(ctx, { service, log = console, runUpdate = null
|
|
|
50
50
|
if (endpoint === POCKET_ENDPOINTS.update) {
|
|
51
51
|
if (!runUpdate) return fail('bad-request', '更新不可用 | update unavailable');
|
|
52
52
|
const result = await runUpdate.perform(payload?.profile ?? 'web');
|
|
53
|
+
// 更新成功 → 自动重启生效(用户只点一次;helper 拉起失败则保持现状,可手动重启)
|
|
54
|
+
if (result?.ok && restart) {
|
|
55
|
+
const rr = restart();
|
|
56
|
+
result.autoRestart = rr?.helperPid != null;
|
|
57
|
+
}
|
|
53
58
|
return ok(result);
|
|
54
59
|
}
|
|
55
60
|
if (endpoint === POCKET_ENDPOINTS.restart) {
|
|
56
61
|
if (!restart) return fail('bad-request', '重启不可用 | restart unavailable');
|
|
57
62
|
const result = restart();
|
|
58
|
-
|
|
63
|
+
// 重启拉起失败(helper 都没 spawn 出来)→ 如实报错,别让 UI 误报成功
|
|
64
|
+
if (!result || result.helperPid == null) {
|
|
65
|
+
return fail('bad-request', `重启失败:${result?.error ?? '未知'} | restart failed`);
|
|
66
|
+
}
|
|
67
|
+
const dshPort = service.dshPort ?? 3080;
|
|
68
|
+
return ok({ ...result, hint: `重启后进程在后台运行;如需停止:lsof -ti :${dshPort} | xargs kill -9` });
|
|
59
69
|
}
|
|
60
70
|
return fail('bad-request', `Unknown endpoint: ${endpoint}`);
|
|
61
71
|
} catch (err) {
|
package/package.json
CHANGED