@having5548/dsh-downloader 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,391 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # ⬇️ dsh-downloader
4
+
5
+ **DeepSeek Harness 下载代理插件** — 模型下载境外文件时自动走代理、境内直连;代理出口自包含(填一条订阅地址即可,**不需要本机装 Clash**);并内置国内各大 AI 平台域名强制直连,节点抖动或代理程序关闭都不会断开会话。
6
+
7
+ 简体中文 | [English](README.en.md)
8
+
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)
11
+ ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-0078d6?style=flat-square)
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)
13
+ ![Tests](https://img.shields.io/badge/tests-368%20passed-2fa95e?style=flat-square)
14
+ ![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)
15
+
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## ✨ 特性
21
+
22
+ | | | |
23
+ |---|---|---|
24
+ | 🌏 **境外走代理、境内直连**<br>`dsh_download` 一次调用内完成判定与下载,不用模型自己挑路线 | 📦 **代理出口自包含**<br>一条订阅即可用,**不依赖本机 Clash**;YAML 与分享链接两种订阅格式都吃,ss / trojan / vless / vmess / hysteria2(含 salamander 混淆)/ socks5 / http | 🛡️ **会话保活**<br>内置 22 家国内 AI 平台域名强制直连,优先级高于一切规则;另有 `dsh_session_guard` 守住 `$DSH_HOME/.env` |
25
+ | ⚡ **大文件多线程**<br>服务端支持 `Range` 时自动分片并发;先完成的连接去偷剩余最多的分片,慢服务器拖不住整体 | 📊 **侧边栏实时流量**<br>不开设置页也能看到经过内核的上下行速率与累计流量 | 🧮 **离线分流判定**<br>内置 11 万条国内域名后缀 + 8.7K 条国内 IP 段,**判定不发任何网络请求**,不怕被墙 | 🔁 **失败自动回退**<br>直连失败(超时 / DNS / 连接重置)自动改用代理重试一次,结果里如实标 `fallback_used` | 🔐 **完整性可验证**<br>边下边算 sha256,返回保存路径、字节数、实时速度与所用路由 |
26
+ | 🧯 **失败不留残骸**<br>先写 `.part` 再原子重命名;**任何失败路径都删掉半截文件**,重跑永远安全 | ⏱️ **大文件不被打断**<br>刻意不声明宿主工具超时,只受自己可配的 `downloadTimeoutS` 与停滞检测约束 | 🖥️ **设置页管理面板**<br>状态 / 配置导入与管理 / 节点列表 / 逐节点测速 / 选组 / 实时流量 / 全部配置项 | 🗂️ **节点配置对齐 FlClash**<br>多份配置(订阅 URL / clash:// 深链 / 本地文件),各自的名字、上次更新时间与自动更新间隔 | 🔒 **只服务 DSH 自己发起的下载**<br>内核只绑回环 + 每进程随机令牌,其它程序连不上也借不走;系统代理与环境变量从未被改动 |
27
+
28
+ ## ⚠️ 先读这一段:它能覆盖什么
29
+
30
+ 本插件是**工具型** —— 它只管 `dsh_download` 自己的下载。
31
+
32
+ | 出网路径 | 是否走本插件 |
33
+ |---|---|
34
+ | `dsh_download` 工具 | ✅ 走,且按境内外分流 |
35
+ | `web_fetch` 工具 | ❌ 不走 |
36
+ | shell 里的 `curl` / `git` / `npm` / `Invoke-WebRequest` | ❌ 不走 |
37
+ | jobs / workflow / subagent 派生的进程 | ❌ 不走 |
38
+ | MCP server 的 HTTP 传输 | ❌ 不走 |
39
+
40
+ 想让**上面这些也走代理**,不需要插件:在 `$DSH_HOME/.env`(Windows 上是 `C:\Users\<你>\.dsh\.env`)里写:
41
+
42
+ ```dotenv
43
+ HTTPS_PROXY=http://127.0.0.1:7890
44
+ HTTP_PROXY=http://127.0.0.1:7890
45
+ NO_PROXY=localhost,127.0.0.1,::1,deepseek.com,.deepseek.com
46
+ ```
47
+
48
+ 重启 DSH 即可 —— DSH 在挂载任何插件之前就会读这个文件,把全局 dispatcher、`web_fetch` 以及每个派生子进程一起接管。**只有这个文件能写代理变量**;仓库目录里的 `.env` 写了会被直接拒绝启动。
49
+
50
+ 两者可以同时用、互不干扰:本插件**从不修改 `process.env`,也从不替换全局 dispatcher**。
51
+
52
+ ## 📦 安装
53
+
54
+ 从 npm 安装(已发布到官方源):
55
+
56
+ ```bash
57
+ dsh plugin --profile desktop add @having5548/dsh-downloader
58
+ ```
59
+
60
+ 从 GitHub Release 安装:
61
+
62
+ ```bash
63
+ dsh plugin --profile desktop add https://github.com/having5548/dsh-downloader/releases/latest/download/having5548-dsh-downloader-0.5.3.tgz
64
+ ```
65
+
66
+ 本地打包安装:
67
+
68
+ ```bash
69
+ npm pack
70
+ dsh plugin --profile desktop add having5548-dsh-downloader-0.5.3.tgz
71
+ ```
72
+
73
+ > profile 名:新版桌面端是 `desktop`,旧版 Web 端是 `web`。
74
+ > npm 页面上显示的 README 是**发布那一刻**的版本 —— 后来改的文档要等下一次发版才会同步过去。
75
+
76
+ 装完打开 **设置 → 下载代理**。
77
+
78
+ ## 🚀 快速开始
79
+
80
+ 1. 打开 **设置 → 下载代理**。
81
+ 2. 在**「节点配置」**卡片里导入节点 —— 对齐 FlClash 的三种方式:
82
+ - **从订阅地址导入**:粘贴 `https://…`,或直接粘 FlClash / Clash Verge 那种 `clash://install-config?url=…` 深链(会自动取出里面的地址)。
83
+ - **从文件导入**:选一个本地 `.yaml` / `.yml` 配置文件。
84
+ - **名称留空**时按 FlClash 的规则自动取名:先取响应头 `Content-Disposition` 里的文件名,再退回域名。
85
+ 每份配置独立保存,带自己的**上次更新时间**与**自动更新开关/间隔**(默认 24 小时;本地文件配置不自动更新,与 FlClash 一致)。
86
+ 3. 点「选用」切换当前配置、「更新」重新拉取、「删除」移除。
87
+ 4. 也可以完全不动手,直接让模型用 `dsh_profile` 工具导入,例如:
88
+
89
+ > 把这个订阅导入:https://example.com/api/v1/client/subscribe?token=xxx
90
+
91
+ 5. 让模型下载一个境外文件,例如:
92
+
93
+ > 下载 https://github.com/X/Y/releases/latest/download/app.zip 到 D:\dl
94
+
95
+ 6. 结果里看 `route`:`proxy` = 走了节点,`direct` = 境内直连,`fallback_used: true` = 直连失败后回退到代理。
96
+
97
+ > 旧版的单条 `subscriptionUrl` 配置依然可用:只有在**没有任何配置**时才会走它;一旦导入过配置,就以选中的那份为准。
98
+
99
+ ## 🔑 需要鉴权的下载(GitHub Actions 产物等)
100
+
101
+ `dsh_download` 支持带凭据下载,但**只接受「引用」,不接受明文 token** —— 因为工具参数会永久留在会话记录里:
102
+
103
+ ```
104
+ dsh_download({ url: "<archive_download_url>", save_path: "...", auth: "gh" })
105
+ ```
106
+
107
+ | `auth` 取值 | 含义 |
108
+ | --- | --- |
109
+ | `gh` | 插件自己跑 `gh auth token --hostname github.com`,从 GitHub CLI 的凭据库取。**推荐** |
110
+ | `env:变量名` | 读环境变量,例如 `auth: "env:GH_TOKEN"` |
111
+ | `bearer:token` | 直接给字面量。方便,但 token 会进会话记录 —— 只用在非敏感场景 |
112
+
113
+ `headers` 参数可以附加任意请求头(键值都是字符串):
114
+
115
+ ```
116
+ dsh_download({ url: "...", save_path: "...", headers: { "X-Api-Key": "..." } })
117
+ ```
118
+
119
+ `Host` / `Content-Length` / `Connection` / `Transfer-Encoding` / `Proxy-Authorization` 这类会破坏请求的头会被**静默忽略**,不会把下载搞坏。
120
+
121
+ **跨域重定向会丢掉凭据(这一点是故意的)**:GitHub 的 `archive_download_url` 会 302 到 blob 域,把 `Authorization` 带过去既没用、又等于把 token 交给第三方主机,某些情况下还会被对端 400 拒绝。所以跳转到**不同主机**时会自动剥掉 `Authorization` / `Cookie`;同主机跳转则保留。
122
+
123
+ 凭据会应用到**每一条**请求 —— 初次探测、每一个分片 Range 请求、以及回退后的单连接重试,都一样。
124
+
125
+ ## ⚡ 多线程下载
126
+
127
+ 服务端支持 `Range` 时,大文件会自动分成 `threads`(默认 4)个连接并发下载。
128
+
129
+ **关键不是"切成 N 份",而是怎么切。** 固定等分只在各家网速相当时才快 —— 一条慢连接会拖住整体(`总时间 = 最慢那份的时间 × 份数` 除以并发)。所以这里照搬 gopeed 的做法(`internal/protocol/http/fetcher.go`):
130
+
131
+ - 起始按 `threads` 均分;
132
+ - **谁先下完,谁就去偷「剩余字节最多的那个分片」的前半段**(下限 512 KiB,太碎就不值得单开请求);
133
+ - 于是慢分片会被不断切走,快的连接一直满载,直到全部抢完。
134
+
135
+ | 情况 | 行为 |
136
+ | --- | --- |
137
+ | 服务端没有 `accept-ranges: bytes` | 单连接 |
138
+ | 文件 < 1 MiB | 单连接(协调开销大于收益) |
139
+ | `threads` = 1 | 单连接 |
140
+ | 服务端**嘴上支持 Range 却返回 200** | 检测到后**回退单连接重下**,绝不拼出坏文件 |
141
+ | `Content-Range` 起点不符 / 合计字节数不符 | 同上,回退重下 |
142
+
143
+ 分片各写同一文件的对应偏移(`FileHandle.write` 定位写),完成后按顺序流式算 sha256 —— 因为分片是乱序落盘的,边下边算会得到错误的哈希。
144
+
145
+ 返回值里的 `via_threads` 与 `segments` 说明实际走了哪条路;面板的「下载」卡片可以调 `threads`。
146
+
147
+ ## 📊 侧边栏实时流量
148
+
149
+ 侧边栏页脚(设置图标旁)有一个流量徽标,实时显示经过本插件内核的上下行速率与累计流量:
150
+
151
+ ```
152
+ ↑ 1.2 MB/s 340 KB
153
+ ↓ 3.4 MB/s 1.8 GB
154
+ ```
155
+
156
+ - 数据来自内核的字节计数器(`/traffic`),**只统计经过本插件的流量**,不是全机网卡流量。
157
+ - 侧边栏收起成 56px 窄条时自动切成极简排版,只显示下行速率。
158
+ - 速率是两次采样之间的差值,所以内核侧加了 1.2 秒的采样缓存 —— 否则面板(5 秒一次)与徽标(1.5 秒一次)会互相偷走对方的采样窗口,读数乱跳。
159
+ - 页面切到后台时降到 5 秒一次。
160
+ - 内核没在跑时徽标不占地方。
161
+
162
+
163
+ ## 🔒 代理作用域:只服务 DSH 自己发起的下载
164
+
165
+ 代理**只**服务本插件发起的下载请求。这不是靠约定,而是三重硬约束:
166
+
167
+ | 约束 | 效果 |
168
+ |---|---|
169
+ | **只绑 `127.0.0.1`** | 局域网内其它机器根本连不上这个监听 |
170
+ | **每进程随机令牌** | 与下载客户端在进程内一起生成,从不落盘、从不写日志。没有令牌的连接一律 `407`;SOCKS5 因为握手带不了令牌,直接被拒 |
171
+ | **不碰系统代理、不写环境变量** | 浏览器、其它 App、以及 DSH 里非本插件的出网请求,全部保持原样 |
172
+
173
+ `dsh_proxy_status` 的返回里有 `scope` 字段如实报告这三条;面板的「上游」卡片也会写出来。
174
+
175
+ **换句话说**:就算这个端口被人扫到,他也用不了;而除 `dsh_download` 以外的任何流量,从来没有经过这个插件。
176
+
177
+ ## 🧩 git / curl 下载境外内容:也走这个代理
178
+
179
+ `git` / `curl` 跑在 shell 子进程里,工具型插件无法在进程外截获它们。所以这里做成**两条腿**:
180
+
181
+ **① 给模型一条带上代理的命令**(`dsh_run_proxied`):
182
+
183
+ ```
184
+ dsh_run_proxied({ command: "git clone https://github.com/x/y.git" })
185
+ ```
186
+
187
+ 它会为**这一次** shell 调用注入代理环境(`HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` 及小写形式,指向本插件内核),
188
+ 然后交给**正常的 shell 工具**执行 —— 沙箱与权限策略照常生效,不是另开一条后门。
189
+ 国内目标仍由内核判定直连;国内 AI 平台域名写进 `NO_PROXY`,连内核都不经过。
190
+
191
+ `execute=false` 时它只返回拼好的命令,你或模型自己用 pwsh 跑也行。
192
+
193
+ **② 一条工具守卫**(`guardShellDownloads`,默认开):
194
+
195
+ 当 shell 命令看起来要从境外下载、又没带代理时(`git clone|fetch|pull|submodule`、`curl`、`wget`、
196
+ `Invoke-WebRequest` 且目标经离线规则判定为境外),命令会被**拦下来**,并提示改用 `dsh_run_proxied` 或 `dsh_download`。
197
+
198
+ 不会被拦的情况:命令里已经带了代理(`HTTP_PROXY`、`curl -x`、`git -c http.proxy`);目标是回环/内网;
199
+ 内核没有可用出口(拦下来却没有替代方案比不拦更糟);SSH 形式(`git@github.com:…` 不走 HTTP,代理环境变量对它无效)。
200
+
201
+ 嫌它碍事可以在设置里关掉。
202
+
203
+ **`git` 会被单独多喂两条配置**:`dsh_run_proxied` 除了注入 `*_PROXY` 环境变量,还会通过 `GIT_CONFIG_COUNT`
204
+ 把 `http.proxy`(带凭据)与 `http.proxyAuthMethod=basic` 塞给 git。原因是 **git 不像 curl 那样预发
205
+ `Proxy-Authorization`** —— 只给环境变量的话它会吃 `407`。这两条对非 git 命令完全无害,所以不做命令嗅探。
206
+
207
+ 内核的 `407` 挑战写的是 **`Basic`**(我们两种都接受:`Bearer <令牌>` 与 `Basic dsh:<令牌>`,
208
+ 但只有 Basic 是客户端普遍能应答的方案)。写 `Bearer` 时 curl/git 拿到不认识的方案会直接放弃重试。
209
+
210
+ **只是要下一个文件时,优先 `dsh_download`** —— 它带 sha256、大小上限、停滞检测与失败回退。
211
+ `dsh_run_proxied` 是给"必须用 git/curl 本身"的场景(克隆仓库、调用 API)准备的。
212
+
213
+ ## 🛡️ 会话保护:国内 AI 平台强制直连
214
+
215
+ 代理节点一抖,走代理的长连接就会断;而这个 Harness 自己的模型接口**全都是国内服务**,本来就不该走代理。这件事需要两层 —— 因为工具型插件碰不到 LLM 长连接。
216
+
217
+ ### 第一层:下载路径(插件内核,默认开)
218
+
219
+ 内置下列平台的域名,转成 `DOMAIN-SUFFIX,<域名>,DIRECT` 并**置于最高优先级** —— 高于订阅自带规则、高于你写的 `extraRules`,**连 `domesticDirect=false`(全部走代理)模式下也照样直连**:
220
+
221
+ > DeepSeek · 智谱/Z.ai/ChatGLM · 月之暗面 Kimi · 阿里通义/DashScope · 字节豆包/火山方舟 · 百度文心/千帆 · 腾讯混元 · 讯飞星火 · MiniMax · 零一万物 · 阶跃星辰 · 商汤日日新 · 百川智能 · 硅基流动 · 昆仑万维天工 · 华为云盘古/ModelArts · 网易有道 · 澜舟 · 元象 · 面壁 · 出门问问 · 国家超算/启智
222
+
223
+ 清单见 [`lib/core/ai-domains.js`](lib/core/ai-domains.js)。表里不存在的条目最多只是不匹配,没有副作用;用 `extraDirectDomains` 可以继续加自己的域名或完整 URL。
224
+
225
+ ### 第二层:Harness 自身的模型连接(点一下)
226
+
227
+ 真正决定模型流量走哪的是 DSH 启动期的全局代理策略(`$DSH_HOME/.env` 里的 `HTTP_PROXY` / `HTTPS_PROXY`)。代理程序一关,走它的连接就全断 —— 会话当场终止。
228
+
229
+ 用 `dsh_session_guard` 工具(或设置页里的「会话保护」卡片)修:
230
+
231
+ | 动作 | 效果 |
232
+ |---|---|
233
+ | `check`(默认) | 只读:报告该文件有没有代理变量、AI 域名在 `NO_PROXY` 里的覆盖情况与缺口 |
234
+ | `apply` | 把缺失域名合并进 `NO_PROXY`。**写之前自动备份**;**只改 `NO_PROXY` 这一行**,绝不动 `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY`;无变化时不写 |
235
+ | `restore` | 用最新备份还原;没有备份就明确报错 |
236
+
237
+ 每个域名同时写 `d` 与 `.d` 两种写法(DSH 匹配裸后缀,老版 curl/git 认前导点),其它行与原有换行符原样保留。
238
+
239
+ > `apply` 之后**需要重启 DSH** —— 那个文件是启动期读一次的,工具返回值里也带这条提醒。
240
+
241
+ ## ⚙️ 配置
242
+
243
+ 上游按下面的顺序解析,命中即用:
244
+
245
+ | 顺序 | 模式 | 条件 |
246
+ |---|---|---|
247
+ | 1 | `subscription` | 有「节点配置」或填了 `subscriptionUrl` —— **自包含,不需要本机 Clash**,并且能选节点 |
248
+ | 2 | `explicit` | 填了 `proxyUrl`(`http://` / `socks5://`),或订阅拿不到节点时的兜底 |
249
+ | 3 | `environment` | 都没填,但 `$HTTPS_PROXY` / `$HTTP_PROXY` 有值(就是 `.env` 里那份) |
250
+ | 4 | `none` | 都没有:境外目标直接报错并给出可操作提示,境内目标仍可直连下载 |
251
+
252
+ > **为什么订阅优先**:订阅给的是一份**可选节点的列表**,`proxyUrl` 只是一条固定上游。两者都填时若让 `proxyUrl` 赢,就会出现"填了订阅却没反应、也没有节点可选"——这是 v0.3.1 及以前的实际行为,v0.4.0 起修正。
253
+
254
+ | 字段 | 默认 | 说明 |
255
+ |---|---|---|
256
+ | `enabled` | `true` | 总开关 |
257
+ | `proxyUrl` | 空 | 一条固定上游;**两种情况下才生效**:抓订阅时当代理用、以及订阅拿不到节点时兜底 |
258
+ | `subscriptionUrl` | 空 | 单条订阅地址(同「节点配置」的导入源);**优先级高于 `proxyUrl`** |
259
+ | `fetchProxyUrl` | 空 | 只用于抓订阅本身(订阅被墙时填,必须是 `http(s)://`) |
260
+ | `autoUpdateHours` | `24` | 旧版单条订阅的自动更新间隔,也是新导入配置的默认间隔;`0` 关闭 |
261
+ | `groupType` | `url-test` | `url-test` 自动最快 / `select` 手动 / `fallback` 失败切换 |
262
+ | `preferredNode` | 空 | 按名字固定一个节点 |
263
+ | `latencyTestUrl` / `latencyTimeoutMs` | gstatic 204 / `3000` | 节点健康检查 URL 与超时 |
264
+ | `domesticDirect` | `true` | 关掉则所有下载都走代理(**AI 平台仍直连**) |
265
+ | `protectAiPlatforms` | `true` | 国内 AI 平台域名强制直连,最高优先级 |
266
+ | `extraDirectDomains` | `[]` | 追加强制直连的域名(支持完整 URL 与 `*.x.com`) |
267
+ | `guardShellDownloads` | `true` | 拦住没走代理的境外 shell 下载,提示改用 `dsh_run_proxied` |
268
+ | `extraRules` / `excludeRules` | `[]` | 追加 / 剔除规则行 |
269
+ | `downloadDir` | 空 | 空 = `$DSH_HOME/downloads` |
270
+ | `maxDownloadMb` | `512` | 单文件大小上限 |
271
+ | `downloadTimeoutS` | `600` | 单次下载总超时 |
272
+ | `stallTimeoutS` | `30` | 无数据停滞多久判定卡死 |
273
+ | `threads` | `4` | 多线程下载的分片连接数;`1` = 单连接。服务端不支持 `Range`、响应不可信、或文件 < 1 MiB 时自动回退单连接 |
274
+ | `insecureTls` | `false` | 跳过 TLS 校验(企业 TLS 解密环境才开) |
275
+ | `allowOutsideWorkspace` | `false` | 允许 `save_path` 写到工作区与下载目录之外 |
276
+ | `maxRedirects` | `10` | 重定向上限 |
277
+
278
+ 数据目录:`$DSH_HOME/dsh-downloader/` —— `profiles.json`(配置清单)+ `profiles/<id>.yaml`(每份配置正文),以及旧版单条订阅的 `subscription.yaml` 缓存与选中节点的 `state.json`。
279
+
280
+ ## 🧰 工具
281
+
282
+ | 工具 | 作用 |
283
+ |---|---|
284
+ | `dsh_download` | 下载文件。`url` 必填;可选 `save_path` / `overwrite` / `force_route` / `max_mb` / `timeout_seconds`。返回 `saved_to`、`bytes`、`sha256`、`speed_bps`、`route`、`route_reason`、`fallback_used`。 |
285
+ | `dsh_proxy_status` | 上游模式、订阅状态、节点数与延迟、选中节点、内核端口、AI 保护状态、会话守卫状态、最近错误。 |
286
+ | `dsh_geo_check` | 判定某个 URL / 主机走直连还是代理。**优先用内置离线规则库(不发任何网络请求)**,只有规则判不出来时才做 DNS + 在线 GeoIP + ping。 |
287
+ | `dsh_session_guard` | 检查 / 写入 / 还原 `$DSH_HOME/.env` 的 `NO_PROXY`(见上文「会话保护」)。 |
288
+ | `dsh_profile` | 管理节点配置(FlClash 式):列出 / 从订阅地址或 `clash://` 深链导入 / 从配置正文导入 / 更新 / 选择 / 删除 / 开关自动更新 / 排序 / 改名。 |
289
+ | `dsh_run_proxied` | 让 `git` / `curl` 这类 shell 下载走本插件的代理(只作用于这一次调用,仍走正常 shell 工具与权限策略)。 |
290
+
291
+ 另注册一个运行时技能 **`smart-download`**:让模型在"下载互联网文件"类任务上优先用 `dsh_download`,而不是 `curl`。
292
+
293
+ ### 路由语义
294
+
295
+ - **判定是离线的**:内置 11 万条国内域名后缀 + 8.7K 条国内 IP 段,加上订阅自带规则与你的 `extraRules`,按 `DOMAIN` / `DOMAIN-SUFFIX` / `DOMAIN-KEYWORD` / `IP-CIDR` / `REJECT` 匹配。不发网络请求,不怕被墙。
296
+ - **国内 AI 平台域名永远直连**:以最高优先级插在所有规则之前,用户规则也覆盖不掉(除非关掉 `protectAiPlatforms`)。
297
+ - **loopback 与私有地址永不进代理**(`127.0.0.0/8`、`::1`、`.local`、内网段),即使 `force_route=proxy`。
298
+ - 判定为 `direct` 时**完全不经内核**,直接连;判定为 `proxy` 时经本地内核 → 节点。
299
+ - 直连失败且上游可用时,**自动改用代理重试一次**,返回值里 `fallback_used: true`。
300
+
301
+ ### 完整性语义
302
+
303
+ - 先写 `<目标>.part`,完成才原子重命名;**任何失败路径都会删掉 `.part`**。
304
+ - 边下边算 sha256、边下边卡大小上限、无数据停滞超时检测、全程可被 `exec.signal` 取消。
305
+ - 工具的宿主超时被刻意省略(DSH 的 `timeoutMs` 是 opt-in),所以大文件不会被框架掐断,只受 `downloadTimeoutS` 约束。
306
+
307
+ ## 🌐 节点协议支持
308
+
309
+ | 协议 | 支持 |
310
+ |---|---|
311
+ | `socks5` / `http` | ✅ |
312
+ | `ss`(aes-128-gcm / aes-256-gcm / chacha20-ietf-poly1305) | ✅ |
313
+ | `trojan`(tcp / tls / ws) | ✅ |
314
+ | `vless`(tcp / tls / ws) | ✅ |
315
+ | `vmess`(AEAD alterId=0,tcp / ws) | ✅ |
316
+ | `hysteria2`(含 `salamander` 混淆) | ✅ 自带 Go 原生连接器(Windows amd64,已打包) |
317
+ | `vless reality` | ✅ 同上 |
318
+ | `tuic` | ❌ |
319
+
320
+ **原生连接器已内置**:`lib/native/connector.exe`(自编译,含 hysteria2 的 salamander 混淆与 vless reality)。`dsh_proxy_status` 会报告 `原生连接器:已内置`。非 Windows-amd64 平台上这些节点会被自动跳过并计数,不影响其他节点。
321
+
322
+ ## 📥 支持哪些订阅格式
323
+
324
+ | 格式 | 说明 |
325
+ |---|---|
326
+ | Clash / mihomo YAML | `proxies` + `proxy-groups` + `rules`,完整支持 |
327
+ | **base64 包裹的 YAML** | 常见于 `?flag=clash` 之类的接口 |
328
+ | **分享链接列表**(base64 或明文) | 一行一个 `hysteria2://` / `ss://` / `trojan://` / `vmess://` / `vless://` / `socks5://` / `http(s)://`,最常见的机场格式之一 |
329
+
330
+ 分享链接会被转成与 Clash YAML 完全等价的节点对象,下游(规则引擎、传输层)一行都不用改。认不出的行会**计数并报告**(`订阅已加载 9 个节点(跳过 2 条)`),而不是静默丢弃。
331
+
332
+ > 带插件的 `ss`(`?plugin=…`)与 `tuic` 会被明确列为「不支持」,不会假装加载成功。
333
+
334
+
335
+ ## 🔗 与 DSH 内建代理的关系
336
+
337
+ - 本插件**从不写 `process.env`**,也**从不调 `setGlobalDispatcher`**,所以不存在"卸载后残留"或"和启动器策略互相覆盖"的问题。
338
+ - 内核模式下,下载流量是 `node:http` 直连 `127.0.0.1:<本地端口>`,不经过 undici 全局 dispatcher。
339
+ - 唯一间接接触:抓订阅用的是自带 HTTP 客户端;若 `fetchProxyUrl` 没填,会退而使用 `$HTTPS_PROXY` / `$HTTP_PROXY`(若它们存在)。
340
+
341
+ ## 🔧 工作原理
342
+
343
+ ```
344
+ 模型 → dsh_download
345
+ │
346
+ ├─ 1. 解析上游 explicit / environment / subscription / none
347
+ ├─ 2. 离线规则判定 内置国内域名表 + IP 段 + AI 平台保护 + 订阅规则 + extraRules
348
+ │
349
+ ├─ direct ──────────► 直接连目标
350
+ └─ proxy ──────────► 127.0.0.1:<随机端口> 本地混合代理
351
+ │
352
+ RuleEngine 决策
353
+ ├─ direct → 直连
354
+ └─ proxy → connectThrough(node)
355
+ ss / trojan / vless / vmess /
356
+ socks5 / http / hysteria2
357
+
358
+ 系统其他程序、DSH 自身的其它出网 ── 完全不受影响
359
+ ```
360
+
361
+ 内核代码(规则引擎、国内分流数据、本地混合代理、节点传输、订阅解析)派生自 `dsh-clash-proxy` 0.2.0(MIT),每个文件头部保留出处声明。本仓库在其之上**修复了一个真实的数据截断 bug**:`SocketBuffer.readAny()` 原先在连接关闭时先看 `#ended` 再看缓冲区,导致"已收到但消费者尚未取走"的尾部字节被静默丢弃 —— 实测 512 KiB 的响应只传出 384 KiB,即静默损坏的下载。详见 `lib/core/transports.js` 的 `[dsh-downloader]` 标记。
362
+
363
+ ## 🧪 开发与测试
364
+
365
+ ```bash
366
+ npm install
367
+ node test/smoke.mjs # 368 项:规则引擎 / 订阅解析 / 配置导入 / 路由 / HTTP 客户端 / 端到端下载 / 会话守卫 / 令牌鉴权
368
+ node --check lib/client.js
369
+ npm pack
370
+ ```
371
+
372
+ `test/smoke.mjs` **不需要外网**:它起一个本地 HTTP 夹具服务器,并让下载真正穿过插件自己的 `ProxyServer`,逐字节校验 sha256,同时覆盖大小上限、停滞检测、取消、404、重定向、`.part` 清理,以及"AI 域名在最严格规则下仍直连""没有令牌的连接被拒 407""apply 绝不新增代理变量"这类断言。
373
+
374
+ ## ⚠️ 已知限制
375
+
376
+ | 限制 | 说明 |
377
+ |---|---|
378
+ | 只覆盖本插件的下载 | 见文首的边界表;其它出网路径请用 `$DSH_HOME/.env` |
379
+ | `save_path` 的准入检查不等于沙箱 | 写入用的是 `node:fs` 流式写(`ctx.fs` 没有流式写接口),所以是"准入用 DSH 策略、实际写入是宿主写" |
380
+ | 缺省文件名按 URL 推断 | `Content-Disposition` 只在返回值里可见;需要精确命名时显式给 `save_path` |
381
+ | 无实时进度条 | DSH 工具没有 MCP 那种 `report()` 进度通道;进度只体现在最终返回值的 `bytes` / `speed_bps` |
382
+ | 原生连接器仅 Windows amd64 | `lib/native/connector.exe` 是 Windows amd64 二进制;其它平台会跳过 hysteria2 / reality 节点并计数 |
383
+ | 多实例共用同一数据目录 | 同一 `$DSH_HOME` 下多开 DSH 会共用配置清单、订阅缓存与选中节点 |
384
+ | 令牌模式下不接受 SOCKS5 入站 | SOCKS5 握手装不下令牌,所以该协议被直接拒绝;本插件自身走 HTTP CONNECT,不受影响 |
385
+ | 会话保护会写工作区外的文件 | 只在**显式**调 `dsh_session_guard action="apply"`(或点面板按钮)时写 `$DSH_HOME/.env`:写前自动备份、可 `restore`,且只改 `NO_PROXY` 这一行 |
386
+
387
+ ## 📄 License
388
+
389
+ MIT。内核部分派生自 `dsh-clash-proxy` 0.2.0(MIT),出处见各文件头部与 [LICENSE](LICENSE)。
390
+
391
+ 本插件仅提供技术能力,请确保你的代理服务与使用方式符合当地法律法规。
@@ -0,0 +1,9 @@
1
+ # dsh-downloader —— DSH bundle patch
2
+ # 安装后由 DSH 启动时按 bundle 层栈叠加,把下载代理插件插入 profile。
3
+ # 单条双面行:node 半边注册下载工具与订阅内核,client 半边
4
+ # (见 package.json 的 dsh.client)把管理面板挂进 settings.section。
5
+ # 这里刻意不声明 inject:timer / skills / webServer / settings 都是可选的,
6
+ # 由插件在 apply 里惰性 ctx.inject 解析,缺任一都不会让整插件停在 pending。
7
+ - insert:
8
+ - id: dsh-downloader
9
+ name: '@having5548/dsh-downloader'