dsh-pocket 1.0.28 → 1.1.0

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
@@ -121,7 +121,10 @@ 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):
124
+ **First run: "Downloading cloudflared" fails or hangs**:
125
+ - **macOS/Linux**: the plugin first downloads from the **Tsinghua mirror** (measured ~3MB/s, done in seconds); falls back to official GitHub + acceleration mirrors if it fails.
126
+ - **Windows**: no Tsinghua mirror (Homebrew doesn't support Windows) — downloads the ~50MB exe from GitHub directly; **single-threaded, so it's slower — that's expected**, wait a few minutes, or use a proxy.
127
+ - If all sources fail, the settings page shows a hint. Alternatives (any one):
125
128
  1. Install the `cloudflared` command and retry (the plugin then uses the PATH binary, no download):
126
129
  - macOS: `brew install cloudflared`; Linux: `sudo apt install cloudflared` or from the official site
127
130
  - Windows: `winget install cloudflared` or from the official site
package/README.md CHANGED
@@ -122,7 +122,10 @@ npx @deepseek-ai/dsh web
122
122
 
123
123
  **其他可能**:企业防火墙/校园网拦截出站;此时请让 IT 放行或改用热点。
124
124
 
125
- **首次开启时「下载 cloudflared」失败/卡住**:cloudflared 从 GitHub 发布页下载(约 20MB),国内直连经常失败。插件会自动依次尝试多个 GitHub 加速镜像;全部失败时设置页会给出提示。备选方案(任选其一):
125
+ **首次开启时「下载 cloudflared」失败/卡住**:
126
+ - **macOS/Linux**:优先走**清华镜像**(实测 ~3MB/s,几秒下完);失败自动回退官方 GitHub + 加速源。
127
+ - **Windows**:无清华镜像(Homebrew 不支持 Windows),走官方直连下载(约 50MB,**单线程会慢,属正常**,耐心等几分钟;也可挂代理加速)。
128
+ - 全部失败时设置页会给出提示。备选方案(任选其一):
126
129
  1. 手动装好命令行 cloudflared 后重试(装好后 dsh-pocket 直接用 PATH 里的,不再下载):
127
130
  - macOS:`brew install cloudflared`;Linux:`sudo apt install cloudflared` 或官网下载
128
131
  - Windows:`winget install cloudflared` 或官网下载
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, stat } from 'node:fs/promises';
7
+ import { mkdir, access, chmod, rm, stat, rename, cp } 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';
@@ -21,68 +21,146 @@ function platformBinary() {
21
21
  }
22
22
 
23
23
  /**
24
- * cloudflared 下载源(依次尝试)。
25
- * 官方源在国内直连经常失败(GitHub releases 被墙/不稳定),
26
- * 后面几个是常见的 GitHub 加速镜像——多源尝试提高国内成功率。
24
+ * cloudflared 下载源。
25
+ * 优先:清华 TUNA 镜像的 Homebrew bottle(国内 CDN,实测 ~3MB/s)——仅 macOS/Linux
26
+ * 且有对应 bottle 时可用(Windows 无 Homebrew,自动跳过)。
27
+ * 兜底:官方 GitHub + 国内加速源(ghproxy.net / gh.ddlc.top / gh-proxy.com,2026-08
28
+ * 实测可达)。npmmirror(淘宝)没有 cloudflared 镜像(已实测 404)。
27
29
  */
