dsh-pocket 1.0.28 → 1.0.29
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/lib/tunnel.mjs +110 -30
- package/package.json +1 -1
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
|
-
*
|
|
26
|
-
*
|
|
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://
|
|
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
|
|
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(
|
|
47
|
-
: AbortSignal.timeout(
|
|
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 <
|
|
51
|
-
const url =
|
|
52
|
-
console.log(`⬇️ 下载 cloudflared(${i + 1}/${
|
|
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(
|
|
98
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(tmpFile));
|
|
57
99
|
// 简单校验:空文件/极小文件视为下载失败(可能是镜像返回了错误页)
|
|
58
|
-
const st = await stat(
|
|
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(
|
|
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
|
|
71
|
-
+
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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