dsh-pocket 1.0.26 → 1.0.28

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 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 with `lsof -ti :3080 \| xargs kill -9` (logs under `$DSH_HOME` as `dsh-pocket-restart-*.log`) |
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 进程还占着端口:`lsof -ti :3081 \| xargs kill -9` 后重试 |
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 后台进程(不挂终端),是页内更新的标准做法;停止它:`lsof -ti :3080 \| xargs kill -9`(日志在 `$DSH_HOME` 下 `dsh-pocket-restart-*.log`) |
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
@@ -1311,6 +1311,15 @@ function PocketSettingsTab({ rpcCall }) {
1311
1311
  if (s.restartNotice) {
1312
1312
  setRestartNotice(true);
1313
1313
  setUpdateInfo(null);
1314
+ if (!sessionStorage.getItem("dshp-auto-reloaded")) {
1315
+ sessionStorage.setItem("dshp-auto-reloaded", "1");
1316
+ setTimeout(() => {
1317
+ try {
1318
+ location.reload();
1319
+ } catch {
1320
+ }
1321
+ }, 2e3);
1322
+ }
1314
1323
  }
1315
1324
  } catch {
1316
1325
  }
@@ -1320,6 +1329,12 @@ function PocketSettingsTab({ rpcCall }) {
1320
1329
  const t = setInterval(load, 3e3);
1321
1330
  return () => clearInterval(t);
1322
1331
  }, []);
