@motrix/mdxp 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +341 -53
  2. package/README.zh-CN.md +375 -0
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -1,103 +1,391 @@
1
1
  # @motrix/mdxp
2
2
 
3
- Motrix Download eXchange Protocol (MDXP) v1.0 — JSON-RPC 2.0 wire types, Zod schemas, and bidirectional connection helpers shared between the Motrix desktop app and the Motrix browser extension. Both live under the [motrixapp](https://github.com/motrixapp) organization.
3
+ [![npm version](https://img.shields.io/npm/v/@motrix/mdxp.svg)](https://www.npmjs.com/package/@motrix/mdxp)
4
+ [![license](https://img.shields.io/npm/l/@motrix/mdxp.svg)](./LICENSE)
5
+ [![types](https://img.shields.io/npm/types/@motrix/mdxp.svg)](./dist/index.d.ts)
4
6
 
5
- 完整协议规范见 `motrix-extension` 仓库的 `docs/01-protocol-mdxp.md`。
7
+ **English** | [简体中文](./README.zh-CN.md)
6
8
 
7
- ## 安装
9
+ > **MDXP** (Motrix Download eXchange Protocol) — the JSON-RPC 2.0 wire types, Zod
10
+ > schemas, and bidirectional connection helper that let a browser, CLI, or AI
11
+ > agent hand downloads to a Motrix desktop downloader over any duplex transport.
12
+
13
+ `@motrix/mdxp` is the single source of truth for the MDXP wire contract. Both
14
+ sides of the bridge — the [Motrix](https://github.com/agalwood/Motrix) desktop app
15
+ (the **server**, which owns the download engine) and its **clients** (the
16
+ browser extension, a CLI, or an agent) — depend on this package so the protocol
17
+ shape is defined exactly once.
18
+
19
+ It ships nothing transport-specific: you bring any
20
+ [`vscode-jsonrpc`](https://www.npmjs.com/package/vscode-jsonrpc)
21
+ `MessageReader`/`MessageWriter` pair (stdio, a socket, a WebSocket, a
22
+ `MessagePort`) and the library builds a fully typed, bidirectional connection on
23
+ top of it.
24
+
25
+ ## Highlights
26
+
27
+ - **Schema-first.** Every wire shape is a [Zod](https://zod.dev) schema; the
28
+ TypeScript types are `z.infer` of those schemas, so validation and types can
29
+ never drift apart.
30
+ - **Fully typed connection.** `sendRequest`/`onRequest`/`sendNotification`/
31
+ `onNotification` are generic over the method name — params and result types
32
+ are inferred from that name, with no casts at the call site.
33
+ - **Transport-agnostic.** Works over anything that implements
34
+ `MessageReader`/`MessageWriter`.
35
+ - **Platform RAL entry points.** `./node` and `./browser` install the matching
36
+ `vscode-jsonrpc` runtime abstraction layer for you.
37
+ - **Agent-ready.** A built-in tool registry emits a JSON-Schema tool catalog you
38
+ can feed straight into an LLM function-calling API.
39
+ - **Forward-compatible.** Unknown methods and fields are ignored, not rejected.
40
+
41
+ ## Installation
8
42
 
9
43
  ```bash
10
- pnpm add @motrix/mdxp
44
+ npm install @motrix/mdxp
45
+ # or: pnpm add @motrix/mdxp · yarn add @motrix/mdxp
11
46
  ```
12
47
 
13
- ## 用法
48
+ Runtime dependency: [`vscode-jsonrpc`](https://www.npmjs.com/package/vscode-jsonrpc)
49
+ `^9`, a direct dependency installed alongside this package. The examples below
50
+ also import reader/writer classes straight from `vscode-jsonrpc/node` and
51
+ `vscode-jsonrpc/browser`; if you do the same, declare `vscode-jsonrpc` in your
52
+ own dependencies rather than relying on hoisting. ESM-only; requires
53
+ Node.js ≥ 18 or a modern bundler.
14
54
 
15
- ### 创建一个连接
55
+ ### Entry points
56
+
57
+ | Import | Installs a RAL? | Use it from |
58
+ | --- | --- | --- |
59
+ | `@motrix/mdxp` | No — platform-agnostic core | Shared code, tests, type-only imports |
60
+ | `@motrix/mdxp/node` | Node RAL | A Node host (Electron main, a CLI, a native-messaging host) |
61
+ | `@motrix/mdxp/browser` | Browser RAL | A browser host (extension service worker, page) |
62
+
63
+ `vscode-jsonrpc` v9 requires a runtime abstraction layer (RAL) to be installed
64
+ before a connection can be created. Importing `@motrix/mdxp/node` or
65
+ `@motrix/mdxp/browser` installs the right one into the **same** `vscode-jsonrpc`
66
+ instance this package uses, and re-exports the entire public API — so a host
67
+ imports everything from a single place.
68
+
69
+ ## Quick start
70
+
71
+ ### Node host (over stdio)
16
72
 
17
73
  ```ts
18
- import { createMdxpConnection } from '@motrix/mdxp'
74
+ import { createMdxpConnection } from '@motrix/mdxp/node'
19
75
  import { StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node'
20
76
 
21
77
  const conn = createMdxpConnection(
22
78
  new StreamMessageReader(process.stdin),
23
- new StreamMessageWriter(process.stdout)
79
+ new StreamMessageWriter(process.stdout),
24
80
  )
25
81
 
26
- // 注册 handlers BEFORE listen
27
- conn.onRequest('url/resolve', async (params) => {
28
- // ... 返回 typed UrlResolveResult
82
+ // Register handlers BEFORE listen().
83
+ conn.onNotification('$/task/progress', (p) => {
84
+ const pct = p.bytesTotal ? Math.round((p.bytesDone / p.bytesTotal) * 100) : null
85
+ console.log(`[${p.taskId}] ${p.phase} ${pct ?? '?'}% @ ${p.speedBps} B/s`)
29
86
  })
30
87
 
31
- conn.onNotification('$/task/progress', (params) => {
32
- // ...
33
- })
88
+ conn.listen()
89
+ ```
90
+
91
+ ### Browser host
92
+
93
+ ```ts
94
+ // Installs the browser RAL, then re-exports the full API.
95
+ import { createMdxpConnection } from '@motrix/mdxp/browser'
96
+ import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-jsonrpc/browser'
34
97
 
98
+ // e.g. a MessagePort / Worker; adapt your WebSocket to reader/writer as needed.
99
+ const conn = createMdxpConnection(
100
+ new BrowserMessageReader(worker),
101
+ new BrowserMessageWriter(worker),
102
+ )
35
103
  conn.listen()
36
104
  ```
37
105
 
38
- ### 发送 request
106
+ ## Core concepts
107
+
108
+ **Server vs. client.** The Motrix desktop app is the **server** — it owns the
109
+ download engine. A **client** is whatever drives it: the browser extension, a
110
+ CLI, or an agent. The connection is symmetric, but methods flow in a defined
111
+ direction (below).
112
+
113
+ **The handshake comes first.** `motrix/initialize` MUST be the first message of
114
+ every session. It negotiates the protocol version, exchanges identity, and
115
+ declares capabilities. Nothing else should be sent until it resolves.
116
+
117
+ **Message direction.** Most methods are client→server (the client asks the
118
+ downloader to do something). Two are server→client — the server asks the client
119
+ to inspect a page: `url/probe` and `url/resolve` (see
120
+ [`SERVER_INITIATED_METHODS`](#api-reference)). Because the server initiates both,
121
+ a client answers them with `onRequest`, while the server side calls them with
122
+ `sendRequest`.
123
+
124
+ ## Usage
125
+
126
+ ### Handshake
39
127
 
40
128
  ```ts
41
- const result = await conn.sendRequest('url/resolve', {
42
- url: 'https://www.youtube.com/watch?v=...',
43
- preferences: { maxQuality: '1080p' },
129
+ const hello = await conn.sendRequest('motrix/initialize', {
130
+ protocolVersion: '1.0',
131
+ client: {
132
+ kind: 'cli', // or 'extension'
133
+ name: 'my-download-agent',
134
+ version: '1.0.0',
135
+ locale: 'en-US',
136
+ },
137
+ capabilities: { submitDownload: true, progress: true, cancellation: true },
138
+ adapters: [], // page adapters this client can resolve, if any
44
139
  })
45
- // result 类型自动推断为 UrlResolveResult
140
+
141
+ console.log(hello.server.name, hello.server.version)
142
+ console.log(hello.capabilities.selectionKinds) // e.g. ['direct', 'hls', 'mux']
46
143
  ```
47
144
 
48
- ### 发送 notification
145
+ ### Add a download (client → server)
146
+
147
+ `download/add` is the public, agent-facing entry point. It accepts a direct
148
+ URL list, a magnet link, or a base64 torrent, and returns the created task
149
+ snapshot so you can render it without polling.
49
150
 
50
151
  ```ts
51
- conn.sendNotification('$/task/progress', {
52
- taskId: 't1',
53
- bytesDone: 1024,
54
- bytesTotal: 10240,
55
- speedBps: 512,
56
- etaSec: 18,
57
- phase: 'downloading',
152
+ // Direct HTTP(S) file
153
+ const task = await conn.sendRequest('download/add', {
154
+ kind: 'url',
155
+ saveDir: '/Users/me/Downloads',
156
+ uris: ['https://cdn.example.com/releases/app-1.4.2-arm64.dmg'],
157
+ connections: 8,
158
+ })
159
+ console.log(task.id, task.status) // "t_01H…", "downloading"
160
+
161
+ // Magnet link
162
+ await conn.sendRequest('download/add', {
163
+ kind: 'magnet',
164
+ saveDir: '/Users/me/Downloads',
165
+ uri: 'magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a',
58
166
  })
59
167
  ```
60
168
 
61
- ### Schema 运行时校验
169
+ > Only `http`, `https`, `ftp`, `ftps`, and `sftp` URLs are accepted — the schema
170
+ > rejects `file:`, `data:`, and `javascript:` at the contract boundary, so an
171
+ > agent can never be coerced into a local-file read.
172
+
173
+ ### Query and control tasks (client → server)
62
174
 
63
175
  ```ts
64
- import { UrlResolveParamsSchema } from '@motrix/mdxp'
176
+ const { tasks, total } = await conn.sendRequest('task/list', {
177
+ status: 'downloading',
178
+ limit: 20,
179
+ })
65
180
 
66
- const validated = UrlResolveParamsSchema.parse(rawData)
67
- // 失败抛 ZodError;用 safeParse() 取 success 字段判断
181
+ await conn.sendRequest('task/pause', { taskId: task.id })
182
+ await conn.sendRequest('task/resume', { taskId: task.id })
183
+ await conn.sendRequest('task/remove', { taskId: task.id, deleteFiles: false })
68
184
  ```
69
185
 
70
- ### Error 模型
186
+ ### Resolve a page (server → client)
187
+
188
+ The desktop app asks a client whether it can handle a page (`url/probe`), then
189
+ asks it to extract the downloadable resources (`url/resolve`). A client answers
190
+ by registering handlers:
71
191
 
72
192
  ```ts
73
- import { ErrorCodes, makeMdxpError } from '@motrix/mdxp'
193
+ conn.onRequest('url/probe', async ({ url }) => ({
194
+ handled: /videos\.example\.com/.test(url),
195
+ adapterId: 'example-video',
196
+ confidence: 'high',
197
+ }))
198
+
199
+ conn.onRequest('url/resolve', async ({ url, preferences }) => ({
200
+ selections: [
201
+ {
202
+ kind: 'direct',
203
+ primary: {
204
+ url: 'https://cdn.example.com/v/abc123/1080p.mp4',
205
+ headers: {},
206
+ cookies: [],
207
+ refererPolicy: 'strict-origin-when-cross-origin',
208
+ },
209
+ container: 'mp4',
210
+ quality: preferences?.maxQuality ?? '1080p',
211
+ sizeBytes: 734_003_200,
212
+ },
213
+ ],
214
+ meta: { title: 'Sample clip', author: 'example.com', durationSec: 372 },
215
+ extractedBy: {
216
+ adapterId: 'example-video',
217
+ adapterVersion: '1.0.0',
218
+ extractedAt: Date.now(),
219
+ },
220
+ }))
221
+ ```
222
+
223
+ A `selection` is a discriminated union on `kind`: `direct` (one file), `hls`
224
+ (a playlist), or `mux` (separate video + audio streams the server muxes). Each
225
+ `Resource` carries the `headers`/`cookies` needed to re-fetch it server-side.
74
226
 
75
- throw makeMdxpError(ErrorCodes.AdapterError, 'YouTube extraction failed', {
76
- appCode: 'youtube.video_unavailable',
77
- retryable: false,
78
- context: { videoId: 'abc' },
227
+ ### Progress and lifecycle (server → client)
228
+
229
+ ```ts
230
+ conn.onNotification('$/task/progress', (p) => {
231
+ // p.phase: 'queued' | 'downloading' | 'muxing' | 'finalizing'
232
+ })
233
+ conn.onNotification('$/task/completed', (p) => {
234
+ console.log('done →', p.filePath, `(${p.durationMs} ms)`)
235
+ })
236
+ conn.onNotification('$/task/error', (p) => {
237
+ console.error(`task ${p.taskId} failed: [${p.code}] ${p.message}`)
79
238
  })
80
239
  ```
81
240
 
82
- ## 公共 API
241
+ ### Cancellation
242
+
243
+ `sendRequest` accepts an optional `CancellationToken`. Cancelling emits
244
+ `$/cancelRequest` on the wire (handled by `vscode-jsonrpc`); a cooperative
245
+ handler observes `token.isCancellationRequested`.
246
+
247
+ ```ts
248
+ import { CancellationTokenSource } from 'vscode-jsonrpc'
249
+
250
+ const cts = new CancellationTokenSource()
251
+ const pending = conn.sendRequest('url/resolve', { url }, cts.token)
252
+ // …the user navigated away:
253
+ cts.cancel()
254
+ ```
255
+
256
+ ### Runtime validation
257
+
258
+ Every wire shape has a schema. Validate untrusted input at your boundary with
259
+ `safeParse` before acting on it:
260
+
261
+ ```ts
262
+ import { DownloadAddParamsSchema } from '@motrix/mdxp'
263
+
264
+ const parsed = DownloadAddParamsSchema.safeParse(untrusted)
265
+ if (!parsed.success) {
266
+ // parsed.error — a ZodError describing exactly what was wrong
267
+ return
268
+ }
269
+ await conn.sendRequest('download/add', parsed.data)
270
+ ```
271
+
272
+ ### Error model
273
+
274
+ Return structured errors from a handler with `makeMdxpError`. The `code` is a
275
+ JSON-RPC error code; `data` carries a machine-readable `appCode`, a retry hint,
276
+ and free-form context.
277
+
278
+ ```ts
279
+ import { ErrorCodes, makeMdxpError } from '@motrix/mdxp'
280
+
281
+ throw makeMdxpError(
282
+ ErrorCodes.ResourceUnavailable,
283
+ 'The requested file is no longer available',
284
+ { appCode: 'http.gone', retryable: false, context: { status: 410 } },
285
+ )
286
+ ```
287
+
288
+ Classify a received code with `isProtocolError(code)` (JSON-RPC reserved) or
289
+ `isMotrixError(code)` (Motrix's `-32001…-32099` range).
290
+
291
+ ### AI-agent tool catalog
292
+
293
+ The agent-facing methods are exposed as a JSON-Schema tool catalog, ready for an
294
+ LLM function-calling / tool-use API:
295
+
296
+ ```ts
297
+ import { toAgentToolCatalog } from '@motrix/mdxp'
298
+
299
+ const tools = toAgentToolCatalog()
300
+ // [
301
+ // { name: 'download/add', description, inputSchema: {…JSON Schema}, outputSchema },
302
+ // { name: 'task/list', … },
303
+ // …
304
+ // ]
305
+ ```
306
+
307
+ ## API reference
308
+
309
+ ### Exports
310
+
311
+ | Export | Kind | Purpose |
312
+ | --- | --- | --- |
313
+ | `createMdxpConnection(reader, writer)` | function | Wrap a reader/writer pair in a typed `MdxpConnection`. |
314
+ | `MdxpConnection` | type | The connection interface (`sendRequest`, `onRequest`, `sendNotification`, `onNotification`, `dispose`, `raw`). |
315
+ | `MdxpRequestMap` / `MdxpNotificationMap` | type | Method/notification name → params/result type maps. |
316
+ | `Methods` / `Notifications` | const | Wire-name constants (`Methods.DownloadAdd === 'download/add'`). |
317
+ | `ErrorCodes` | const | JSON-RPC + Motrix-defined error codes. |
318
+ | `makeMdxpError(code, msg, data?)` | function | Build a structured `MdxpError`. |
319
+ | `isProtocolError` / `isMotrixError` | function | Classify an error code. |
320
+ | `Tools` | const | Registry of every client→server method → `{ description, paramsSchema, resultSchema, agentFacing }`. |
321
+ | `toAgentToolCatalog()` | function | The `agentFacing` subset as JSON-Schema tools. |
322
+ | `SERVER_INITIATED_METHODS` | const | Methods the server calls on the client (`url/probe`, `url/resolve`). |
323
+ | `*Schema` | Zod schema | Every wire shape, for runtime validation. |
324
+
325
+ ### Methods
326
+
327
+ | Method | Direction | Agent-facing | Purpose |
328
+ | --- | --- | :---: | --- |
329
+ | `motrix/initialize` | client → server | | Handshake: version, identity, capabilities. |
330
+ | `system/ping` | client → server | | Liveness probe; echoes `sentAt` with `recvAt`. |
331
+ | `download/submit` | client → server | | Submit a browser-detected, page-shaped download. |
332
+ | `download/cancel` | client → server | | Cancel a submitted download by task id. |
333
+ | `download/add` | client → server | ✓ | Add a download by URL(s), magnet, or torrent. |
334
+ | `task/list` | client → server | ✓ | List tasks, filterable + paginated. |
335
+ | `task/get` | client → server | ✓ | Get one task by id. |
336
+ | `task/pause` · `task/resume` | client → server | ✓ | Pause / resume a task. |
337
+ | `task/remove` | client → server | ✓ | Remove a task, optionally deleting files. |
338
+ | `stats/get` | client → server | ✓ | Aggregate global stats (speeds + counts). |
339
+ | `engine/status` | client → server | ✓ | Engine lifecycle state + feature report. |
340
+ | `url/probe` | **server → client** | | Can this client's adapters handle a page? |
341
+ | `url/resolve` | **server → client** | | Extract downloadable resources from a page. |
342
+
343
+ ### Notifications
344
+
345
+ | Notification | Direction | Payload |
346
+ | --- | --- | --- |
347
+ | `motrix/initialized` | client → server | Handshake completion (no payload). |
348
+ | `$/task/progress` | server → client | `bytesDone`, `bytesTotal`, `speedBps`, `etaSec`, `phase`. |
349
+ | `$/task/completed` | server → client | `filePath`, `durationMs`. |
350
+ | `$/task/error` | server → client | `code`, `message`. |
351
+ | `$/stats` | server → client | Periodic aggregate stats push. |
352
+ | `$/pair/revoked` | server → client | Pairing was revoked (`reason`). |
353
+ | `$/cancelRequest` | either | Cancellation — handled by `vscode-jsonrpc`. |
354
+
355
+ ### Error codes
356
+
357
+ | Code | Value | Range |
358
+ | --- | --- | --- |
359
+ | `ParseError` | `-32700` | JSON-RPC reserved |
360
+ | `InvalidRequest` | `-32600` | JSON-RPC reserved |
361
+ | `MethodNotFound` | `-32601` | JSON-RPC reserved |
362
+ | `InvalidParams` | `-32602` | JSON-RPC reserved |
363
+ | `InternalError` | `-32603` | JSON-RPC reserved |
364
+ | `RequestCancelled` | `-32800` | LSP extension |
365
+ | `AdapterError` | `-32001` | Motrix |
366
+ | `ResourceUnavailable` | `-32002` | Motrix |
367
+ | `PermissionDenied` | `-32003` | Motrix |
368
+ | `RateLimited` | `-32004` | Motrix |
369
+ | `CapabilityNotSupported` | `-32005` | Motrix |
370
+ | `PairRevoked` | `-32006` | Motrix |
371
+
372
+ ## Protocol notes
83
373
 
84
- | Export | 作用 |
85
- |---|---|
86
- | `Methods.*` | 方法名常量 (`motrix/initialize` 等) |
87
- | `Notifications.*` | 通知名常量 (`$/task/progress` 等) |
88
- | `ErrorCodes.*` | JSON-RPC + Motrix-defined error codes |
89
- | `isProtocolError(code)`, `isMotrixError(code)` | error 分类 helpers |
90
- | `makeMdxpError(code, msg, data?)` | error 构造 helper |
91
- | `createMdxpConnection(reader, writer)` | 主入口,返回 `MdxpConnection` |
92
- | `*Schema` | 全部 Zod schemas(运行时校验) |
93
- | `MdxpRequestMap`, `MdxpNotificationMap` | 类型表(method → [params, result]) |
374
+ - **Version.** `protocolVersion` is `'1.0'`. This is the wire-compatibility
375
+ version and is independent of this package's npm version.
376
+ - **No batching.** JSON-RPC batching is forbidden — one frame, one message.
377
+ - **Forward-compatible.** Result and notification payloads are non-strict:
378
+ a newer server may add fields that older clients ignore. An unknown method is
379
+ rejected with `MethodNotFound` rather than crashing the session.
94
380
 
95
- ## 设计原则
381
+ ## Design principles
96
382
 
97
- - **transport-agnostic**:本包不绑死特定 transport;任何符合 `MessageReader`/`MessageWriter` 接口的双工流都可用
98
- - **schema-first**:所有 wire 形态先用 Zod 定义,TypeScript 类型从 schema 推断
99
- - **forward-compat**:未知 method/字段忽略而非 reject
383
+ - **Transport-agnostic** — the library never assumes a specific transport; any
384
+ `MessageReader`/`MessageWriter` duplex works.
385
+ - **Schema-first** — define the Zod schema, infer the type; never hand-write a
386
+ type that has a corresponding schema.
387
+ - **Forward-compatible** — ignore the unknown rather than reject it.
100
388
 
101
389
  ## License
102
390
 
103
- MIT
391
+ [MIT](./LICENSE) © Dr_rOot
@@ -0,0 +1,375 @@
1
+ # @motrix/mdxp
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@motrix/mdxp.svg)](https://www.npmjs.com/package/@motrix/mdxp)
4
+ [![license](https://img.shields.io/npm/l/@motrix/mdxp.svg)](./LICENSE)
5
+ [![types](https://img.shields.io/npm/types/@motrix/mdxp.svg)](./dist/index.d.ts)
6
+
7
+ [English](./README.md) | **简体中文**
8
+
9
+ > **MDXP**(Motrix Download eXchange Protocol)—— 一套 JSON-RPC 2.0 wire 类型、
10
+ > Zod schema 与双向连接封装,让浏览器、CLI 或 AI agent 通过任意双工 transport,
11
+ > 把下载任务移交给 Motrix 桌面下载器。
12
+
13
+ `@motrix/mdxp` 是 MDXP wire 契约的唯一真源。bridge 的两端 —— 作为 **server** 的
14
+ [Motrix](https://github.com/agalwood/Motrix) 桌面端(持有下载引擎),与各类 **client**
15
+ (浏览器扩展、CLI、agent)—— 都依赖本包,从而让协议形态只需定义一次。
16
+
17
+ 本包不含任何 transport 相关实现:你提供一对
18
+ [`vscode-jsonrpc`](https://www.npmjs.com/package/vscode-jsonrpc) 的
19
+ `MessageReader`/`MessageWriter`(stdio、socket、WebSocket、`MessagePort` 皆可),
20
+ 本库便在其上构建出一个完全类型化的双向连接。
21
+
22
+ ## 特性
23
+
24
+ - **Schema-first**:每个 wire 形态都是一个 [Zod](https://zod.dev) schema,
25
+ TypeScript 类型再由 schema `z.infer` 推导而来 —— 校验与类型因此永远不会脱节。
26
+ - **完全类型化的连接**:`sendRequest`/`onRequest`/`sendNotification`/
27
+ `onNotification` 均以 method 名为泛型参数,params 与 result 类型据此自动推断,
28
+ 调用处无需任何 cast。
29
+ - **transport-agnostic**:只要实现了 `MessageReader`/`MessageWriter`,任何双工流都能用。
30
+ - **平台 RAL 入口**:`./node` 与 `./browser` 会替你装好对应的 `vscode-jsonrpc`
31
+ runtime abstraction layer。
32
+ - **面向 agent**:内置的 tool registry 可产出一份 JSON-Schema tool catalog,
33
+ 能直接对接 LLM 的 function-calling API。
34
+ - **forward-compatible**:遇到未知的 method 或字段选择忽略,而非 reject。
35
+
36
+ ## 安装
37
+
38
+ ```bash
39
+ npm install @motrix/mdxp
40
+ # 或:pnpm add @motrix/mdxp · yarn add @motrix/mdxp
41
+ ```
42
+
43
+ 运行时依赖:[`vscode-jsonrpc`](https://www.npmjs.com/package/vscode-jsonrpc)
44
+ `^9`,是本包的直接 `dependency`,会随本包一并装上。下面的示例还直接从
45
+ `vscode-jsonrpc/node` 和 `vscode-jsonrpc/browser` import reader/writer 类;若你也
46
+ 这样用,请在自己的 dependencies 里显式声明 `vscode-jsonrpc`,不要依赖 hoisting。
47
+ 仅 ESM;需要 Node.js ≥ 18 或现代 bundler。
48
+
49
+ ### 入口点
50
+
51
+ | import | 是否装 RAL | 使用场景 |
52
+ | --- | --- | --- |
53
+ | `@motrix/mdxp` | 否 —— 平台无关的核心 | 共享代码、测试、仅类型 import |
54
+ | `@motrix/mdxp/node` | Node RAL | Node host(Electron main、CLI、native-messaging host) |
55
+ | `@motrix/mdxp/browser` | Browser RAL | 浏览器 host(扩展 service worker、页面) |
56
+
57
+ `vscode-jsonrpc` v9 要求先装好一层 runtime abstraction layer(RAL),才能创建连接。
58
+ import `@motrix/mdxp/node` 或 `@motrix/mdxp/browser` 会把对应的 RAL 装进本包所用的
59
+ **同一个** `vscode-jsonrpc` 实例,并 re-export 完整的公开 API —— 于是 host 只需从
60
+ 一个入口 import 一切。
61
+
62
+ ## 快速开始
63
+
64
+ ### Node host(走 stdio)
65
+
66
+ ```ts
67
+ import { createMdxpConnection } from '@motrix/mdxp/node'
68
+ import { StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node'
69
+
70
+ const conn = createMdxpConnection(
71
+ new StreamMessageReader(process.stdin),
72
+ new StreamMessageWriter(process.stdout),
73
+ )
74
+
75
+ // 务必在 listen() 之前注册 handler。
76
+ conn.onNotification('$/task/progress', (p) => {
77
+ const pct = p.bytesTotal ? Math.round((p.bytesDone / p.bytesTotal) * 100) : null
78
+ console.log(`[${p.taskId}] ${p.phase} ${pct ?? '?'}% @ ${p.speedBps} B/s`)
79
+ })
80
+
81
+ conn.listen()
82
+ ```
83
+
84
+ ### 浏览器 host
85
+
86
+ ```ts
87
+ // 装好 browser RAL,并 re-export 全量 API。
88
+ import { createMdxpConnection } from '@motrix/mdxp/browser'
89
+ import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-jsonrpc/browser'
90
+
91
+ // 例如一个 MessagePort / Worker;若用 WebSocket,需自行适配成 reader/writer。
92
+ const conn = createMdxpConnection(
93
+ new BrowserMessageReader(worker),
94
+ new BrowserMessageWriter(worker),
95
+ )
96
+ conn.listen()
97
+ ```
98
+
99
+ ## 核心概念
100
+
101
+ **server 与 client**:Motrix 桌面端是 **server** —— 下载引擎由它持有。**client**
102
+ 则是驱动它的一方:浏览器扩展、CLI 或 agent。连接本身是对称的,但每个 method 都有
103
+ 既定的调用方向(见下)。
104
+
105
+ **握手先行**:`motrix/initialize` **必须**是每个 session 的第一条消息 —— 它负责协商
106
+ protocol version、交换身份、声明 capabilities。在它 resolve 之前,不应发送任何其它消息。
107
+
108
+ **消息方向**:绝大多数 method 是 client→server(由 client 请求下载器做事)。只有两个是
109
+ server→client —— server 请求 client 去检视某个页面:`url/probe` 与 `url/resolve`
110
+ (见 [`SERVER_INITIATED_METHODS`](#api-参考))。既然这两个都由 server 发起,client
111
+ 一侧就用 `onRequest` 应答,server 一侧则用 `sendRequest` 调用。
112
+
113
+ ## 用法
114
+
115
+ ### 握手
116
+
117
+ ```ts
118
+ const hello = await conn.sendRequest('motrix/initialize', {
119
+ protocolVersion: '1.0',
120
+ client: {
121
+ kind: 'cli', // 或 'extension'
122
+ name: 'my-download-agent',
123
+ version: '1.0.0',
124
+ locale: 'zh-CN',
125
+ },
126
+ capabilities: { submitDownload: true, progress: true, cancellation: true },
127
+ adapters: [], // 该 client 能解析的页面 adapter(如有)
128
+ })
129
+
130
+ console.log(hello.server.name, hello.server.version)
131
+ console.log(hello.capabilities.selectionKinds) // 例如 ['direct', 'hls', 'mux']
132
+ ```
133
+
134
+ ### 添加下载(client → server)
135
+
136
+ `download/add` 是面向 agent 的公开入口,接受直连 URL 列表、magnet 链接或 base64
137
+ torrent,并直接返回新建的 task 快照 —— 调用方无需轮询即可渲染。
138
+
139
+ ```ts
140
+ // 直连 HTTP(S) 文件
141
+ const task = await conn.sendRequest('download/add', {
142
+ kind: 'url',
143
+ saveDir: '/Users/me/Downloads',
144
+ uris: ['https://cdn.example.com/releases/app-1.4.2-arm64.dmg'],
145
+ connections: 8,
146
+ })
147
+ console.log(task.id, task.status) // "t_01H…", "downloading"
148
+
149
+ // magnet 链接
150
+ await conn.sendRequest('download/add', {
151
+ kind: 'magnet',
152
+ saveDir: '/Users/me/Downloads',
153
+ uri: 'magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a',
154
+ })
155
+ ```
156
+
157
+ > 只接受 `http`、`https`、`ftp`、`ftps`、`sftp` 协议的 URL —— schema 会在契约边界
158
+ > 直接 reject 掉 `file:`、`data:`、`javascript:`,因此 agent 绝无可能被诱导去读取
159
+ > 本地文件。
160
+
161
+ ### 查询与控制 task(client → server)
162
+
163
+ ```ts
164
+ const { tasks, total } = await conn.sendRequest('task/list', {
165
+ status: 'downloading',
166
+ limit: 20,
167
+ })
168
+
169
+ await conn.sendRequest('task/pause', { taskId: task.id })
170
+ await conn.sendRequest('task/resume', { taskId: task.id })
171
+ await conn.sendRequest('task/remove', { taskId: task.id, deleteFiles: false })
172
+ ```
173
+
174
+ ### 解析页面(server → client)
175
+
176
+ 桌面端会先问某个 client 能否处理这个页面(`url/probe`),再请它抽取出可下载的资源
177
+ (`url/resolve`)。client 通过注册 handler 来应答:
178
+
179
+ ```ts
180
+ conn.onRequest('url/probe', async ({ url }) => ({
181
+ handled: /videos\.example\.com/.test(url),
182
+ adapterId: 'example-video',
183
+ confidence: 'high',
184
+ }))
185
+
186
+ conn.onRequest('url/resolve', async ({ url, preferences }) => ({
187
+ selections: [
188
+ {
189
+ kind: 'direct',
190
+ primary: {
191
+ url: 'https://cdn.example.com/v/abc123/1080p.mp4',
192
+ headers: {},
193
+ cookies: [],
194
+ refererPolicy: 'strict-origin-when-cross-origin',
195
+ },
196
+ container: 'mp4',
197
+ quality: preferences?.maxQuality ?? '1080p',
198
+ sizeBytes: 734_003_200,
199
+ },
200
+ ],
201
+ meta: { title: 'Sample clip', author: 'example.com', durationSec: 372 },
202
+ extractedBy: {
203
+ adapterId: 'example-video',
204
+ adapterVersion: '1.0.0',
205
+ extractedAt: Date.now(),
206
+ },
207
+ }))
208
+ ```
209
+
210
+ 一个 `selection` 是按 `kind` 区分的 discriminated union:`direct`(单个文件)、
211
+ `hls`(一份 playlist),或 `mux`(分离的 video 与 audio 流,由 server 端合流)。
212
+ 每个 `Resource` 都带有 server 端重新抓取时所需的 `headers`/`cookies`。
213
+
214
+ ### 进度与生命周期(server → client)
215
+
216
+ ```ts
217
+ conn.onNotification('$/task/progress', (p) => {
218
+ // p.phase: 'queued' | 'downloading' | 'muxing' | 'finalizing'
219
+ })
220
+ conn.onNotification('$/task/completed', (p) => {
221
+ console.log('完成 →', p.filePath, `(${p.durationMs} ms)`)
222
+ })
223
+ conn.onNotification('$/task/error', (p) => {
224
+ console.error(`task ${p.taskId} 失败:[${p.code}] ${p.message}`)
225
+ })
226
+ ```
227
+
228
+ ### 取消
229
+
230
+ `sendRequest` 可接受一个可选的 `CancellationToken`。取消时会在 wire 上发出
231
+ `$/cancelRequest`(由 `vscode-jsonrpc` 处理);采用协作式取消的 handler 只需观察
232
+ `token.isCancellationRequested` 即可响应。
233
+
234
+ ```ts
235
+ import { CancellationTokenSource } from 'vscode-jsonrpc'
236
+
237
+ const cts = new CancellationTokenSource()
238
+ const pending = conn.sendRequest('url/resolve', { url }, cts.token)
239
+ // …用户离开了页面:
240
+ cts.cancel()
241
+ ```
242
+
243
+ ### 运行时校验
244
+
245
+ 每个 wire 形态都配有 schema。在边界处先用 `safeParse` 校验不可信输入,通过后再执行:
246
+
247
+ ```ts
248
+ import { DownloadAddParamsSchema } from '@motrix/mdxp'
249
+
250
+ const parsed = DownloadAddParamsSchema.safeParse(untrusted)
251
+ if (!parsed.success) {
252
+ // parsed.error —— 一个精确指出问题所在的 ZodError
253
+ return
254
+ }
255
+ await conn.sendRequest('download/add', parsed.data)
256
+ ```
257
+
258
+ ### Error 模型
259
+
260
+ 在 handler 里用 `makeMdxpError` 返回结构化的错误。`code` 是 JSON-RPC error code;
261
+ `data` 则携带机器可读的 `appCode`、重试提示,以及任意自定义上下文。
262
+
263
+ ```ts
264
+ import { ErrorCodes, makeMdxpError } from '@motrix/mdxp'
265
+
266
+ throw makeMdxpError(
267
+ ErrorCodes.ResourceUnavailable,
268
+ 'The requested file is no longer available',
269
+ { appCode: 'http.gone', retryable: false, context: { status: 410 } },
270
+ )
271
+ ```
272
+
273
+ 收到 code 后,可用 `isProtocolError(code)`(JSON-RPC 保留段)或 `isMotrixError(code)`
274
+ (Motrix 的 `-32001…-32099` 段)为它归类。
275
+
276
+ ### AI-agent tool catalog
277
+
278
+ 面向 agent 的那些 method 会被导出成一份 JSON-Schema tool catalog,可直接对接 LLM 的
279
+ function-calling / tool-use API:
280
+
281
+ ```ts
282
+ import { toAgentToolCatalog } from '@motrix/mdxp'
283
+
284
+ const tools = toAgentToolCatalog()
285
+ // [
286
+ // { name: 'download/add', description, inputSchema: {…JSON Schema}, outputSchema },
287
+ // { name: 'task/list', … },
288
+ // …
289
+ // ]
290
+ ```
291
+
292
+ ## API 参考
293
+
294
+ ### 导出
295
+
296
+ | 导出 | 类型 | 作用 |
297
+ | --- | --- | --- |
298
+ | `createMdxpConnection(reader, writer)` | function | 把一对 reader/writer 封装成类型化的 `MdxpConnection`。 |
299
+ | `MdxpConnection` | type | 连接接口(`sendRequest`、`onRequest`、`sendNotification`、`onNotification`、`dispose`、`raw`)。 |
300
+ | `MdxpRequestMap` / `MdxpNotificationMap` | type | method / notification 名 → params/result 类型的映射表。 |
301
+ | `Methods` / `Notifications` | const | wire 名常量(`Methods.DownloadAdd === 'download/add'`)。 |
302
+ | `ErrorCodes` | const | JSON-RPC 与 Motrix 自定义的 error code。 |
303
+ | `makeMdxpError(code, msg, data?)` | function | 构造结构化的 `MdxpError`。 |
304
+ | `isProtocolError` / `isMotrixError` | function | 为 error code 归类。 |
305
+ | `Tools` | const | 每个 client→server method 的注册表 → `{ description, paramsSchema, resultSchema, agentFacing }`。 |
306
+ | `toAgentToolCatalog()` | function | 取 `agentFacing` 子集,转成 JSON-Schema tools。 |
307
+ | `SERVER_INITIATED_METHODS` | const | 由 server 向 client 发起的 method(`url/probe`、`url/resolve`)。 |
308
+ | `*Schema` | Zod schema | 全部 wire 形态,供运行时校验。 |
309
+
310
+ ### Methods
311
+
312
+ | Method | 方向 | agent-facing | 作用 |
313
+ | --- | --- | :---: | --- |
314
+ | `motrix/initialize` | client → server | | 握手:协商 version、身份、capabilities。 |
315
+ | `system/ping` | client → server | | 存活探测;回显 `sentAt` 与 `recvAt`。 |
316
+ | `download/submit` | client → server | | 提交浏览器侦测到的 page 形态下载。 |
317
+ | `download/cancel` | client → server | | 按 task id 取消已提交的下载。 |
318
+ | `download/add` | client → server | ✓ | 按 URL / magnet / torrent 添加下载。 |
319
+ | `task/list` | client → server | ✓ | 列出 task,可过滤、可分页。 |
320
+ | `task/get` | client → server | ✓ | 按 id 取单个 task。 |
321
+ | `task/pause` · `task/resume` | client → server | ✓ | 暂停 / 恢复 task。 |
322
+ | `task/remove` | client → server | ✓ | 移除 task,可选一并删除文件。 |
323
+ | `stats/get` | client → server | ✓ | 取聚合的全局统计(速度 + 计数)。 |
324
+ | `engine/status` | client → server | ✓ | 取下载引擎的生命周期状态与 feature report。 |
325
+ | `url/probe` | **server → client** | | 该 client 的 adapter 能否处理某页面? |
326
+ | `url/resolve` | **server → client** | | 从页面中抽取可下载资源。 |
327
+
328
+ ### Notifications
329
+
330
+ | Notification | 方向 | 载荷 |
331
+ | --- | --- | --- |
332
+ | `motrix/initialized` | client → server | 握手完成(无载荷)。 |
333
+ | `$/task/progress` | server → client | `bytesDone`、`bytesTotal`、`speedBps`、`etaSec`、`phase`。 |
334
+ | `$/task/completed` | server → client | `filePath`、`durationMs`。 |
335
+ | `$/task/error` | server → client | `code`、`message`。 |
336
+ | `$/stats` | server → client | 周期性推送的聚合统计。 |
337
+ | `$/pair/revoked` | server → client | 配对被撤销(`reason`)。 |
338
+ | `$/cancelRequest` | 双向 | 取消 —— 由 `vscode-jsonrpc` 处理。 |
339
+
340
+ ### Error codes
341
+
342
+ | Code | 值 | 所属段 |
343
+ | --- | --- | --- |
344
+ | `ParseError` | `-32700` | JSON-RPC 保留 |
345
+ | `InvalidRequest` | `-32600` | JSON-RPC 保留 |
346
+ | `MethodNotFound` | `-32601` | JSON-RPC 保留 |
347
+ | `InvalidParams` | `-32602` | JSON-RPC 保留 |
348
+ | `InternalError` | `-32603` | JSON-RPC 保留 |
349
+ | `RequestCancelled` | `-32800` | LSP 扩展 |
350
+ | `AdapterError` | `-32001` | Motrix |
351
+ | `ResourceUnavailable` | `-32002` | Motrix |
352
+ | `PermissionDenied` | `-32003` | Motrix |
353
+ | `RateLimited` | `-32004` | Motrix |
354
+ | `CapabilityNotSupported` | `-32005` | Motrix |
355
+ | `PairRevoked` | `-32006` | Motrix |
356
+
357
+ ## 协议说明
358
+
359
+ - **版本**:`protocolVersion` 为 `'1.0'`。这是 wire 的兼容性版本,与本包的 npm
360
+ version 相互独立。
361
+ - **不支持 batching**:JSON-RPC batching 被明确禁止 —— 一帧一消息。
362
+ - **forward-compatible**:result 与 notification 的载荷都是 non-strict 的 ——
363
+ 较新的 server 可以新增字段,较旧的 client 忽略即可;未知的 method 会以
364
+ `MethodNotFound` 被拒绝,而不会拖垮整个 session。
365
+
366
+ ## 设计原则
367
+
368
+ - **transport-agnostic** —— 本库不假定任何特定 transport;任何 `MessageReader`/
369
+ `MessageWriter` 双工流都能承载它。
370
+ - **schema-first** —— 先定义 Zod schema,再推断类型;绝不手写已有对应 schema 的类型。
371
+ - **forward-compatible** —— 对未知之物选择忽略,而非 reject。
372
+
373
+ ## License
374
+
375
+ [MIT](./LICENSE) © Dr_rOot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motrix/mdxp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Motrix Download eXchange Protocol — JSON-RPC 2.0 wire types and connection helpers",
5
5
  "license": "MIT",
6
6
  "author": "Motrix",
@@ -46,9 +46,10 @@
46
46
  "dist",
47
47
  "src",
48
48
  "!src/__tests__",
49
- "README.md"
49
+ "README.md",
50
+ "README.zh-CN.md"
50
51
  ],
51
- "packageManager": "pnpm@11.9.0",
52
+ "packageManager": "pnpm@11.13.0",
52
53
  "publishConfig": {
53
54
  "access": "public"
54
55
  },