dsh-dl-server 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +46 -2
- package/README.zh.md +45 -0
- package/cordis.patch.yml +16 -0
- package/lib/bootlog.js +31 -0
- package/lib/domain.js +51 -0
- package/lib/index.js +92 -0
- package/lib/queue.js +591 -0
- package/lib/tools.js +216 -0
- package/lib/transport.js +357 -0
- package/lib/types.js +54 -0
- package/package.json +55 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jestions
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-dl-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
**DLS** — DSH Download Server
|
|
6
|
+
A global download service for DeepSeek Harness: visible across sessions, owned per session, persistent queue, pausable and resumable.
|
|
7
|
+
|
|
8
|
+
> DLS is all you need
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
Every existing DSH downloader is one tool function that blocks until the file lands. They make downloads **fast**; they don't solve three things:
|
|
15
|
+
|
|
16
|
+
- the download dies when the session or the turn ends
|
|
17
|
+
- there is no queue, no pause, no resume
|
|
18
|
+
- nothing survives a restart
|
|
19
|
+
|
|
20
|
+
DLS is the **queue layer**, not the transport layer.
|
|
21
|
+
|
|
22
|
+
## Status
|
|
23
|
+
|
|
24
|
+
`v0.1` — scaffold. Design: [SPEC.md](SPEC.md).
|
|
25
|
+
|
|
26
|
+
| Module | State |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| Data model | done |
|
|
29
|
+
| Persistent domain | done |
|
|
30
|
+
| Transport (aria2 JSON-RPC + single-stream fallback) | done |
|
|
31
|
+
| Queue engine (scheduling, ownership, pause reasons, checkpoints, recovery) | done |
|
|
32
|
+
| Seven tools | done |
|
|
33
|
+
| Real DSH install / verification | done (DSH 0.2.1-rc.5) |
|
|
34
|
+
| Hermetic tests (`npm test`) | done |
|
|
35
|
+
| User panel (v3) | later |
|
|
36
|
+
|
|
37
|
+
## Tools (planned)
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
download_enqueue download_status download_wait
|
|
41
|
+
download_pause download_resume download_cancel
|
|
42
|
+
download_cleanup
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# dsh-dl-server
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
**DLS** — DSH Download Server
|
|
6
|
+
大肥鱼自己的下载服务器插件
|
|
7
|
+
|
|
8
|
+
> DLS is all you need
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 为什么
|
|
13
|
+
|
|
14
|
+
现成的 DSH 下载插件都是「一个工具函数 + 阻塞等到下完」。它们让下载**变快**,但没解决三件事:
|
|
15
|
+
|
|
16
|
+
- 会话或回合结束,下载就断了
|
|
17
|
+
- 不能排队、不能暂停恢复
|
|
18
|
+
- 进程重启,什么都没了
|
|
19
|
+
|
|
20
|
+
DLS 做的是**队列层**,不是传输层。
|
|
21
|
+
|
|
22
|
+
## 状态
|
|
23
|
+
|
|
24
|
+
`v0.1` 脚手架阶段。完整设计见 [SPEC.md](SPEC.md)。
|
|
25
|
+
|
|
26
|
+
| 模块 | 状态 |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| 数据模型 | ✅ |
|
|
29
|
+
| 持久化 domain | ✅ |
|
|
30
|
+
| `download_status` | 最小实现 |
|
|
31
|
+
| 队列引擎 | 待做 |
|
|
32
|
+
| 传输层(aria2 / Node 兜底) | 待做 |
|
|
33
|
+
| 其余六个工具 | 待做 |
|
|
34
|
+
|
|
35
|
+
## 工具(规划)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
download_enqueue download_status download_wait
|
|
39
|
+
download_pause download_resume download_cancel
|
|
40
|
+
download_cleanup
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 许可
|
|
44
|
+
|
|
45
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# dsh-dl-server bundle patch
|
|
2
|
+
- insert:
|
|
3
|
+
- id: dsh-dl-server
|
|
4
|
+
name: 'dsh-dl-server'
|
|
5
|
+
config:
|
|
6
|
+
maxConcurrentJobs: 3
|
|
7
|
+
connectionsPerJob: 16
|
|
8
|
+
aria2Path: ''
|
|
9
|
+
aria2Port: 16801
|
|
10
|
+
speedLimitKbps: 0
|
|
11
|
+
proxy: ''
|
|
12
|
+
defaultDownloadDir: ''
|
|
13
|
+
checkpointSeconds: 30
|
|
14
|
+
autoResumeOnSessionReopen: true
|
|
15
|
+
autoResumeOnHostRestart: true
|
|
16
|
+
keepCompletedRecords: 200
|
package/lib/bootlog.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal boot trace. Overwritten every boot, so it always describes the latest
|
|
7
|
+
* start. A support report is "send me %TEMP%/dls-boot.log".
|
|
8
|
+
*/
|
|
9
|
+
export const BOOT_LOG = path.join(os.tmpdir(), 'dls-boot.log')
|
|
10
|
+
|
|
11
|
+
let started = false
|
|
12
|
+
|
|
13
|
+
/** @param {string} step @param {string} [detail] */
|
|
14
|
+
export function mark(step, detail) {
|
|
15
|
+
try {
|
|
16
|
+
if (!started) {
|
|
17
|
+
fs.writeFileSync(BOOT_LOG, `dsh-dl-server boot trace\n`)
|
|
18
|
+
started = true
|
|
19
|
+
}
|
|
20
|
+
const line = `${new Date().toISOString()} ${step}${detail ? ` ${detail}` : ''}`
|
|
21
|
+
fs.appendFileSync(BOOT_LOG, `${line}\n`)
|
|
22
|
+
} catch {
|
|
23
|
+
/* tracing must never break the plugin */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @param {string} step @param {unknown} error */
|
|
28
|
+
export function markFailure(step, error) {
|
|
29
|
+
const stack = error instanceof Error ? (error.stack ?? error.message) : String(error)
|
|
30
|
+
mark(`FAILED at ${step}`, stack.split('\n').slice(0, 6).join(' | '))
|
|
31
|
+
}
|
package/lib/domain.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'
|
|
3
|
+
|
|
4
|
+
const JobRecordSchema = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
seq: z.number().int().nonnegative(),
|
|
7
|
+
|
|
8
|
+
ownerWorkspace: z.string(),
|
|
9
|
+
ownerSession: z.string(),
|
|
10
|
+
origin: z.string().optional(),
|
|
11
|
+
|
|
12
|
+
urls: z.array(z.string()).min(1),
|
|
13
|
+
destPath: z.string(),
|
|
14
|
+
|
|
15
|
+
state: z.enum(['queued', 'running', 'paused', 'completed', 'failed', 'canceled']),
|
|
16
|
+
pauseReason: z.enum(['user', 'session_closed', 'host_restart']).optional(),
|
|
17
|
+
|
|
18
|
+
createdAt: z.number(),
|
|
19
|
+
updatedAt: z.number(),
|
|
20
|
+
|
|
21
|
+
bytesTotal: z.number(),
|
|
22
|
+
bytesCheckpoint: z.number(),
|
|
23
|
+
|
|
24
|
+
attempts: z.number().int().nonnegative(),
|
|
25
|
+
lastError: z.string().optional(),
|
|
26
|
+
|
|
27
|
+
expectSha256: z.string().optional(),
|
|
28
|
+
finalBytes: z.number().optional(),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The persistent queue domain.
|
|
33
|
+
*
|
|
34
|
+
* per-record: one corrupt job must not make the whole queue unreadable.
|
|
35
|
+
* backup-and-skip: move the bad document aside and keep booting.
|
|
36
|
+
* global.nextSeq: the global counter behind J# ids.
|
|
37
|
+
*/
|
|
38
|
+
export const JobDomain = defineDomain({
|
|
39
|
+
// Domain names must match /^[a-z][a-z0-9_]*$/ — hyphens are rejected at load.
|
|
40
|
+
name: 'dsh_dl_jobs',
|
|
41
|
+
version: 1,
|
|
42
|
+
layout: 'per-record',
|
|
43
|
+
invalidRecords: 'backup-and-skip',
|
|
44
|
+
tables: {
|
|
45
|
+
jobs: domainTable(JobRecordSchema),
|
|
46
|
+
},
|
|
47
|
+
global: {
|
|
48
|
+
initial: { nextSeq: 1 },
|
|
49
|
+
schema: z.object({ nextSeq: z.number().int().positive() }),
|
|
50
|
+
},
|
|
51
|
+
})
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { JobDomain } from './domain.js'
|
|
2
|
+
import { Queue } from './queue.js'
|
|
3
|
+
import { Transport } from './transport.js'
|
|
4
|
+
import { registerTools } from './tools.js'
|
|
5
|
+
import { mark, markFailure } from './bootlog.js'
|
|
6
|
+
|
|
7
|
+
export const name = 'dsh-dl-server'
|
|
8
|
+
export const inject = ['tools', 'storageDomain']
|
|
9
|
+
|
|
10
|
+
const DEFAULTS = Object.freeze({
|
|
11
|
+
maxConcurrentJobs: 3,
|
|
12
|
+
connectionsPerJob: 16,
|
|
13
|
+
aria2Path: '',
|
|
14
|
+
aria2Port: 16801,
|
|
15
|
+
speedLimitKbps: 0,
|
|
16
|
+
proxy: '',
|
|
17
|
+
defaultDownloadDir: '',
|
|
18
|
+
checkpointSeconds: 30,
|
|
19
|
+
autoResumeOnSessionReopen: true,
|
|
20
|
+
autoResumeOnHostRestart: true,
|
|
21
|
+
keepCompletedRecords: 200,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Use the host logger when one is available so INFO lines never land on stdout.
|
|
26
|
+
* @param {any} ctx
|
|
27
|
+
*/
|
|
28
|
+
function makeLog(ctx) {
|
|
29
|
+
try {
|
|
30
|
+
const factory = ctx?.logger
|
|
31
|
+
if (typeof factory === 'function') {
|
|
32
|
+
const logger = factory('dsh-dl-server')
|
|
33
|
+
if (logger && typeof logger.info === 'function') {
|
|
34
|
+
return (level, message) => {
|
|
35
|
+
const fn = logger[level] ?? logger.info
|
|
36
|
+
fn.call(logger, message)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
/* fall through to the console fallback */
|
|
42
|
+
}
|
|
43
|
+
return (level, message) => {
|
|
44
|
+
// DLS_DEBUG=1 surfaces INFO lines on stderr; useful when no host logger is wired.
|
|
45
|
+
if (process.env.DLS_DEBUG === '1' || level === 'warn' || level === 'error') {
|
|
46
|
+
console.error(`[dsh-dl-server] ${message}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
53
|
+
* @param {Partial<typeof DEFAULTS>} [rawConfig]
|
|
54
|
+
*/
|
|
55
|
+
export async function apply(ctx, rawConfig) {
|
|
56
|
+
mark('apply enter')
|
|
57
|
+
|
|
58
|
+
const config = { ...DEFAULTS, ...(rawConfig ?? {}) }
|
|
59
|
+
const log = makeLog(ctx)
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
mark('opening domain')
|
|
63
|
+
const domain = await ctx.storageDomain.open(JobDomain)
|
|
64
|
+
mark('domain open')
|
|
65
|
+
|
|
66
|
+
const transport = new Transport(config, log)
|
|
67
|
+
await transport.init()
|
|
68
|
+
mark('transport ready', `mode=${transport.mode}`)
|
|
69
|
+
|
|
70
|
+
const queue = new Queue({ domain, transport, config, log })
|
|
71
|
+
await queue.recover()
|
|
72
|
+
mark('queue ready')
|
|
73
|
+
|
|
74
|
+
registerTools({ ctx, queue, config })
|
|
75
|
+
// The tools themselves register inside the deferred ctx.inject callback;
|
|
76
|
+
// this mark only records that the subscription was installed.
|
|
77
|
+
mark('registerTools dispatched')
|
|
78
|
+
|
|
79
|
+
ctx.effect(() => async () => {
|
|
80
|
+
await queue.close()
|
|
81
|
+
await transport.dispose()
|
|
82
|
+
await domain.close()
|
|
83
|
+
})
|
|
84
|
+
mark('effect installed')
|
|
85
|
+
|
|
86
|
+
log('info', `ready (transport: ${transport.mode}, concurrency: ${config.maxConcurrentJobs})`)
|
|
87
|
+
mark('apply done')
|
|
88
|
+
} catch (error) {
|
|
89
|
+
markFailure('apply', error)
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
}
|