28
30
  const CLOUDFLARED_MIRRORS = [
29
31
  (asset) => `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
30
- (asset) => `https://ghfast.top/https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
32
+ (asset) => `https://ghproxy.net/https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
33
+ (asset) => `https://gh.ddlc.top/https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
31
34
  (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
35
  ];
34
36
 
37
+ const TUNA_BOTTLES = 'https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/';
38
+
35
39
  function hostOf(url) {
36
40
  try { return new URL(url).host; } catch { return url; }
37
41
  }
38
42
 
43
+ /**
44
+ * 清华 TUNA 镜像的 cloudflared Homebrew bottle URL(国内 CDN,实测 ~3MB/s)。
45
+ * 只覆盖 macOS/Linux(Windows 无 Homebrew bottle,返回 null)。
46
+ * 匹配按 CPU 架构取清华目录里版本号最新的 bottle——Homebrew 构建时部署目标
47
+ * 设得较老、向后兼容,所以旧系统(如 Ventura)也能用新一点的 bottle。
48
+ * 抓目录失败/无匹配 → null(调用方回退 GitHub/加速源,不影响可用性)。
49
+ */
50
+ async function tsinghuaBottleUrl({ os, a }) {
51
+ if (os !== 'darwin' && os !== 'linux') return null;
52
+ let res;
53
+ try {
54
+ res = await fetch(TUNA_BOTTLES, { signal: AbortSignal.timeout(20_000) });
55
+ } catch { return null; }
56
+ if (!res.ok) return null;
57
+ let html;
58
+ try { html = await res.text(); } catch { return null; }
59
+ // macOS: arm64_<代号> 或 <代号>(Intel 无前缀),代号白名单排除 linux;Linux: arm64_linux / x86_64_linux
60
+ const MACOS_CODES = 'monterey|ventura|sonoma|sequoia|tahoe';
61
+ const pattern = os === 'darwin'
62
+ ? new RegExp(`cloudflared-([0-9.]+)\\.${a === 'arm64' ? 'arm64_' : ''}(${MACOS_CODES})\\.bottle\\.tar\\.gz`, 'g')
63
+ : new RegExp(`cloudflared-([0-9.]+)\\.${a === 'arm64' ? 'arm64' : 'x86_64'}_linux\\.bottle\\.tar\\.gz`, 'g');
64
+ let best = null;
65
+ let bestV = '';
66
+ for (const m of html.matchAll(pattern)) {
67
+ if (m[1] > bestV) { bestV = m[1]; best = m[0]; }
68
+ }
69
+ return best ? `${TUNA_BOTTLES}${best}` : null;
70
+ }
71
+
39
72
  async function downloadCloudflared(binPath, signal) {
40
73
  const { os, a, ext } = platformBinary();
41
- // cloudflared 新版发布资产是 .tgz 压缩包(内含 cloudflared 二进制)
42
- const asset = `cloudflared-${os}-${a}.tgz`;
43
74
  const dir = dirname(binPath);
44
- const tgz = join(dir, `cloudflared.tgz`);
75
+ const tmpFile = join(dir, `cloudflared.download`);
76
+ const isWindows = os === 'windows';
77
+ // 发布资产:Windows 是 .exe(下载即二进制),macOS/Linux 是 .tgz(需解压)
78
+ const asset = isWindows ? `cloudflared-windows-${a}.exe` : `cloudflared-${os}-${a}.tgz`;
45
79
  const fetchSignal = signal
46
- ? AbortSignal.any([signal, AbortSignal.timeout(90_000)])
47
- : AbortSignal.timeout(90_000);
80
+ ? AbortSignal.any([signal, AbortSignal.timeout(120_000)])
81
+ : AbortSignal.timeout(120_000);
82
+
83
+ // 构建有序源列表:[{url, host}];清华(如有,仅 macOS/Linux)排第一,再官方 + 加速源
84
+ const sources = [];
85
+ if (!isWindows) {
86
+ const tua = await tsinghuaBottleUrl({ os, a }).catch(() => null);
87
+ if (tua) sources.push({ url: tua, host: 'mirrors.tuna.tsinghua.edu.cn' });
88
+ }
89
+ for (const m of CLOUDFLARED_MIRRORS) sources.push({ url: m(asset), host: hostOf(m(asset)) });
48
90
 
49
91
  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)})…`);
92
+ for (let i = 0; i < sources.length; i++) {
93
+ const { url, host } = sources[i];
94
+ console.log(`⬇️ 下载 cloudflared(${i + 1}/${sources.length}:${host})…`);
53
95
  try {
54
96
  const res = await fetch(url, { signal: fetchSignal });
55
97
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
56
- await pipeline(Readable.fromWeb(res.body), createWriteStream(tgz));
98
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(tmpFile));
57
99
  // 简单校验:空文件/极小文件视为下载失败(可能是镜像返回了错误页)
58
- const st = await stat(tgz);
100
+ const st = await stat(tmpFile);
59
101
  if (st.size < 1024 * 1024) throw new Error(`文件异常小(${st.size} 字节),疑似镜像错误页`);
60
102
  lastErr = null;
61
103
  break; // 下载成功
62
104
  } catch (err) {
63
105
  lastErr = err;
64
- await rm(tgz, { force: true }).catch(() => {}); // 清掉半截文件
106
+ await rm(tmpFile, { force: true }).catch(() => {}); // 清掉半截文件
65
107
  console.warn(` ⚠️ 源 ${i + 1} 失败:${err?.message ?? err},尝试下一个…`);
66
108
  }
67
109
  }
68
110
  if (lastErr) {
69
111
  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`,
