dsh-pocket 1.0.7 → 1.0.8
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/api.js +1 -0
- package/client/client.js +16 -3
- package/client/index.jsx +16 -3
- package/lib/index.js +1 -0
- package/lib/service.mjs +27 -5
- package/lib/tunnel.mjs +26 -8
- package/package.json +1 -1
package/client/api.js
CHANGED
package/client/client.js
CHANGED
|
@@ -68,6 +68,7 @@ function redactStatus(s) {
|
|
|
68
68
|
tunnelRunning: s?.tunnelRunning === true,
|
|
69
69
|
tunnelUrl: s?.tunnelUrl ?? null,
|
|
70
70
|
tunnelQr: s?.tunnelQr ?? null,
|
|
71
|
+
tunnelState: s?.tunnelState ?? { phase: "idle" },
|
|
71
72
|
dshPort: s?.dshPort ?? null
|
|
72
73
|
};
|
|
73
74
|
}
|
|
@@ -1303,6 +1304,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1303
1304
|
const [error, setError] = (0, import_react2.useState)(null);
|
|
1304
1305
|
const [pushEnabled, setPushEnabled] = (0, import_react2.useState)(true);
|
|
1305
1306
|
const [pushState, setPushState] = (0, import_react2.useState)("checking");
|
|
1307
|
+
const [tunnelState, setTunnelState] = (0, import_react2.useState)(null);
|
|
1306
1308
|
const [updateInfo, setUpdateInfo] = (0, import_react2.useState)(null);
|
|
1307
1309
|
const call = async (endpoint, payload) => {
|
|
1308
1310
|
const res = await rpcCall(endpoint, payload);
|
|
@@ -1311,7 +1313,9 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1311
1313
|
};
|
|
1312
1314
|
const load = async () => {
|
|
1313
1315
|
try {
|
|
1314
|
-
|
|
1316
|
+
const s = await call(POCKET_ENDPOINTS.status, {});
|
|
1317
|
+
setStatus(s);
|
|
1318
|
+
setTunnelState(s.tunnelState ?? null);
|
|
1315
1319
|
} catch {
|
|
1316
1320
|
}
|
|
1317
1321
|
};
|
|
@@ -1376,6 +1380,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1376
1380
|
const startTunnel = async () => {
|
|
1377
1381
|
setBusy(true);
|
|
1378
1382
|
setError(null);
|
|
1383
|
+
setTunnelState({ phase: "starting", detail: "\u6B63\u5728\u5F00\u542F\u2026", startedAt: Date.now() });
|
|
1379
1384
|
try {
|
|
1380
1385
|
setStatus(await call(POCKET_ENDPOINTS.tunnelStart, {}));
|
|
1381
1386
|
} catch (err) {
|
|
@@ -1392,6 +1397,10 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1392
1397
|
};
|
|
1393
1398
|
const lanUrl = status?.lanUrl;
|
|
1394
1399
|
const tunnelUrl = status?.tunnelUrl;
|
|
1400
|
+
const tunnelPhase = tunnelState?.phase ?? "idle";
|
|
1401
|
+
const tunnelStarting = ["downloading", "starting", "registering"].includes(tunnelPhase);
|
|
1402
|
+
const tunnelStateDetail = tunnelState?.detail ?? "";
|
|
1403
|
+
const tunnelStateStarted = tunnelState?.startedAt ?? null;
|
|
1395
1404
|
return (0, import_react2.createElement)(
|
|
1396
1405
|
"div",
|
|
1397
1406
|
{ style: styles.card },
|
|
@@ -1445,8 +1454,12 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1445
1454
|
) : (0, import_react2.createElement)(
|
|
1446
1455
|
"div",
|
|
1447
1456
|
null,
|
|
1448
|
-
(0, import_react2.createElement)("button", { style: styles.primary, onClick: startTunnel, disabled: busy }, busy ? "\u5F00\u542F\u4E2D\u2026
|
|
1449
|
-
(0, import_react2.createElement)(
|
|
1457
|
+
(0, import_react2.createElement)("button", { style: styles.primary, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? "\u5F00\u542F\u4E2D\u2026" : "\u5F00\u542F\u516C\u7F51\u8BBF\u95EE | Enable anywhere"),
|
|
1458
|
+
tunnelStarting ? (0, import_react2.createElement)(
|
|
1459
|
+
"div",
|
|
1460
|
+
{ style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
|
|
1461
|
+
`\u23F3 ${tunnelStateDetail}\uFF08\u5DF2\u7B49\u5F85 ${Math.floor((Date.now() - (tunnelStateStarted || Date.now())) / 1e3)} \u79D2\uFF09\u2026`
|
|
1462
|
+
) : (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")
|
|
1450
1463
|
)
|
|
1451
1464
|
),
|
|
1452
1465
|
// Web Push 状态 + 开关
|
package/client/index.jsx
CHANGED
|
@@ -60,6 +60,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
60
60
|
const [error, setError] = useState(null);
|
|
61
61
|
const [pushEnabled, setPushEnabled] = useState(true); // 宿主开关
|
|
62
62
|
const [pushState, setPushState] = useState('checking'); // checking|on|unsupported|insecure|off
|
|
63
|
+
const [tunnelState, setTunnelState] = useState(null); // 隧道进度 {phase, detail, startedAt}
|
|
63
64
|
const [updateInfo, setUpdateInfo] = useState(null); // { current, latest, updating, result } | null
|
|
64
65
|
|
|
65
66
|
const call = async (endpoint, payload) => {
|
|
@@ -69,7 +70,11 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
const load = async () => {
|
|
72
|
-
try {
|
|
73
|
+
try {
|
|
74
|
+
const s = await call(POCKET_ENDPOINTS.status, {});
|
|
75
|
+
setStatus(s);
|
|
76
|
+
setTunnelState(s.tunnelState ?? null);
|
|
77
|
+
} catch { /* 忽略瞬时失败 */ }
|
|
73
78
|
};
|
|
74
79
|
|
|
75
80
|
useEffect(() => {
|
|
@@ -139,6 +144,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
139
144
|
const startTunnel = async () => {
|
|
140
145
|
setBusy(true);
|
|
141
146
|
setError(null);
|
|
147
|
+
setTunnelState({ phase: 'starting', detail: '正在开启…', startedAt: Date.now() });
|
|
142
148
|
try {
|
|
143
149
|
setStatus(await call(POCKET_ENDPOINTS.tunnelStart, {}));
|
|
144
150
|
} catch (err) {
|
|
@@ -154,6 +160,10 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
154
160
|
|
|
155
161
|
const lanUrl = status?.lanUrl;
|
|
156
162
|
const tunnelUrl = status?.tunnelUrl;
|
|
163
|
+
const tunnelPhase = tunnelState?.phase ?? 'idle';
|
|
164
|
+
const tunnelStarting = ['downloading', 'starting', 'registering'].includes(tunnelPhase);
|
|
165
|
+
const tunnelStateDetail = tunnelState?.detail ?? '';
|
|
166
|
+
const tunnelStateStarted = tunnelState?.startedAt ?? null;
|
|
157
167
|
|
|
158
168
|
return h('div', { style: styles.card },
|
|
159
169
|
h('div', null,
|
|
@@ -198,8 +208,11 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
198
208
|
h('button', { style: styles.btn, onClick: stopTunnel }, '关闭公网 | Stop'),
|
|
199
209
|
)
|
|
200
210
|
: h('div', null,
|
|
201
|
-
h('button', { style: styles.primary, onClick: startTunnel, disabled: busy }, busy ? '
|
|
202
|
-
|
|
211
|
+
h('button', { style: styles.primary, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? '开启中…' : '开启公网访问 | Enable anywhere'),
|
|
212
|
+
tunnelStarting
|
|
213
|
+
? h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } },
|
|
214
|
+
`⏳ ${tunnelStateDetail}(已等待 ${Math.floor((Date.now() - (tunnelStateStarted || Date.now())) / 1000)} 秒)…`)
|
|
215
|
+
: h('div', { style: styles.warn, marginTop: 8 }, '⚠️ DSH 能执行电脑代码:二维码/URL 就是钥匙,请勿发给别人'),
|
|
203
216
|
),
|
|
204
217
|
),
|
|
205
218
|
|
package/lib/index.js
CHANGED
package/lib/service.mjs
CHANGED
|
@@ -39,6 +39,7 @@ function lanIPv4() {
|
|
|
39
39
|
export function createPocketService({
|
|
40
40
|
dshPort,
|
|
41
41
|
port = 3081,
|
|
42
|
+
home,
|
|
42
43
|
internals = {},
|
|
43
44
|
} = {}) {
|
|
44
45
|
const createProxy = internals.createProxy ?? createPocketProxy;
|
|
@@ -48,6 +49,8 @@ export function createPocketService({
|
|
|
48
49
|
let proxy = null;
|
|
49
50
|
let tunnel = null;
|
|
50
51
|
let tunnelAbort = null;
|
|
52
|
+
/** 隧道进度:{ phase: idle|downloading|starting|registering|ready|error, detail, startedAt } */
|
|
53
|
+
const tunnelState = { phase: 'idle', detail: '', startedAt: null };
|
|
51
54
|
|
|
52
55
|
return {
|
|
53
56
|
/** 启动局域网代理(幂等)。 */
|
|
@@ -61,15 +64,30 @@ export function createPocketService({
|
|
|
61
64
|
return proxy;
|
|
62
65
|
},
|
|
63
66
|
|
|
64
|
-
/** 启动公网隧道(幂等;返回公网 URL
|
|
67
|
+
/** 启动公网隧道(幂等;返回公网 URL)。进度写进 tunnelState。 */
|
|
65
68
|
async startTunnel() {
|
|
66
69
|
await this.startProxy();
|
|
67
70
|
if (tunnel) return tunnel.url;
|
|
68
71
|
tunnelAbort = new AbortController();
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
tunnelState.startedAt = Date.now();
|
|
73
|
+
const onPhase = (phase) => {
|
|
74
|
+
tunnelState.phase = phase;
|
|
75
|
+
if (phase === 'downloading') tunnelState.detail = '首次下载 cloudflared(约 20MB)| first run downloads cloudflared (~20MB)';
|
|
76
|
+
else if (phase === 'starting') tunnelState.detail = '启动隧道进程… | starting tunnel…';
|
|
77
|
+
else if (phase === 'registering') tunnelState.detail = '连接 Cloudflare 边缘(通常 5-30 秒)| connecting to Cloudflare edge (usually 5-30s)';
|
|
78
|
+
else if (phase === 'ready') tunnelState.detail = '隧道就绪 | ready';
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
const result = await startTunnel({ port: proxy.port, home, signal: tunnelAbort.signal, onPhase });
|
|
82
|
+
// 归一化:startTunnel 契约返回 {url, kill}(字符串也兼容)
|
|
83
|
+
tunnel = typeof result === 'string' ? { url: result, kill: () => {} } : result;
|
|
84
|
+
tunnelState.phase = 'ready';
|
|
85
|
+
return tunnel.url;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
tunnelState.phase = 'error';
|
|
88
|
+
tunnelState.detail = err?.message ?? String(err);
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
73
91
|
},
|
|
74
92
|
|
|
75
93
|
/** 停止公网隧道(代理保持)。 */
|
|
@@ -78,6 +96,9 @@ export function createPocketService({
|
|
|
78
96
|
tunnelAbort = null;
|
|
79
97
|
if (tunnel) tunnel.kill();
|
|
80
98
|
tunnel = null;
|
|
99
|
+
tunnelState.phase = 'idle';
|
|
100
|
+
tunnelState.detail = '';
|
|
101
|
+
tunnelState.startedAt = null;
|
|
81
102
|
},
|
|
82
103
|
|
|
83
104
|
/** 状态快照(RPC 返回,不含敏感信息;二维码 data URL 本地生成)。 */
|
|
@@ -94,6 +115,7 @@ export function createPocketService({
|
|
|
94
115
|
tunnelRunning: tunnel !== null,
|
|
95
116
|
tunnelUrl: tunnel?.url ?? null,
|
|
96
117
|
tunnelQr: tunnel?.url ? await encode(tunnel.url) : null,
|
|
118
|
+
tunnelState: { ...tunnelState },
|
|
97
119
|
dshPort,
|
|
98
120
|
};
|
|
99
121
|
},
|
package/lib/tunnel.mjs
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// 无密码模式:URL 即钥匙(dsh web 能执行代码,请勿把二维码/URL 发给别人)。
|
|
5
5
|
|
|
6
6
|
import { spawn, execSync } from 'node:child_process';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { mkdir, access, chmod } from 'node:fs/promises';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
9
|
import { join, dirname } from 'node:path';
|
|
10
10
|
import { pipeline } from 'node:stream/promises';
|
|
11
11
|
import { Readable } from 'node:stream';
|
|
@@ -51,28 +51,45 @@ function cloudflaredOnPath() {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* 拿一个可用的 cloudflared 路径。
|
|
56
|
+
* 优先:PATH 已有 → 直接用;否则用持久缓存($DSH_HOME/dsh-pocket/cloudflared),
|
|
57
|
+
* 只有缓存缺失才下载——避免每次开启公网都重新下 20MB。
|
|
58
|
+
*/
|
|
59
|
+
export async function resolveCloudflared({ home, onPhase = () => {} } = {}) {
|
|
56
60
|
if (cloudflaredOnPath()) return 'cloudflared';
|
|
57
|
-
const
|
|
58
|
-
|
|
61
|
+
const dshHome = home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
62
|
+
const cacheDir = join(dshHome, 'dsh-pocket', 'bin');
|
|
63
|
+
const bin = join(cacheDir, `cloudflared${platformBinary().ext}`);
|
|
64
|
+
try {
|
|
65
|
+
await access(bin);
|
|
66
|
+
return bin; // 缓存命中,秒开
|
|
67
|
+
} catch { /* 缓存缺失,下载 */ }
|
|
68
|
+
onPhase('downloading');
|
|
69
|
+
await mkdir(cacheDir, { recursive: true });
|
|
70
|
+
const downloaded = await downloadCloudflared(bin);
|
|
71
|
+
return downloaded;
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
/**
|
|
62
75
|
* 启动 cloudflared 快速隧道,返回公网 URL。
|
|
63
76
|
* @param {object} opts
|
|
64
77
|
* @param {number} opts.port 本机代理端口
|
|
78
|
+
* @param {string} [opts.home] $DSH_HOME(cloudflared 持久缓存)
|
|
65
79
|
* @param {AbortSignal} [opts.signal]
|
|
80
|
+
* @param {(phase:string)=>void} [opts.onPhase] 进度回调:downloading→starting→registering→ready
|
|
66
81
|
* @returns {Promise<{url:string, kill:()=>void}>}
|
|
67
82
|
*/
|
|
68
|
-
export async function startQuickTunnel({ port, signal }) {
|
|
69
|
-
const bin = await resolveCloudflared();
|
|
83
|
+
export async function startQuickTunnel({ port, home, signal, onPhase = () => {} }) {
|
|
84
|
+
const bin = await resolveCloudflared({ home, onPhase });
|
|
85
|
+
onPhase('starting');
|
|
70
86
|
// 强制 HTTP/2(TCP 443)而不是默认的 QUIC(UDP 7844):
|
|
71
87
|
// 国内网络/部分企业网常屏蔽 UDP 7844,导致 tunnel 报 error 1033(Tunnel error);
|
|
72
88
|
// HTTP/2 走 443 更稳。若平台未来恢复 QUIC 可达,可去掉 --protocol http2。
|
|
73
89
|
const child = spawn(bin, ['tunnel', '--url', `http://127.0.0.1:${port}`, '--protocol', 'http2', '--no-autoupdate'], {
|
|
74
90
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
91
|
});
|
|
92
|
+
onPhase('registering');
|
|
76
93
|
|
|
77
94
|
const url = await new Promise((resolve, reject) => {
|
|
78
95
|
let buf = '';
|
|
@@ -81,6 +98,7 @@ export async function startQuickTunnel({ port, signal }) {
|
|
|
81
98
|
const m = buf.match(QUICK_TUNNEL_URL_RE);
|
|
82
99
|
if (m) {
|
|
83
100
|
cleanup();
|
|
101
|
+
onPhase('ready');
|
|
84
102
|
resolve(m[0]);
|
|
85
103
|
}
|
|
86
104
|
};
|
package/package.json
CHANGED