dsh-pocket 1.0.6 → 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 CHANGED
@@ -40,6 +40,7 @@ export function redactStatus(s) {
40
40
  tunnelRunning: s?.tunnelRunning === true,
41
41
  tunnelUrl: s?.tunnelUrl ?? null,
42
42
  tunnelQr: s?.tunnelQr ?? null,
43
+ tunnelState: s?.tunnelState ?? { phase: 'idle' },
43
44
  dshPort: s?.dshPort ?? null,
44
45
  };
45
46
  }
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
- setStatus(await call(POCKET_ENDPOINTS.status, {}));
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\uFF08\u9996\u6B21\u9700\u4E0B\u8F7D cloudflared\uFF09" : "\u5F00\u542F\u516C\u7F51\u8BBF\u95EE | Enable anywhere"),
1449
- (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")
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 { setStatus(await call(POCKET_ENDPOINTS.status, {})); } catch { /* 忽略瞬时失败 */ }
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 ? '开启中…(首次需下载 cloudflared)' : '开启公网访问 | Enable anywhere'),
202
- h('div', { style: styles.warn, marginTop: 8 }, '⚠️ DSH 能执行电脑代码:二维码/URL 就是钥匙,请勿发给别人'),
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
@@ -70,6 +70,7 @@ export function apply(ctx, config = {}, internals = {}) {
70
70
  const service = internals.service ?? createPocketService({
71
71
  dshPort,
72
72
  port: internals.port ?? config.port ?? 3081,
73
+ home: internals.home,
73
74
  internals,
74
75
  });
75
76
 
package/lib/push.mjs CHANGED
@@ -13,6 +13,9 @@
13
13
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
14
14
  import { homedir } from 'node:os';
15
15
  import { join, dirname } from 'node:path';
16
+ import { createRequire } from 'node:module';
17
+
18
+ const require = createRequire(import.meta.url);
16
19
 
17
20
  const DATA_REL = join('dsh-pocket', 'push');
18
21
  const VAPID_FILE = 'vapid.json';
@@ -20,7 +23,7 @@ const SUBS_FILE = 'subscriptions.json';
20
23
  const SETTINGS_FILE = 'settings.json';
21
24
 
22
25
  function defaultWebPush() {
23
- // 动态 require(web-push 是 CJS
26
+ // web-push 是 CJS,ESM 里必须经 createRequire 加载
24
27
  return require('web-push');
25
28
  }
26
29
 
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
- const result = await startTunnel({ port: proxy.port, signal: tunnelAbort.signal });
70
- // 归一化:startTunnel 契约返回 {url, kill}(字符串也兼容)
71
- tunnel = typeof result === 'string' ? { url: result, kill: () => {} } : result;
72
- return tunnel.url;
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 { mkdtemp, chmod } from 'node:fs/promises';
8
- import { tmpdir } from 'node:os';
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
- /** 拿一个可用的 cloudflared 路径(PATH 没有就下载到临时目录)。 */
55
- async function resolveCloudflared() {
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 dir = await mkdtemp(join(tmpdir(), 'dsh-pocket-'));
58
- return downloadCloudflared(join(dir, `cloudflared${platformBinary().ext}`));
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
@@ -77,5 +77,5 @@
77
77
  "access": "public",
78
78
  "registry": "https://registry.npmjs.org/"
79
79
  },
80
- "version": "1.0.6"
80
+ "version": "1.0.8"
81
81
  }