112
+ `cloudflared 下载失败:所有源都不通(最后错误:${lastErr?.message ?? lastErr})。`
113
+ + (isWindows
114
+ ? `Windows 可手动安装后重试:winget install cloudflared;或下载 ${asset} 放到 ${dir} 目录 | download failed — try: winget install cloudflared, or put the exe into ${dir}`
115
+ : `可手动安装后重试:npm i -g cloudflared(装好命令行 cloudflared 即可,无需下载);或开启代理/换网络后重试 | all mirrors failed — install cloudflared manually: npm i -g cloudflared, then retry`),
73
116
  );
74
117
  }
75
118
 
76
- // 解压 tar.gz 目录下的 cloudflared 二进制
77
- await new Promise((resolve, reject) => {
78
- const child = spawn('tar', ['-xzf', tgz, '-C', dir], { stdio: 'ignore' });
79
- child.once('exit', (code) => code === 0 ? resolve() : reject(new Error(`cloudflared 解压失败(code=${code})`)));
80
- child.once('error', reject);
81
- });
82
- const extracted = join(dir, `cloudflared${ext}`);
83
- if (os !== 'windows') await chmod(extracted, 0o755);
84
- // 解压成功就删掉 ~20MB tgz,避免长期占用缓存目录
85
- await rm(tgz, { force: true }).catch(() => {});
119
+ let extracted = join(dir, `cloudflared${ext}`);
120
+ if (isWindows) {
121
+ // Windows:exe 直接就是二进制,无需解压
122
+ await rename(tmpFile, extracted).catch(async () => {
123
+ await cp(tmpFile, extracted).catch(() => {});
124
+ });
125
+ } else {
126
+ // 解压到独立临时子目录(bottle 解压产物会占用 cacheDir/cloudflared 这个名字,
127
+ // 直接解压到 dir 会让目标路径变成目录,rename 失败)
128
+ const extractDir = join(dir, `.extract-${process.pid}-${Date.now()}`);
129
+ await mkdir(extractDir, { recursive: true });
130
+ try {
131
+ await new Promise((resolve, reject) => {
132
+ const child = spawn('tar', ['-xzf', tmpFile, '-C', extractDir], { stdio: 'ignore' });
133
+ child.once('exit', (code) => code === 0 ? resolve() : reject(new Error(`cloudflared 解压失败(code=${code})`)));
134
+ child.once('error', reject);
135
+ });
136
+ // 找真实的二进制**文件**(排除目录):
137
+ // - GitHub tgz:extractDir/cloudflared
138
+ // - Homebrew bottle(清华):extractDir/cloudflared/<版本>/bin/cloudflared
139
+ const { readdir } = await import('node:fs/promises');
140
+ let found = null;
141
+ const direct = join(extractDir, `cloudflared${ext}`);
142
+ try { if ((await stat(direct)).isFile()) found = direct; } catch { /* 不存在 */ }
143
+ if (!found) {
144
+ const verDir = join(extractDir, 'cloudflared');
145
+ try {
146
+ const vers = await readdir(verDir);
147
+ for (const v of vers) {
148
+ const bin = join(verDir, v, 'bin', `cloudflared${ext}`);
149
+ try { if ((await stat(bin)).isFile()) { found = bin; break; } } catch { /* 继续 */ }
150
+ }
151
+ } catch { /* 无此目录 */ }
152
+ }
153
+ if (!found) throw new Error('cloudflared 解压成功但未找到二进制 | binary not found after extract');
154
+ if (found !== extracted) {
155
+ await rename(found, extracted).catch(async () => { await cp(found, extracted).catch(() => {}); });
156
+ }
157
+ } finally {
158
+ await rm(extractDir, { recursive: true, force: true }).catch(() => {});
159
+ }
160
+ }
161
+ if (!isWindows) await chmod(extracted, 0o755);
162
+ // 解压/搬移完成就删掉临时下载文件,避免长期占用缓存目录
163
+ await rm(tmpFile, { force: true }).catch(() => {});
86
164
  return extracted;
87
165
  }
88
166
 
@@ -104,6 +182,8 @@ let downloading = null;
104
182
  * 优先:PATH 已有 → 直接用;否则用持久缓存($DSH_HOME/dsh-pocket/cloudflared),
105
183
  * 只有缓存缺失才下载——避免每次开启公网都重新下 20MB。
106
184
  */
185
+ export { tsinghuaBottleUrl };
186
+
107
187
  export async function resolveCloudflared({ home, onPhase = () => {}, signal } = {}) {
108
188
  if (cloudflaredOnPath()) return 'cloudflared';
109
189
  const dshHome = home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
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.28"
79
+ "version": "1.1.0"
80
80
  }