1332
+ (0, import_react2.useEffect)(() => {
1333
+ try {
1334
+ sessionStorage.removeItem("dshp-auto-reloaded");
1335
+ } catch {
1336
+ }
1337
+ }, []);
1323
1338
  (0, import_react2.useEffect)(() => {
1324
1339
  let alive = true;
1325
1340
  (async () => {
@@ -1424,7 +1439,7 @@ function PocketSettingsTab({ rpcCall }) {
1424
1439
  (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F504} \u5DF2\u91CD\u542F | Restarted"),
1425
1440
  (0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, "\u77E5\u9053\u4E86 | OK")
1426
1441
  ),
1427
- (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`)
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`}`)
1428
1443
  ) : null,
1429
1444
  // 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
1430
1445
  updateInfo ? (0, import_react2.createElement)(
package/client/index.jsx CHANGED
@@ -46,9 +46,14 @@ function PocketSettingsTab({ rpcCall }) {
46
46
  setStatus(s);
47
47
  setTunnelState(s.tunnelState ?? null);
48
48
  if (s.restartNotice) {
49
- // 新进程确认起来了:显示一次「已重启」,并清掉旧的更新横幅(单状态,不并存)
49
+ // 新进程确认起来了:显示一次「已重启」,清掉旧的更新横幅(单状态,不并存),
50
+ // 然后自动刷新页面加载新代码——不用用户手动刷新
50
51
  setRestartNotice(true);
51
52
  setUpdateInfo(null);
53
+ if (!sessionStorage.getItem('dshp-auto-reloaded')) {
54
+ sessionStorage.setItem('dshp-auto-reloaded', '1');
55
+ setTimeout(() => { try { location.reload(); } catch { /* 忽略 */ } }, 2000);
56
+ }
52
57
  }
53
58
  } catch { /* 忽略瞬时失败 */ }
54
59
  };
@@ -59,6 +64,11 @@ function PocketSettingsTab({ rpcCall }) {
59
64
  return () => clearInterval(t);
60
65
  }, []);
61
66
 
67
+ // 每次页面加载清掉自动刷新标记——这样下次重启(更新后)才能再次触发自动刷新
68
+ useEffect(() => {
69
+ try { sessionStorage.removeItem('dshp-auto-reloaded'); } catch { /* 忽略 */ }
70
+ }, []);
71
+
62
72
  // 版本检测:host 当前版本 vs npm registry latest(registry 带 CORS *)
63
73
  // 两种情况显示横幅:① 有新版可更新;② 磁盘已更新但进程还是旧代码(重启生效)
64
74
  useEffect(() => {
@@ -158,7 +168,7 @@ function PocketSettingsTab({ rpcCall }) {
158
168
  h('div', { style: { fontWeight: 600, fontSize: 13 } }, '🔄 已重启 | Restarted'),
159
169
  h('button', { style: styles.btn, onClick: () => setRestartNotice(false) }, '知道了 | OK'),
160
170
  ),
161
- h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' }, `进程在后台运行(不挂终端)。如需停止:lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9`),
171
+ h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' }, `进程在后台运行(不挂终端)。如需停止:${status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9`}`),
162
172
  ) : null,
163
173
 
164
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 } 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,20 +20,59 @@ function platformBinary() {
20
20
  return { os, a, ext: os === 'windows' ? '.exe' : '' };
21
21
  }
22
22
 
23
+ /**
24
+ * cloudflared 下载源(依次尝试)。
25
+ * 官方源在国内直连经常失败(GitHub releases 被墙/不稳定),
26
+ * 后面几个是常见的 GitHub 加速镜像——多源尝试提高国内成功率。
27
+ */
28
+ const CLOUDFLARED_MIRRORS = [
29
+ (asset) => `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
30
+ (asset) => `https://ghfast.top/https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
31
+ (asset) => `https://gh-proxy.com/https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
32
+ (asset) => `https://mirror.ghproxy.com/https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
33
+ ];
34
+
35
+ function hostOf(url) {
36
+ try { return new URL(url).host; } catch { return url; }
37
+ }
38
+
23
39
  async function downloadCloudflared(binPath, signal) {
24
40
  const { os, a, ext } = platformBinary();
25
41
  // 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})`);
42
+ const asset = `cloudflared-${os}-${a}.tgz`;
34
43
  const dir = dirname(binPath);
35
44
  const tgz = join(dir, `cloudflared.tgz`);
36
- await pipeline(Readable.fromWeb(res.body), createWriteStream(tgz));
45
+ const fetchSignal = signal
46
+ ? AbortSignal.any([signal, AbortSignal.timeout(90_000)])
47
+ : AbortSignal.timeout(90_000);
48
+
49
+ let lastErr = null;
50
+ for (let i = 0; i < CLOUDFLARED_MIRRORS.length; i++) {
51
+ const url = CLOUDFLARED_MIRRORS[i](asset);
52
+ console.log(`⬇️ 下载 cloudflared(${i + 1}/${CLOUDFLARED_MIRRORS.length}:${hostOf(url)})…`);
53
+ try {
54
+ const res = await fetch(url, { signal: fetchSignal });
55
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
56
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(tgz));
57
+ // 简单校验:空文件/极小文件视为下载失败(可能是镜像返回了错误页)
58
+ const st = await stat(tgz);
59
+ if (st.size < 1024 * 1024) throw new Error(`文件异常小(${st.size} 字节),疑似镜像错误页`);
60
+ lastErr = null;
61
+ break; // 下载成功
62
+ } catch (err) {
63
+ lastErr = err;
64
+ await rm(tgz, { force: true }).catch(() => {}); // 清掉半截文件
65
+ console.warn(` ⚠️ 源 ${i + 1} 失败:${err?.message ?? err},尝试下一个…`);
66
+ }
67
+ }
68
+ if (lastErr) {
69
+ throw new Error(
70
+ `cloudflared 下载失败:所有镜像源都不通(最后错误:${lastErr?.message ?? lastErr})。`
71
+ + `可手动安装后重试:npm i -g cloudflared(装好命令行 cloudflared 即可,无需下载);`
72
+ + `或开启代理/换网络后重试 | all mirrors failed — install cloudflared manually: npm i -g cloudflared, then retry`,
73
+ );
74
+ }
75
+
37
76
  // 解压 tar.gz → 目录下的 cloudflared 二进制
38
77
  await new Promise((resolve, reject) => {
39
78
  const child = spawn('tar', ['-xzf', tgz, '-C', dir], { stdio: 'ignore' });
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
- return ok({ ...redactStatus(await service.status()), restartNotice: notice });
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: `重启后进程在后台运行;如需停止:lsof -ti :${dshPort} | xargs kill -9` });
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
@@ -76,5 +76,5 @@
76
76
  "access": "public",
77
77
  "registry": "https://registry.npmjs.org/"
78
78
  },
79
- "version": "1.0.26"
79
+ "version": "1.0.28"
80
80
  }