dsh-dl-server 0.1.1 → 0.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.md CHANGED
@@ -17,14 +17,18 @@ A persistent download queue for DeepSeek Harness.
17
17
  Settings → Plugins → Install a third-party plugin → Add plugin
18
18
  ```
19
19
 
20
- Type **`dsh-dl-server`**, confirm, then **restart DSH once**.
20
+ Type **`dsh-dl-server@0.1.1`**, confirm, then **restart DSH once**.
21
21
 
22
22
  ### From the command line
23
23
 
24
24
  ```sh
25
- dsh plugin add dsh-dl-server
25
+ dsh plugin add dsh-dl-server@0.1.1
26
26
  ```
27
27
 
28
+ > **Pin the version.** `0.1.0` declared `@deepseek-ai/dsh-storage-domain` as a regular dependency, so pnpm installed a second copy inside the plugin; the injected service then no longer matched the host and the plugin never mounted (every service stayed `pending`). `0.1.1` moves it to `peerDependencies`.
29
+ >
30
+ > pnpm also caches registry metadata — without an explicit version you may still resolve `0.1.0`.
31
+
28
32
  > `dsh` must be on your PATH. The desktop build ships one at
29
33
  > `…\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd`; if you have never added it, use the UI path.
30
34
 
@@ -63,6 +67,17 @@ A download started in one session is visible to every other session — progress
63
67
  | `download_cancel` | Cancel, optionally dropping the partial file |
64
68
  | `download_cleanup` | Drop records (the default scope leaves downloaded files alone) |
65
69
 
70
+ ## New in 0.2.0
71
+
72
+ | | |
73
+ | --- | --- |
74
+ | **Mirror expansion** | GitHub Releases / HuggingFace / raw.githubusercontent / PyPI URLs gain a fallback mirror automatically. The original always stays first. |
75
+ | **Mirror cooldown** | Two consecutive failures on one host parks it for 10 minutes instead of retrying it on every job. |
76
+ | **Size-aware concurrency** | Under 1 MB skips aria2 entirely; 1–8 MB runs 4 connections; only 8 MB and up gets 16. |
77
+ | **Session fairness** | When two sessions queue at once they share the slots instead of one of them taking the whole queue. |
78
+ | **Cumulative disk budget** | Enqueue counts what already-queued jobs still need, so a batch cannot quietly fill the disk. |
79
+ | **Failures are no longer silent** | Checkpoint write errors are counted and surfaced in `download_status` under `storage`; an unwritable destination is refused before the transfer starts. |
80
+
66
81
  ## Example
67
82
 
68
83
  ```
package/README.zh.md CHANGED
@@ -17,18 +17,22 @@
17
17
  设置 → 插件 → 安装第三方插件 → 添加插件
18
18
  ```
19
19
 
20
- 输入框里填 **`dsh-dl-server`**,确认,然后**重启一次 DSH**。
20
+ 输入框里填 **`dsh-dl-server@0.1.1`**,确认,然后**重启一次 DSH**。
21
21
 
22
22
  ### 方式二:命令行
23
23
 
24
24
  ```sh
25
- dsh plugin add dsh-dl-server
25
+ dsh plugin add dsh-dl-server@0.1.1
26
26
  ```
27
27
 
28
+ > **带上 `@0.1.1`。** `0.1.0` 把 `@deepseek-ai/dsh-storage-domain` 声明成了普通依赖,pnpm 会给插件装第二份,服务和宿主对不上、插件挂不住(全部 `pending`)。0.1.1 改成 `peerDependencies` 已修复。
29
+ >
30
+ > 另外 pnpm 会缓存 registry 元数据 —— 不带版本号时可能仍然解析到 `0.1.0`。
31
+
28
32
  > `dsh` 需要先在 PATH 里。桌面版自带的那份在
29
33
  > `…\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd`,没加过 PATH 的话用方式一。
30
34
 
31
- **要求 DSH 0.2.1-rc.5 或以上。**
35
+ **要求 DSH 0.2.1 或以上。**
32
36
  **aria2 可选** —— 装了走 16 线程分段,没装走单流兜底,一样能下,只是慢一些。
33
37
 
34
38
  ---
@@ -63,6 +67,17 @@ DLS 做的是**队列层**,不是传输层。
63
67
  | `download_cancel` | 取消,可选删掉半截文件 |
64
68
  | `download_cleanup` | 清记录(默认不动已下载的文件) |
65
69
 
70
+ ## 0.2.0 新增
71
+
72
+ | 能力 | 说明 |
73
+ | --- | --- |
74
+ | **镜像表** | GitHub Releases / HuggingFace / raw.githubusercontent / PyPI 的地址自动多带一个镜像,原地址永远排第一 |
75
+ | **镜像冷却** | 同一 host 连续失败两次就冷却 10 分钟,不再每次撞 |
76
+ | **并发分档** | < 1 MB 不进 aria2;1–8 MB 开 4 连接;≥ 8 MB 才 16 |
77
+ | **会话级公平** | 两个会话同时排队时各占一半槽位,一个会话吃不下整条队列 |
78
+ | **磁盘余量累加** | 入队时把「已在队列里还没下完的」一起算进去,批量入队写不满盘 |
79
+ | **失败不再静默** | 断点写失败会计数并通过 `download_status` 的 `storage` 字段暴露;目标目录不可写在传输开始前就拒绝 |
80
+
66
81
  ## 用它
67
82
 
68
83
  ```
