@having5548/dsh-downloader 0.5.2 → 0.5.3

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/CHANGELOG.md CHANGED
@@ -9,6 +9,31 @@
9
9
 
10
10
  ## [未发布]
11
11
 
12
+ ## [0.5.3] - 2026-10-11
13
+
14
+ ### 修复
15
+
16
+ - **上游断管会把整个 DSH 宿主崩掉**(严重,宿主连续崩了两次)。
17
+ 现象:`crash-2026-10-11T03-46-46-081Z-host.log` 与 `03-48-03-999Z-host.log` 的栈指向同一处:
18
+ ```
19
+ dsh: fatal uncaught exception: Error: write EPIPE
20
+ at Writable.write (node:internal/streams/writable:508:10)
21
+ at Object.write (.../@having5548/dsh-downloader/lib/core/transports.js:598:28)
22
+ at Socket.<anonymous> (.../@having5548/dsh-downloader/lib/core/proxy-server.js:154:9)
23
+ ```
24
+ 根因:`nativeConnect()` 用 `stdio: ["pipe","pipe","pipe"]` 起 Go 连接器,但**三根管道一个 `error` 监听都没挂**。
25
+ 连接器进程一死(连接失败 / 超时 / 被 kill),管道随即断裂,之后任何一次写入都会让 `child.stdin` 抛出
26
+ `EPIPE` —— 而在 Node 里**没有监听者的 `error` 事件就是未捕获异常**,直接把宿主带走。
27
+ 原有的 `ended` 标志挡不住这个竞态:写入可能排在一个已经断掉的管道上,而 `exit` 事件还没派发。
28
+ 修法(三层):
29
+ 1. 新增 `stdioFace(child)`:给 `stdin` / `stdout` / `stderr` 与子进程本身都挂 `error` 监听,
30
+ 写入再包一层 try/catch(防同步抛出),并保留连接器最后的 stderr 供排查;
31
+ 2. `ProxyServer.#relay()` 里的 `face.write(chunk)` 包 try/catch,失败即关闭上游并销毁客户端连接;
32
+ 3. 新增**独立子进程**回归测试:让子进程关掉 stdin 但继续活着(于是 `ended` 不会置位、管道却已断),
33
+ 再灌 1.25 MB 数据。旧写法在该探针下必崩(实测 `exit=1`、`code: 'EOF'`),修复后存活。
34
+
35
+ > 测试 366 → 368 项。
36
+
12
37
  ## [0.5.2] - 2026-10-10
13
38
 
14
39
  两处都是把 `dsh_run_proxied` 真正用起来时才暴露的:`git push` 打插件内核**必定**失败。
package/README.en.md CHANGED
@@ -6,10 +6,11 @@
6
6
 
7
7
  [简体中文](README.md) | English
8
8
 
