dsh-pocket 1.0.29 → 1.2.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 +4 -1
- package/README.md +4 -1
- package/lib/tunnel.mjs +107 -3
- package/package.json +1 -1
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**:
|
|
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」失败/卡住**:
|
|
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
|
@@ -36,10 +36,114 @@ const CLOUDFLARED_MIRRORS = [
|
|
|
36
36
|
|
|
37
37
|
const TUNA_BOTTLES = 'https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/';
|
|
38
38
|
|
|
39
|
+
/** 多线程分块下载的并发段数(Windows 官方源单线程 ~200KB/s,8 并发 ≈ 1.6MB/s)。 */
|
|
40
|
+
const PARALLEL_SEGMENTS = 8;
|
|
41
|
+
/** 小于该字节数的文件不值得分块(直接用单线程)。 */
|
|
42
|
+
const MIN_PARALLEL_SIZE = 8 * 1024 * 1024;
|
|
43
|
+
/** 探针大小:单线程先下这么多测速。 */
|
|
44
|
+
const PROBE_SIZE = 2 * 1024 * 1024;
|
|
45
|
+
/** 探针测速阈值(bytes/ms):低于它认为慢网络,切多线程。300KB/s = 0.3。 */
|
|
46
|
+
const SLOW_SPEED_THRESHOLD = 0.3;
|
|
47
|
+
|
|
39
48
|
function hostOf(url) {
|
|
40
49
|
try { return new URL(url).host; } catch { return url; }
|
|
41
50
|
}
|
|
42
51
|
|
|
52
|
+
/** 合并多个分段文件为一个目标文件(顺序拼接后统一结束)。 */
|
|
53
|
+
async function mergeParts(partFiles, dest) {
|
|
54
|
+
const { createReadStream } = await import('node:fs');
|
|
55
|
+
const out = createWriteStream(dest);
|
|
56
|
+
try {
|
|
57
|
+
for (const f of partFiles) {
|
|
58
|
+
await new Promise((resolve, reject) => {
|
|
59
|
+
const rs = createReadStream(f);
|
|
60
|
+
rs.on('error', reject);
|
|
61
|
+
rs.pipe(out, { end: false });
|
|
62
|
+
rs.on('end', resolve);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
await new Promise((r) => out.end(r));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 下载文件到 dest(自适应):
|
|
72
|
+
* 1. 服务器不支持 Range 或文件小 → 单线程;
|
|
73
|
+
* 2. 单线程下载探针(PROBE_SIZE)测速——速度够快 → 继续单线程(多线程在部分网络/
|
|
74
|
+
* 服务器上反而更慢,如 GitHub CDN 并发限速);
|
|
75
|
+
* 3. 探针速度低于阈值(典型慢网络,如 Windows 用户官方源 ~200KB/s)→ 丢弃探针,
|
|
76
|
+
* 改 8 段并发分块(可把 200KB/s 拉到 1.6MB/s)。
|
|
77
|
+
* 返回实际下载字节数。
|
|
78
|
+
*/
|
|
79
|
+
export async function downloadFile(url, dest, { signal, segments = PARALLEL_SEGMENTS } = {}) {
|
|
80
|
+
// HEAD 探测:Content-Length + Accept-Ranges
|
|
81
|
+
let head = null;
|
|
82
|
+
try { head = await fetch(url, { method: 'HEAD', signal }); } catch { head = null; }
|
|
83
|
+
const len = head ? Number(head.headers.get('content-length') || 0) : 0;
|
|
84
|
+
const acceptsRanges = head ? String(head.headers.get('accept-ranges') || '').toLowerCase() === 'bytes' : false;
|
|
85
|
+
|
|
86
|
+
if (!head || !acceptsRanges || len < MIN_PARALLEL_SIZE) {
|
|
87
|
+
// 单线程
|
|
88
|
+
const res = await fetch(url, { signal });
|
|
89
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
90
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
|
|
91
|
+
return len || 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 探针测速:单线程下载前 PROBE_SIZE,计时
|
|
95
|
+
const probeBytes = Math.min(PROBE_SIZE, len);
|
|
96
|
+
const probeStart = Date.now();
|
|
97
|
+
try {
|
|
98
|
+
const probeRes = await fetch(url, { signal, headers: { Range: `bytes=0-${probeBytes - 1}` } });
|
|
99
|
+
if (!probeRes.ok) throw new Error(`HTTP ${probeRes.status} (probe)`);
|
|
100
|
+
const probeBody = await probeRes.arrayBuffer();
|
|
101
|
+
const probeMs = Date.now() - probeStart;
|
|
102
|
+
const probeSpeed = probeMs > 0 ? probeBytes / probeMs : Infinity; // bytes/ms
|
|
103
|
+
if (probeMs < 500 || probeSpeed >= SLOW_SPEED_THRESHOLD) {
|
|
104
|
+
// 够快 → 单线程下完剩余部分(探针字节已拿到,写入 dest)
|
|
105
|
+
const { createWriteStream, createReadStream } = await import('node:fs');
|
|
106
|
+
const w = createWriteStream(dest);
|
|
107
|
+
await new Promise((resolve, reject) => {
|
|
108
|
+
w.on('error', reject);
|
|
109
|
+
w.write(Buffer.from(probeBody));
|
|
110
|
+
w.end(resolve);
|
|
111
|
+
});
|
|
112
|
+
const restRes = await fetch(url, { signal, headers: { Range: `bytes=${probeBytes}-${len - 1}` } });
|
|
113
|
+
if (!restRes.ok) throw new Error(`HTTP ${restRes.status} (rest)`);
|
|
114
|
+
await pipeline(Readable.fromWeb(restRes.body), createWriteStream(dest, { flags: 'a' }));
|
|
115
|
+
return len;
|
|
116
|
+
}
|
|
117
|
+
// 慢 → 丢弃探针,转分块并发(从 0 开始全量分块)
|
|
118
|
+
await rm(dest, { force: true }).catch(() => {});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
await rm(dest, { force: true }).catch(() => {});
|
|
121
|
+
if (!/HTTP|fetch/i.test(String(err?.message ?? ''))) throw err; // 探针网络异常 → 抛给上层换源
|
|
122
|
+
// 探针 HTTP 错误(部分服务器 HEAD 与 GET 行为不一致)→ 直接分块
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 分块并发
|
|
126
|
+
const parts = [];
|
|
127
|
+
const chunk = Math.ceil(len / segments);
|
|
128
|
+
for (let i = 0; i < segments; i++) {
|
|
129
|
+
const start = i * chunk;
|
|
130
|
+
const end = i === segments - 1 ? len - 1 : Math.min(start + chunk - 1, len - 1);
|
|
131
|
+
if (start > end) break;
|
|
132
|
+
parts.push({ start, end, file: `${dest}.part${i}` });
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await Promise.all(parts.map(async (p) => {
|
|
136
|
+
const res = await fetch(url, { signal, headers: { Range: `bytes=${p.start}-${p.end}` } });
|
|
137
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} (range ${p.start}-${p.end})`);
|
|
138
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(p.file));
|
|
139
|
+
}));
|
|
140
|
+
await mergeParts(parts.map((p) => p.file), dest);
|
|
141
|
+
} finally {
|
|
142
|
+
await Promise.all(parts.map((p) => rm(p.file, { force: true }).catch(() => {})));
|
|
143
|
+
}
|
|
144
|
+
return len;
|
|
145
|
+
}
|
|
146
|
+
|
|
43
147
|
/**
|
|
44
148
|
* 清华 TUNA 镜像的 cloudflared Homebrew bottle URL(国内 CDN,实测 ~3MB/s)。
|
|
45
149
|
* 只覆盖 macOS/Linux(Windows 无 Homebrew bottle,返回 null)。
|
|
@@ -93,9 +197,9 @@ async function downloadCloudflared(binPath, signal) {
|
|
|
93
197
|
const { url, host } = sources[i];
|
|
94
198
|
console.log(`⬇️ 下载 cloudflared(${i + 1}/${sources.length}:${host})…`);
|
|
95
199
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
await
|
|
200
|
+
// 多线程分块(官方 GitHub 支持 Range,Windows 50MB 从几分钟降到几十秒);
|
|
201
|
+
// 不支持 Range 的源自动回退单线程
|
|
202
|
+
await downloadFile(url, tmpFile, { signal: fetchSignal });
|
|
99
203
|
// 简单校验:空文件/极小文件视为下载失败(可能是镜像返回了错误页)
|
|
100
204
|
const st = await stat(tmpFile);
|
|
101
205
|
if (st.size < 1024 * 1024) throw new Error(`文件异常小(${st.size} 字节),疑似镜像错误页`);
|
package/package.json
CHANGED