package/lib/domain.js CHANGED
@@ -10,6 +10,10 @@ const JobRecordSchema = z.object({
10
10
  origin: z.string().optional(),
11
11
 
12
12
  urls: z.array(z.string()).min(1),
13
+ /** Mirrors derived from the original URL at enqueue time, for diagnostics. */
14
+ mirrors: z
15
+ .array(z.object({ from: z.string(), to: z.string(), rule: z.string() }))
16
+ .optional(),
13
17
  destPath: z.string(),
14
18
 
15
19
  state: z.enum(['queued', 'running', 'paused', 'completed', 'failed', 'canceled']),
package/lib/mirrors.js ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Mirror expansion for well-known hosts.
3
+ *
4
+ * Pure functions only: no network, no state. The point is to turn one URL into
5
+ * a small ordered list so the transport can fall back when the origin is slow or
6
+ * blocked — the original URL always stays first.
7
+ *
8
+ * What this deliberately does NOT do: prove that a mirror serves the same bytes.
9
+ * A third-party mirror can return an older build, an HTML error page, or worse.
10
+ * Only `expectSha256` can prove that, and it is checked after the download. The
11
+ * mitigation here is the cooldown in `queue.js`: a host that fails twice stops
12
+ * being tried for a while.
13
+ */
14
+
15
+ /** @typedef {{ from: string, to: string, rule: string }} MirrorApplication */
16
+
17
+ const RULES = [
18
+ {
19
+ name: 'huggingface',
20
+ test: (u) => u.hostname === 'huggingface.co',
21
+ rewrite: (u) => {
22
+ const out = new URL(u.href)
23
+ out.hostname = 'hf-mirror.com'
24
+ return out.href
25
+ },
26
+ },
27
+ {
28
+ name: 'github-release',
29
+ test: (u) => u.hostname === 'github.com' && /\/releases\/download\//.test(u.pathname),
30
+ rewrite: (u) => `https://ghproxy.net/${u.href}`,
31
+ },
32
+ {
33
+ name: 'raw-githubusercontent',
34
+ test: (u) => u.hostname === 'raw.githubusercontent.com',
35
+ rewrite: (u) => {
36
+ const out = new URL(u.href)
37
+ out.hostname = 'raw.gitmirror.com'
38
+ return out.href
39
+ },
40
+ },
41
+ {
42
+ name: 'pythonhosted',
43
+ test: (u) => u.hostname === 'files.pythonhosted.org',
44
+ rewrite: (u) => {
45
+ const out = new URL(u.href)
46
+ out.hostname = 'pypi.tuna.tsinghua.edu.cn'
47
+ return out.href
48
+ },
49
+ },
50
+ ]
51
+
52
+ /** Hosts we already treat as mirrors; expanding them again would recurse. */
53
+ const ALREADY_MIRROR = new Set(['hf-mirror.com', 'ghproxy.net', 'raw.gitmirror.com', 'pypi.tuna.tsinghua.edu.cn'])
54
+
55
+ /**
56
+ * @param {string[]} urls
57
+ * @returns {{ urls: string[], applied: MirrorApplication[] }}
58
+ */
59
+ export function expandMirrors(urls) {
60
+ const out = []
61
+ const seen = new Set()
62
+ /** @type {MirrorApplication[]} */
63
+ const applied = []
64
+
65
+ const push = (url) => {
66
+ if (seen.has(url)) return
67
+ seen.add(url)
68
+ out.push(url)
69
+ }
70
+
71
+ for (const url of urls) {
72
+ push(url)
73
+
74
+ let parsed
75
+ try {
76
+ parsed = new URL(url)
77
+ } catch {
78
+ continue
79
+ }
80
+ if (ALREADY_MIRROR.has(parsed.hostname)) continue
81
+
82
+ for (const rule of RULES) {
83
+ if (!rule.test(parsed)) continue
84
+ const mirror = rule.rewrite(parsed)
85
+ if (seen.has(mirror)) continue
86
+ push(mirror)
87
+ applied.push({ from: url, to: mirror, rule: rule.name })
88
+ break
89
+ }
90
+ }
91
+
92
+ return { urls: out, applied }
93
+ }
94
+
95
+ /** Hostname of a URL, or `undefined` when it does not parse. */
96
+ export function hostOf(url) {
97
+ try {
98
+ return new URL(url).hostname
99
+ } catch {
100
+ return undefined
101
+ }
102
+ }