9
- ![Version](https://img.shields.io/badge/version-0.5.2-4c7ef3?style=flat-square)
9
+ ![Version](https://img.shields.io/badge/version-0.5.3-4c7ef3?style=flat-square)
10
+ ![npm](https://img.shields.io/npm/v/@having5548/dsh-downloader?style=flat-square&label=npm)
10
11
  ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-0078d6?style=flat-square)
11
12
  ![Protocols](https://img.shields.io/badge/nodes-ss%20%7C%20trojan%20%7C%20vless%20%7C%20vmess%20%7C%20socks5%20%7C%20http-2b6cb0?style=flat-square)
12
- ![Tests](https://img.shields.io/badge/tests-366%20passed-2fa95e?style=flat-square)
13
+ ![Tests](https://img.shields.io/badge/tests-368%20passed-2fa95e?style=flat-square)
13
14
  ![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)
14
15
 
15
16
  </div>
@@ -50,20 +51,27 @@ The two can be used together; they do not interfere. This plugin **never mutates
50
51
 
51
52
  ## 📦 Install
52
53
 
54
+ From npm (published to the official registry):
55
+
56
+ ```bash
57
+ dsh plugin --profile desktop add @having5548/dsh-downloader
58
+ ```
59
+
53
60
  From the GitHub release:
54
61
 
55
62
  ```bash
56
- dsh plugin --profile desktop add https://github.com/having5548/dsh-downloader/releases/latest/download/having5548-dsh-downloader-0.5.2.tgz
63
+ dsh plugin --profile desktop add https://github.com/having5548/dsh-downloader/releases/latest/download/having5548-dsh-downloader-0.5.3.tgz
57
64
  ```
58
65
 
59
66
  Or build and install locally:
60
67
 
61
68
  ```bash
62
69
  npm pack
63
- dsh plugin --profile desktop add having5548-dsh-downloader-0.5.2.tgz
70
+ dsh plugin --profile desktop add having5548-dsh-downloader-0.5.3.tgz
64
71
  ```
65
72
 
66
73
  > Profile name: the current Electron desktop app uses `desktop`; the older web CLI used `web`.
74
+ > The README shown on the npm page is the one captured **at publish time** — later documentation edits only reach it with the next release.
67
75
 
68
76
  Then open **Settings → Download proxy**.
69
77
 
@@ -257,7 +265,7 @@ The upstream is resolved in this order; the first hit wins:
257
265
  | `downloadDir` | empty | empty = `$DSH_HOME/downloads` |
258
266
  | `maxDownloadMb` | `512` | per-file size limit |
259
267
  | `downloadTimeoutS` | `600` | per-download wall-clock budget |
260
- | `threads` | `4` | segmented connection count; `1` = single stream. Falls back to one connection when the server lacks `Range`, answers it dishonestly, or the file is under 1 MiB |
268
+ | `threads` | `4` | segmented connection count; `1` = single stream. Falls back to one connection when the server lacks `Range`, answers it dishonestly, or the file is under 1 MiB |
261
269
  | `stallTimeoutS` | `30` | abort after this long with no data |
262
270
  | `insecureTls` | `false` | skip TLS verification (corporate TLS interception only) |
263
271
  | `allowOutsideWorkspace` | `false` | allow `save_path` outside the workspace and download directory |
@@ -351,7 +359,7 @@ The core (rule engine, CN data, loopback mixed proxy, node transports, subscript
351
359
 
352
360
  ```bash
353
361
  npm install
354
- node test/smoke.mjs # 366 checks: rules / subscriptions / profile import / routing / HTTP client / end-to-end / session guard / token gate
362
+ node test/smoke.mjs # 368 checks: rules / subscriptions / profile import / routing / HTTP client / end-to-end / session guard / token gate
355
363
  node --check lib/client.js
356
364
  npm pack
357
365
  ```
package/README.md CHANGED
@@ -6,10 +6,11 @@
6
6
 
7
7
  简体中文 | [English](README.en.md)
8
8
 
9
- ![Version](https://img.shields.io/badge/version-0.5.2-4c7ef3?style=flat-square)
9
+ ![Version](https://img.shields.io/badge/version-0.5.3-4c7ef3?style=flat-square)
10
+ ![npm](https://img.shields.io/npm/v/@having5548/dsh-downloader?style=flat-square&label=npm)
10
11
  ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-0078d6?style=flat-square)
11
12
  ![Protocols](https://img.shields.io/badge/nodes-ss%20%7C%20trojan%20%7C%20vless%20%7C%20vmess%20%7C%20socks5%20%7C%20http-2b6cb0?style=flat-square)
12
- ![Tests](https://img.shields.io/badge/tests-366%20passed-2fa95e?style=flat-square)
13
+ ![Tests](https://img.shields.io/badge/tests-368%20passed-2fa95e?style=flat-square)
13
14
  ![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)
14
15
 
15
16
  </div>
@@ -50,20 +51,27 @@ NO_PROXY=localhost,127.0.0.1,::1,deepseek.com,.deepseek.com
50
51
 
51
52
  ## 📦 安装
52
53
 
54
+ 从 npm 安装(已发布到官方源):
55
+
56
+ ```bash
57
+ dsh plugin --profile desktop add @having5548/dsh-downloader
58
+ ```
59
+
53
60
  从 GitHub Release 安装:
54
61
 
55
62
  ```bash
56
- dsh plugin --profile desktop add https://github.com/having5548/dsh-downloader/releases/latest/download/having5548-dsh-downloader-0.5.2.tgz
63
+ dsh plugin --profile desktop add https://github.com/having5548/dsh-downloader/releases/latest/download/having5548-dsh-downloader-0.5.3.tgz
57
64
  ```
58
65
 
59
66
  本地打包安装:
60
67
 
61
68
  ```bash
62
69
  npm pack
63
- dsh plugin --profile desktop add having5548-dsh-downloader-0.5.2.tgz
70
+ dsh plugin --profile desktop add having5548-dsh-downloader-0.5.3.tgz
64
71
  ```
65
72
 
66
73
  > profile 名:新版桌面端是 `desktop`,旧版 Web 端是 `web`。
74
+ > npm 页面上显示的 README 是**发布那一刻**的版本 —— 后来改的文档要等下一次发版才会同步过去。
67
75
 
68
76
  装完打开 **设置 → 下载代理**。
69
77
 
@@ -356,7 +364,7 @@ dsh_run_proxied({ command: "git clone https://github.com/x/y.git" })
356
364
 
357
365
  ```bash
358
366
  npm install
359
- node test/smoke.mjs # 366 项:规则引擎 / 订阅解析 / 配置导入 / 路由 / HTTP 客户端 / 端到端下载 / 会话守卫 / 令牌鉴权
367
+ node test/smoke.mjs # 368 项:规则引擎 / 订阅解析 / 配置导入 / 路由 / HTTP 客户端 / 端到端下载 / 会话守卫 / 令牌鉴权
360
368
  node --check lib/client.js
361
369
  npm pack
362
370
  ```
@@ -151,7 +151,14 @@ export class ProxyServer {
151
151
  }).catch(() => client.destroy());
152
152
  client.on("data", (chunk) => {
153
153
  this.counters.up += chunk.length;
154
- face.write(chunk);
154
+ // [dsh-downloader] 上游可能已经断了(连接器进程退出、socket 被销毁)。
155
+ // 写入必须兜住:让它抛出去就是未捕获异常,会崩掉整个宿主进程。
156
+ try {
157
+ face.write(chunk);
158
+ } catch {
159
+ try { face.close(); } catch { /* already gone */ }
160
+ client.destroy();
161
+ }
155
162
  });
156
163
  client.on("end", () => face.close());
157
164
  client.on("close", () => face.close());
@@ -552,6 +552,60 @@ export function hasNativeConnector() {
552
552
  try { return existsSync(CONNECTOR_PATH); } catch { return false; }
553
553
  }
554
554
 
555
+ /**
556
+ * Wrap a spawned connector's stdio as a duplex face.
557
+ *
558
+ * [dsh-downloader] 这三根管道**必须**在第一时间挂上 `error` 监听。连接器进程一死,
559
+ * 管道就断了,之后任何一次 `write` 都会让 `child.stdin` 抛出 `EPIPE`;而在 Node 里
560
+ * **没有监听者的 `error` 事件是未捕获异常**,会直接把宿主进程带走 ——
561
+ * 实测把 DSH 宿主崩掉两次(crash-2026-10-11T03-46 / 03-48,栈就落在下面这行 write)。
562
+ *
563
+ * 另外 `ended` 标志挡不住这个竞态:`write` 可能排在一个已经断掉的管道上,
564
+ * 而 `exit` 事件还没派发。所以写入本身也要兜住。
565
+ *
566
+ * 导出只为测试:生产路径只有 `nativeConnect` 调用它。
567
+ */
568
+ export function stdioFace(child) {
569
+ let ended = false;
570
+ let stderrTail = "";
571
+ const markEnded = () => {
572
+ ended = true;
573
+ };
574
+
575
+ child.stdin.on("error", markEnded);
576
+ child.stdout.on("error", markEnded);
577
+ child.stderr.on("error", markEnded);
578
+ child.on("error", markEnded);
579
+ child.on("exit", markEnded);
580
+ child.stderr.on("data", (chunk) => {
581
+ stderrTail = (stderrTail + chunk.toString()).slice(-512);
582
+ });
583
+
584
+ return {
585
+ read: (async function* () {
586
+ for await (const chunk of child.stdout) {
587
+ yield chunk;
588
+ }
589
+ })(),
590
+ write: (data) => {
591
+ if (ended) return;
592
+ try {
593
+ child.stdin.write(data);
594
+ } catch {
595
+ // 管道已断(连接器已退出):丢掉这一块,绝不让它冒泡成未捕获异常。
596
+ ended = true;
597
+ }
598
+ },
599
+ close: () => {
600
+ try { child.stdin.end(); } catch { /* already closed */ }
601
+ try { child.stdout.destroy(); } catch { /* already closed */ }
602
+ if (!ended) setTimeout(() => child.kill(), 500);
603
+ },
604
+ /** 连接器最后的 stderr,排查「节点为什么连不上」时有用。 */
605
+ stderrTail: () => stderrTail
606
+ };
607
+ }
608
+
555
609
  /**
556
610
  * Connect through the self-compiled Go connector via stdio: stdin feeds the
557
611
  * remote target, stdout carries the reply. Supports `hysteria2` and `reality`.
@@ -581,28 +635,7 @@ function nativeConnect(proto, node, host, port) {
581
635
  stdio: ["pipe", "pipe", "pipe"],
582
636
  windowsHide: true
583
637
  });
584
- let ended = false;
585
- let stderrTail = "";
586
- child.stderr.on("data", (chunk) => {
587
- stderrTail = (stderrTail + chunk.toString()).slice(-512);
588
- });
589
- child.on("exit", () => { ended = true; });
590
-
591
- return {
592
- read: (async function* () {
593
- for await (const chunk of child.stdout) {
594
- yield chunk;
595
- }
596
- })(),
597
- write: (data) => {
598
- if (!ended) child.stdin.write(data);
599
- },
600
- close: () => {
601
- try { child.stdin.end(); } catch { /* already closed */ }
602
- try { child.stdout.destroy(); } catch { /* already closed */ }
603
- if (!ended) setTimeout(() => child.kill(), 500);
604
- }
605
- };
638
+ return stdioFace(child);
606
639
  }
607
640
 
608
641
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@having5548/dsh-downloader",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "DeepSeek Harness 下载代理插件:注册 dsh_download 等工具,让模型下载境外文件时自动走插件自带的代理内核(订阅自包含,或显式 proxyUrl),境内直连;带 sha256、大小上限、停滞检测与直连失败自动回退,另附「设置 → 下载代理」管理面板。",
5
5
  "keywords": [
6
6
  "dsh",
@@ -83,6 +83,7 @@
83
83
  },
84
84
  "license": "MIT",
85
85
  "publishConfig": {
86
- "access": "public"
86
+ "access": "public",
87
+ "registry": "https://registry.npmjs.org"
87
88
  }
88
89
  }