dsh-listener 0.3.10

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/lib/index.js ADDED
@@ -0,0 +1,784 @@
1
+ // src/index.ts
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
4
+ import { readFile as readFile2 } from "node:fs/promises";
5
+ import { WebSocket as WebSocket2, WebSocketServer } from "ws";
6
+
7
+ // src/shared.ts
8
+ var BASE = "/dsh-listener";
9
+ var DEFAULT_MODEL = "qwen-audio-3.1-asr-flash-streaming";
10
+ var SAMPLE_RATE = 16e3;
11
+ var MAX_AUDIO_BYTES = 16e3 * 2 * 120;
12
+ var MAX_BUFFER_BYTES = 16e3 * 2 * 15;
13
+ var VERSION = "0.3.10";
14
+ var defaults = {
15
+ model: DEFAULT_MODEL,
16
+ region: "beijing",
17
+ workspaceId: "",
18
+ hotkey: "AltRight",
19
+ mode: "hold",
20
+ autoSend: false,
21
+ updateSource: "official",
22
+ mirrorUrl: "https://registry.npmmirror.com"
23
+ };
24
+ function validateUpdateSource(input, requireMirror = true) {
25
+ const updateSource = input.updateSource ?? "official";
26
+ if (updateSource !== "official" && updateSource !== "mirror") throw new Error("\u66F4\u65B0\u4E0B\u8F7D\u6765\u6E90\u65E0\u6548");
27
+ if (input.mirrorUrl !== void 0 && typeof input.mirrorUrl !== "string") throw new Error("\u955C\u50CF\u7F51\u5740\u65E0\u6548");
28
+ const previousUrl = (input.mirrorUrl ?? "").trim().replace(/\/+$/, "");
29
+ const mirrorUrl = /^https:\/\/(?:gh-proxy\.(?:org|com)|ghproxy\.com)(?:\/|$)/i.test(previousUrl) ? defaults.mirrorUrl : previousUrl;
30
+ if (mirrorUrl) {
31
+ try {
32
+ const url = new URL(mirrorUrl);
33
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || mirrorUrl.length > 512) throw new Error();
34
+ } catch {
35
+ throw new Error("\u955C\u50CF\u7F51\u5740\u987B\u4E3A HTTPS npm registry \u5730\u5740\uFF0C\u4E0D\u5305\u542B\u8D26\u53F7\u3001\u67E5\u8BE2\u53C2\u6570\u6216\u4E0B\u8F7D\u94FE\u63A5");
36
+ }
37
+ }
38
+ if (requireMirror && updateSource === "mirror" && !mirrorUrl) throw new Error("\u8BF7\u5148\u586B\u5199 npm \u955C\u50CF\u7F51\u5740");
39
+ return { updateSource, mirrorUrl: mirrorUrl.replace(/\/+$/, "") };
40
+ }
41
+ function validatePreferences(input) {
42
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("\u8BBE\u7F6E\u683C\u5F0F\u65E0\u6548");
43
+ const p = input;
44
+ if (typeof p.model !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(p.model)) throw new Error("\u6A21\u578B\u540D\u79F0\u65E0\u6548");
45
+ if (p.region !== "beijing" && p.region !== "singapore") throw new Error("\u5730\u57DF\u65E0\u6548");
46
+ if (typeof p.workspaceId !== "string" || !/^[a-zA-Z0-9-]{0,64}$/.test(p.workspaceId)) throw new Error("Workspace ID \u65E0\u6548");
47
+ if (typeof p.hotkey !== "string" || !/^(?:(?:Control|Shift|Alt|Meta)\+)*(?:AltRight|AltLeft|ControlRight|ControlLeft|ShiftRight|ShiftLeft|MetaRight|MetaLeft|Key[A-Z]|Digit[0-9]|F(?:[1-9]|1[0-2])|Space)$/.test(p.hotkey)) throw new Error("\u5FEB\u6377\u952E\u65E0\u6548");
48
+ if (p.mode !== "hold" && p.mode !== "toggle") throw new Error("\u5F55\u97F3\u6A21\u5F0F\u65E0\u6548");
49
+ if (typeof p.autoSend !== "boolean") throw new Error("\u81EA\u52A8\u53D1\u9001\u8BBE\u7F6E\u65E0\u6548");
50
+ const source = validateUpdateSource({ updateSource: p.updateSource, mirrorUrl: p.mirrorUrl ?? defaults.mirrorUrl }, false);
51
+ return { model: p.model, region: p.region, workspaceId: p.workspaceId, hotkey: p.hotkey, mode: p.mode, autoSend: p.autoSend, ...source };
52
+ }
53
+ function upstreamUrl(p) {
54
+ const region = p.region === "beijing" ? "cn-beijing" : "ap-southeast-1";
55
+ const host = p.workspaceId ? `${p.workspaceId}.${region}.maas.aliyuncs.com` : p.region === "beijing" ? "dashscope.aliyuncs.com" : "dashscope-intl.aliyuncs.com";
56
+ return `wss://${host}/api-ws/v1/inference`;
57
+ }
58
+ function joinText(left, right) {
59
+ if (!left || !right || /\s$/.test(left) || /^\s/.test(right)) return left + right;
60
+ return left + (/[A-Za-z0-9]$/.test(left) && /^[A-Za-z0-9]/.test(right) ? " " : "") + right;
61
+ }
62
+
63
+ // src/host/bailian.ts
64
+ import { randomUUID } from "node:crypto";
65
+ import WebSocket from "ws";
66
+ var connect = (url, key) => new WebSocket(url, { headers: { Authorization: `Bearer ${key}` }, handshakeTimeout: 1e4, maxPayload: 1024 * 1024 });
67
+ var BailianTask = class {
68
+ constructor(p, key, emit, factory = connect) {
69
+ this.emit = emit;
70
+ this.socket = factory(upstreamUrl(p), key);
71
+ this.timer = setTimeout(() => this.fail("\u767E\u70BC\u8FDE\u63A5\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u548C\u914D\u7F6E"), 15e3);
72
+ this.deadline = setTimeout(() => this.fail("\u5F55\u97F3\u8D85\u8FC7 120 \u79D2\uFF0C\u5DF2\u53D6\u6D88"), 125e3);
73
+ this.socket.on("open", () => {
74
+ if (this.ended) return;
75
+ this.socket.send(JSON.stringify({
76
+ header: { action: "run-task", task_id: this.taskId, streaming: "duplex" },
77
+ payload: {
78
+ task_group: "audio",
79
+ task: "asr",
80
+ function: "recognition",
81
+ model: p.model,
82
+ parameters: { format: "pcm", sample_rate: SAMPLE_RATE },
83
+ input: {}
84
+ }
85
+ }));
86
+ });
87
+ this.socket.on("message", (raw) => this.message(raw));
88
+ this.socket.on("error", (error) => {
89
+ const code = String(error?.code ?? "").replace(/[^A-Z0-9_]/g, "").slice(0, 40);
90
+ this.fail(`\u767E\u70BC\u8FDE\u63A5\u5931\u8D25${code ? `\uFF08${code}\uFF09` : ""}\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u3001\u5730\u57DF\u548C Workspace ID`);
91
+ });
92
+ this.socket.on("unexpected-response", (_req, response) => {
93
+ this.fail(`\u767E\u70BC\u8FDE\u63A5\u5931\u8D25\uFF08HTTP ${response.statusCode}\uFF09\u3002${response.statusCode === 401 || response.statusCode === 403 ? "\u8BF7\u68C0\u67E5\u5BC6\u94A5\u6709\u6548\u6027\u3001\u5730\u57DF\u3001Workspace ID \u548C\u6A21\u578B\u6743\u9650\u3002" : "\u8BF7\u68C0\u67E5 Workspace ID\u3001\u670D\u52A1\u5730\u5740\u548C\u7F51\u7EDC\u3002"}`);
94
+ });
95
+ this.socket.on("close", () => {
96
+ if (!this.ended) this.fail("\u767E\u70BC\u8FDE\u63A5\u63D0\u524D\u5173\u95ED\uFF0C\u8BC6\u522B\u672A\u5B8C\u6210");
97
+ });
98
+ }
99
+ taskId = randomUUID().replaceAll("-", "");
100
+ socket;
101
+ ready = false;
102
+ finishing = false;
103
+ finishSent = false;
104
+ ended = false;
105
+ bytes = 0;
106
+ queuedBytes = 0;
107
+ queue = [];
108
+ sentences = /* @__PURE__ */ new Map();
109
+ timer;
110
+ deadline;
111
+ sendAudio(audio) {
112
+ if (this.ended) return;
113
+ if (this.finishing) {
114
+ this.fail("\u505C\u6B62\u540E\u6536\u5230\u97F3\u9891\uFF0C\u5F55\u97F3\u534F\u8BAE\u65E0\u6548");
115
+ return;
116
+ }
117
+ if (!audio.length || audio.length % 2 || audio.length > 32e3) {
118
+ this.fail("PCM \u97F3\u9891\u5206\u5757\u65E0\u6548");
119
+ return;
120
+ }
121
+ this.bytes += audio.length;
122
+ if (this.bytes > MAX_AUDIO_BYTES) {
123
+ this.fail("\u5F55\u97F3\u8D85\u8FC7 120 \u79D2\uFF0C\u5DF2\u53D6\u6D88");
124
+ return;
125
+ }
126
+ if (!this.ready) {
127
+ this.queuedBytes += audio.length;
128
+ if (this.queuedBytes > MAX_BUFFER_BYTES) {
129
+ this.fail("\u767E\u70BC\u8FDE\u63A5\u8FC7\u6162\uFF0C\u5DF2\u53D6\u6D88\u5F55\u97F3");
130
+ return;
131
+ }
132
+ this.queue.push(Buffer.from(audio));
133
+ } else if (this.socket.bufferedAmount > MAX_BUFFER_BYTES) this.fail("\u4E0A\u4F20\u7F51\u7EDC\u8FC7\u6162\uFF0C\u5DF2\u53D6\u6D88\u5F55\u97F3");
134
+ else this.socket.send(audio);
135
+ }
136
+ finish() {
137
+ if (this.ended || this.finishing) return;
138
+ if (!this.bytes) {
139
+ this.fail("\u6CA1\u6709\u91C7\u96C6\u5230\u97F3\u9891");
140
+ return;
141
+ }
142
+ this.finishing = true;
143
+ clearTimeout(this.timer);
144
+ this.timer = setTimeout(() => this.fail("\u7B49\u5F85\u6700\u7EC8\u8BC6\u522B\u7ED3\u679C\u8D85\u65F6"), 2e4);
145
+ this.flushFinish();
146
+ }
147
+ cancel() {
148
+ if (this.ended) return;
149
+ this.ended = true;
150
+ clearTimeout(this.timer);
151
+ clearTimeout(this.deadline);
152
+ this.queue = [];
153
+ this.sentences.clear();
154
+ this.socket.terminate();
155
+ }
156
+ fail(message) {
157
+ if (this.ended) return;
158
+ this.cancel();
159
+ this.emit({ type: "error", message });
160
+ }
161
+ flushFinish() {
162
+ if (!this.ready || !this.finishing || this.finishSent || this.ended) return;
163
+ this.finishSent = true;
164
+ this.socket.send(JSON.stringify({ header: { action: "finish-task", task_id: this.taskId, streaming: "duplex" }, payload: { input: {} } }));
165
+ }
166
+ message(raw) {
167
+ if (this.ended) return;
168
+ let e;
169
+ try {
170
+ e = JSON.parse(raw.toString());
171
+ } catch {
172
+ this.fail("\u767E\u70BC\u8FD4\u56DE\u4E86\u65E0\u6548\u6570\u636E");
173
+ return;
174
+ }
175
+ if (e?.header?.task_id !== this.taskId) return;
176
+ switch (e.header.event) {
177
+ case "task-started": {
178
+ if (this.ready) return;
179
+ this.ready = true;
180
+ if (!this.finishing) clearTimeout(this.timer);
181
+ for (const chunk of this.queue) this.socket.send(chunk);
182
+ this.queue = [];
183
+ this.queuedBytes = 0;
184
+ this.emit({ type: "ready" });
185
+ this.flushFinish();
186
+ break;
187
+ }
188
+ case "result-generated": {
189
+ const output = e.payload?.output;
190
+ const s = output?.sentence ?? output?.output?.sentence;
191
+ if (!s || s.heartbeat === true || typeof s.text !== "string") return;
192
+ if (s.sentence_end === true && Number.isInteger(s.sentence_id) && s.sentence_id > 0) this.sentences.set(s.sentence_id, s.text);
193
+ const final = this.text();
194
+ this.emit({ type: "partial", text: s.sentence_end === true ? final : joinText(final, s.text) });
195
+ break;
196
+ }
197
+ case "task-finished": {
198
+ if (!this.finishSent) {
199
+ this.fail("\u767E\u70BC\u4EFB\u52A1\u610F\u5916\u7ED3\u675F");
200
+ return;
201
+ }
202
+ const text = this.text().trim();
203
+ this.ended = true;
204
+ clearTimeout(this.timer);
205
+ clearTimeout(this.deadline);
206
+ this.emit({ type: "final", text });
207
+ this.sentences.clear();
208
+ this.socket.close();
209
+ break;
210
+ }
211
+ case "task-failed":
212
+ this.fail(`\u767E\u70BC\u8BC6\u522B\u5931\u8D25\uFF08${String(e.header.error_code ?? "UNKNOWN").replace(/[^A-Za-z0-9_-]/g, "").slice(0, 80)}\uFF09\uFF0C\u8BF7\u68C0\u67E5\u6A21\u578B\u4E0E\u914D\u7F6E`);
213
+ }
214
+ }
215
+ text() {
216
+ return [...this.sentences].sort(([a], [b]) => a - b).reduce((text, [, sentence]) => joinText(text, sentence), "");
217
+ }
218
+ };
219
+
220
+ // src/host/http-channel.ts
221
+ import { randomUUID as randomUUID2 } from "node:crypto";
222
+ function reply(res, status, value) {
223
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
224
+ res.end(JSON.stringify(value));
225
+ }
226
+ function mountHttpChannel(ctx, preferences, keyRef2, factory = (p, k, emit) => new BailianTask(p, k, emit)) {
227
+ const entries = /* @__PURE__ */ new Map();
228
+ let creating = 0, disposed = false;
229
+ const active = () => creating + [...entries.values()].filter((e) => !e.ended).length;
230
+ const remove = (id, entry) => {
231
+ entry.ended = true;
232
+ entry.task?.cancel();
233
+ for (const wake of entry.wake) wake();
234
+ entries.delete(id);
235
+ };
236
+ const sweep = setInterval(() => {
237
+ for (const [id, entry] of entries) if (Date.now() - entry.touched > 3e4) remove(id, entry);
238
+ }, 5e3);
239
+ const unregister = ctx.webServer.register({ kind: "exact", path: `${BASE}/channel`, handler: async (req, res) => {
240
+ const admission = ctx.connection.admit(req);
241
+ if ("rejection" in admission) {
242
+ reply(res, admission.rejection, { error: "Harness \u767B\u5F55\u6216\u9875\u9762\u6765\u6E90\u9A8C\u8BC1\u5931\u8D25" });
243
+ return;
244
+ }
245
+ const url = new URL(req.url ?? "/", "http://localhost");
246
+ const action = url.searchParams.get("action"), id = url.searchParams.get("id") ?? "";
247
+ try {
248
+ if (req.method === "POST" && action === "start") {
249
+ if (disposed || active() >= 4 || entries.size >= 64) {
250
+ reply(res, 503, { error: "\u8BED\u97F3\u901A\u9053\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" });
251
+ return;
252
+ }
253
+ creating++;
254
+ try {
255
+ const key = await ctx.credentials.resolve(keyRef2);
256
+ if (res.destroyed) return;
257
+ if (!key) {
258
+ reply(res, 400, { error: "\u5C1A\u672A\u914D\u7F6E\u767E\u70BC API Key" });
259
+ return;
260
+ }
261
+ if (disposed) {
262
+ reply(res, 503, { error: "\u8BED\u97F3\u63D2\u4EF6\u6B63\u5728\u66F4\u65B0" });
263
+ return;
264
+ }
265
+ const id2 = randomUUID2(), entry2 = { events: [], seq: 0, ended: false, touched: Date.now(), wake: /* @__PURE__ */ new Set() };
266
+ entries.set(id2, entry2);
267
+ res.once("close", () => {
268
+ if (!res.writableFinished) remove(id2, entry2);
269
+ });
270
+ try {
271
+ entry2.task = factory(preferences(), key.value, (event) => {
272
+ entry2.events.push({ seq: ++entry2.seq, event });
273
+ if (entry2.events.length > 256) entry2.events.shift();
274
+ if (event.type === "final" || event.type === "error") entry2.ended = true;
275
+ for (const wake of entry2.wake) wake();
276
+ });
277
+ } catch {
278
+ remove(id2, entry2);
279
+ throw new Error("initialize");
280
+ }
281
+ reply(res, 200, { id: id2 });
282
+ return;
283
+ } finally {
284
+ creating--;
285
+ }
286
+ }
287
+ const entry = entries.get(id);
288
+ if (!entry) {
289
+ reply(res, 404, { error: "\u5F55\u97F3\u4F1A\u8BDD\u5DF2\u7ED3\u675F\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB" });
290
+ return;
291
+ }
292
+ entry.touched = Date.now();
293
+ if (req.method === "GET" && action === "events") {
294
+ const after = Number(url.searchParams.get("after") ?? 0);
295
+ if (!Number.isSafeInteger(after) || after < 0 || after > entry.seq) {
296
+ reply(res, 400, { error: "\u4E8B\u4EF6\u5E8F\u53F7\u65E0\u6548" });
297
+ return;
298
+ }
299
+ if (entry.seq === after && !entry.ended) await new Promise((resolve) => {
300
+ const done = () => {
301
+ clearTimeout(timer);
302
+ entry.wake.delete(done);
303
+ res.off("close", done);
304
+ resolve();
305
+ };
306
+ const timer = setTimeout(done, 15e3);
307
+ entry.wake.add(done);
308
+ res.once("close", done);
309
+ });
310
+ if (!res.destroyed) reply(res, 200, { events: entry.events.filter((e) => e.seq > after), ended: entry.ended });
311
+ return;
312
+ }
313
+ if (req.method !== "POST") {
314
+ reply(res, 405, { error: "\u65B9\u6CD5\u4E0D\u652F\u6301" });
315
+ return;
316
+ }
317
+ if (action === "cancel") {
318
+ remove(id, entry);
319
+ reply(res, 200, { ok: true });
320
+ return;
321
+ }
322
+ if (entry.ended) {
323
+ reply(res, 409, { error: "\u5F55\u97F3\u5DF2\u7ED3\u675F" });
324
+ return;
325
+ }
326
+ if (action === "audio") {
327
+ if (req.headers["content-type"] !== "application/octet-stream") {
328
+ reply(res, 415, { error: "\u9700\u8981 PCM \u4E8C\u8FDB\u5236\u97F3\u9891" });
329
+ return;
330
+ }
331
+ const chunks = [];
332
+ let size = 0;
333
+ for await (const chunk of req) {
334
+ size += chunk.length;
335
+ if (size > 32e3) {
336
+ remove(id, entry);
337
+ reply(res, 413, { error: "\u97F3\u9891\u5206\u5757\u8FC7\u5927" });
338
+ return;
339
+ }
340
+ ;
341
+ chunks.push(chunk);
342
+ }
343
+ entry.task?.sendAudio(Buffer.concat(chunks));
344
+ } else if (action === "finish") entry.task?.finish();
345
+ else {
346
+ reply(res, 400, { error: "\u64CD\u4F5C\u65E0\u6548" });
347
+ return;
348
+ }
349
+ reply(res, 200, { ok: true });
350
+ } catch {
351
+ if (!res.headersSent) reply(res, 500, { error: "Harness \u65E0\u6CD5\u521D\u59CB\u5316\u8BED\u97F3\u4EFB\u52A1\uFF0C\u8BF7\u68C0\u67E5\u51ED\u636E\u5B58\u50A8\u548C\u914D\u7F6E" });
352
+ }
353
+ } });
354
+ return { active, dispose: () => {
355
+ disposed = true;
356
+ clearInterval(sweep);
357
+ unregister();
358
+ for (const [id, entry] of entries) remove(id, entry);
359
+ } };
360
+ }
361
+
362
+ // src/host/management.ts
363
+ import PluginManager from "@deepseek-ai/dsh-plugin-manager";
364
+ import HMR from "@deepseek-ai/dsh-hmr";
365
+
366
+ // src/host/download.ts
367
+ import { createHash, randomUUID as randomUUID3 } from "node:crypto";
368
+ import { open, readFile, unlink } from "node:fs/promises";
369
+ import { join } from "node:path";
370
+ import { tmpdir } from "node:os";
371
+ import { gunzipSync } from "node:zlib";
372
+ var LIMIT = 32 * 1024 * 1024;
373
+ function verifyArchive(bytes, version) {
374
+ let tar;
375
+ try {
376
+ tar = gunzipSync(bytes, { maxOutputLength: 64 * 1024 * 1024 });
377
+ } catch {
378
+ throw new Error("\u4E0B\u8F7D\u5305\u4E0D\u662F\u6709\u6548\u7684 gzip \u5B89\u88C5\u5305\uFF0C\u8BF7\u68C0\u67E5\u955C\u50CF\u662F\u5426\u8FD4\u56DE\u4E86\u9519\u8BEF\u9875\u9762");
379
+ }
380
+ const files = /* @__PURE__ */ new Map();
381
+ for (let offset = 0; offset + 512 <= tar.length; ) {
382
+ const header = tar.subarray(offset, offset + 512);
383
+ const name2 = header.subarray(0, 100).toString().replace(/\0.*$/s, "");
384
+ if (!name2) break;
385
+ const size = parseInt(header.subarray(124, 136).toString().replace(/\0.*$/s, "").trim(), 8) || 0;
386
+ if (size < 0 || offset + 512 + size > tar.length) throw new Error("\u4E0B\u8F7D\u5305\u635F\u574F");
387
+ files.set(name2, tar.subarray(offset + 512, offset + 512 + size));
388
+ offset += 512 + Math.ceil(size / 512) * 512;
389
+ }
390
+ const entry = [...files.keys()].find((name2) => /^[^/]+\/package\.json$/.test(name2));
391
+ if (!entry) throw new Error("\u4E0B\u8F7D\u5305\u7F3A\u5C11\u63D2\u4EF6\u6E05\u5355\uFF0C\u8BF7\u68C0\u67E5\u955C\u50CF\u4E0B\u8F7D\u5730\u5740");
392
+ const manifest = JSON.parse(files.get(entry).toString());
393
+ const root = entry.slice(0, -"package.json".length);
394
+ if (manifest.name !== "dsh-listener" || manifest.version !== version) throw new Error("\u4E0B\u8F7D\u5305\u4E0E\u76EE\u6807\u63D2\u4EF6\u7248\u672C\u4E0D\u4E00\u81F4");
395
+ for (const file of ["lib/index.js", "lib/client.js", "lib/pcm-worklet.js", "cordis.patch.yml"]) {
396
+ if (!files.get(root + file)?.length) throw new Error(`\u4E0B\u8F7D\u5305\u7F3A\u5C11 ${file}`);
397
+ }
398
+ }
399
+ async function downloadArchive(url, version, progress, fetcher = fetch, integrity) {
400
+ const path = join(tmpdir(), `dsh-listener-${randomUUID3()}.tgz`);
401
+ const file = await open(path, "wx");
402
+ const dispose = () => unlink(path);
403
+ try {
404
+ const response = await fetcher(url, { signal: AbortSignal.timeout(12e4) });
405
+ if (!response.ok || !response.body) throw new Error(`\u5B89\u88C5\u5305\u4E0B\u8F7D\u5931\u8D25\uFF08HTTP ${response.status}\uFF09`);
406
+ const length = Number(response.headers.get("content-length"));
407
+ const total = Number.isSafeInteger(length) && length > 0 ? length : void 0;
408
+ if (total && total > LIMIT) {
409
+ await response.body.cancel();
410
+ throw new Error("\u5B89\u88C5\u5305\u8D85\u8FC7 32 MB \u9650\u5236");
411
+ }
412
+ let received = 0;
413
+ progress({ phase: "downloading", received, total });
414
+ const reader = response.body.getReader();
415
+ try {
416
+ while (true) {
417
+ const { done, value } = await reader.read();
418
+ if (done) break;
419
+ received += value.byteLength;
420
+ if (received > LIMIT) throw new Error("\u5B89\u88C5\u5305\u8D85\u8FC7 32 MB \u9650\u5236");
421
+ await file.writeFile(value);
422
+ progress({ phase: "downloading", received, total });
423
+ }
424
+ } finally {
425
+ await reader.cancel().catch(() => {
426
+ });
427
+ reader.releaseLock();
428
+ }
429
+ await file.close();
430
+ if (total && total !== received) throw new Error("\u5B89\u88C5\u5305\u4E0B\u8F7D\u4E0D\u5B8C\u6574\uFF0C\u8BF7\u91CD\u8BD5");
431
+ const bytes = await readFile(path);
432
+ if (integrity && `sha512-${createHash("sha512").update(bytes).digest("base64")}` !== integrity) throw new Error("\u5B89\u88C5\u5305\u5B8C\u6574\u6027\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u6216\u66F4\u6362 npm \u4E0B\u8F7D\u6765\u6E90");
433
+ verifyArchive(bytes, version);
434
+ return { path, dispose };
435
+ } catch (error) {
436
+ await file.close().catch(() => {
437
+ });
438
+ await dispose().catch(() => {
439
+ });
440
+ throw error;
441
+ }
442
+ }
443
+
444
+ // src/host/management.ts
445
+ var REGISTRY = "https://registry.npmjs.org";
446
+ var managers = /* @__PURE__ */ new WeakMap();
447
+ function managementError(code) {
448
+ if (code === "bundle-in-use") return "\u65E7\u7248\u63D2\u4EF6\u4ECD\u88AB\u5BBF\u4E3B\u52A0\u8F7D\uFF0C\u672C\u6B21\u64CD\u4F5C\u672A\u5B8C\u6210\u3002\u8BF7\u5728 Desktop \u63D2\u4EF6\u7BA1\u7406\u4E2D\u7981\u7528\u8BED\u97F3\u8F93\u5165\uFF0C\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u5F00 Desktop\uFF0C\u518D\u4ECE Desktop \u63D2\u4EF6\u7BA1\u7406\u5B89\u88C5\u65B0\u7248\u3002";
449
+ if (code === "stop-profile") return "\u5BBF\u4E3B\u65E0\u6CD5\u5728\u8FD0\u884C\u4E2D\u66FF\u6362\u6B64\u63D2\u4EF6\u3002\u8BF7\u505C\u6B62\u5F53\u524D profile\uFF0C\u518D\u4ECE Desktop \u63D2\u4EF6\u7BA1\u7406\u64CD\u4F5C\u3002";
450
+ if (/timeout/i.test(code)) return "\u5B89\u88C5\u8D85\u65F6\u3002\u8BF7\u5728 Desktop \u63D2\u4EF6\u7BA1\u7406\u4E2D\u67E5\u770B\u8BE6\u60C5\u540E\u91CD\u8BD5\u3002";
451
+ return `\u63D2\u4EF6\u7BA1\u7406\u64CD\u4F5C\u672A\u5B8C\u6210\uFF08${code}\uFF09\u3002\u8BF7\u5728 Desktop \u63D2\u4EF6\u7BA1\u7406\u4E2D\u67E5\u770B\u8BE6\u60C5\u3002`;
452
+ }
453
+ function compareVersions(a, b) {
454
+ const x = a.split(".").map(Number), y = b.split(".").map(Number);
455
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i];
456
+ return 0;
457
+ }
458
+ function updateUrl(url, source) {
459
+ return source.updateSource === "mirror" ? `${source.mirrorUrl}${new URL(url).pathname}` : url;
460
+ }
461
+ async function latestRelease(fetcher = fetch, input = {}) {
462
+ const source = validateUpdateSource(input);
463
+ const response = await fetcher(updateUrl(`${REGISTRY}/dsh-listener/latest`, source), {
464
+ headers: { Accept: "application/json", "Cache-Control": "no-cache" },
465
+ signal: AbortSignal.timeout(3e4)
466
+ });
467
+ if (!response.ok) throw new Error(`npm latest \u68C0\u67E5\u66F4\u65B0\u5931\u8D25\uFF08HTTP ${response.status}\uFF09`);
468
+ const pkg = await response.json();
469
+ if (pkg?.name !== "dsh-listener" || typeof pkg.version !== "string" || !/^\d+\.\d+\.\d+$/.test(pkg.version)) throw new Error("npm latest \u63D2\u4EF6\u6E05\u5355\u65E0\u6548");
470
+ const tarball = `${REGISTRY}/dsh-listener/-/dsh-listener-${pkg.version}.tgz`;
471
+ if (!pkg.dist?.tarball || ![tarball, updateUrl(tarball, source)].includes(pkg.dist.tarball)) throw new Error("npm \u5B89\u88C5\u5305\u5730\u5740\u65E0\u6548");
472
+ if (!pkg.dist.integrity || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(pkg.dist.integrity)) throw new Error("npm \u5B89\u88C5\u5305\u7F3A\u5C11\u6709\u6548\u7684\u5B8C\u6574\u6027\u6821\u9A8C");
473
+ return { version: pkg.version, tarball: updateUrl(tarball, source), integrity: pkg.dist.integrity };
474
+ }
475
+ async function manager(ctx) {
476
+ const existing = ctx.get("pluginManager");
477
+ if (existing) {
478
+ await enableProfileReload(ctx);
479
+ return existing;
480
+ }
481
+ if (!ctx.get("profileContext") || !ctx.get("loader")) throw new Error("\u5F53\u524D\u8FD0\u884C\u65B9\u5F0F\u6CA1\u6709\u63D2\u4EF6\u7BA1\u7406\u80FD\u529B\uFF0C\u8BF7\u5728 Desktop \u63D2\u4EF6\u7BA1\u7406\u4E2D\u64CD\u4F5C");
482
+ let pending = managers.get(ctx.root);
483
+ if (!pending) {
484
+ pending = (async () => {
485
+ await enableProfileReload(ctx);
486
+ const fiber = ctx.root.plugin(PluginManager, {});
487
+ await fiber.await();
488
+ const service = ctx.root.get("pluginManager");
489
+ if (!service) throw new Error("\u63D2\u4EF6\u7BA1\u7406\u670D\u52A1\u672A\u5C31\u7EEA");
490
+ return service;
491
+ })();
492
+ managers.set(ctx.root, pending);
493
+ void pending.catch(() => managers.delete(ctx.root));
494
+ }
495
+ return pending;
496
+ }
497
+ async function enableProfileReload(ctx) {
498
+ if (ctx.get("hmr")) return;
499
+ if (!ctx.get("timer") || !ctx.get("profileContext")) throw new Error("\u5BBF\u4E3B\u7F3A\u5C11\u70ED\u52A0\u8F7D\u670D\u52A1\uFF0C\u8BF7\u5728 Desktop \u63D2\u4EF6\u7BA1\u7406\u4E2D\u64CD\u4F5C");
500
+ const hmr = ctx.root.plugin(HMR, { root: [], debounce: 100, ignored: ["**/node_modules", "**/.*", "cache", "data"] });
501
+ await hmr.await();
502
+ }
503
+ function mountManagement(ctx, active, fetcher = fetch, getManager = manager, download = downloadArchive) {
504
+ let busy = false;
505
+ let progress = { phase: "idle", received: 0 };
506
+ return ctx.webServer.register({ kind: "exact", path: `${BASE}/manage`, handler: async (req, res) => {
507
+ const reply2 = (status, value) => {
508
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
509
+ res.end(JSON.stringify(value));
510
+ };
511
+ const admission = ctx.connection.admit(req);
512
+ if ("rejection" in admission) {
513
+ reply2(admission.rejection, { error: "Harness \u767B\u5F55\u6216\u9875\u9762\u6765\u6E90\u9A8C\u8BC1\u5931\u8D25" });
514
+ return;
515
+ }
516
+ const query = new URL(req.url ?? "/", "http://localhost").searchParams;
517
+ const action = query.get("action");
518
+ if (req.method === "GET" && action === "progress") {
519
+ reply2(200, progress);
520
+ return;
521
+ }
522
+ if (req.method !== "POST") {
523
+ reply2(405, { error: "\u65B9\u6CD5\u4E0D\u652F\u6301" });
524
+ return;
525
+ }
526
+ if (!["check", "update"].includes(action ?? "")) {
527
+ reply2(400, { error: "\u64CD\u4F5C\u65E0\u6548" });
528
+ return;
529
+ }
530
+ if (busy) {
531
+ reply2(409, { error: "\u5DF2\u6709\u63D2\u4EF6\u7BA1\u7406\u64CD\u4F5C\u6B63\u5728\u8FDB\u884C" });
532
+ return;
533
+ }
534
+ if (active() > 0 && action !== "check") {
535
+ reply2(409, { error: "\u8BF7\u5148\u7ED3\u675F\u5F55\u97F3\uFF0C\u518D\u66F4\u65B0\u63D2\u4EF6" });
536
+ return;
537
+ }
538
+ busy = true;
539
+ if (action === "update") progress = { phase: "checking", received: 0 };
540
+ try {
541
+ const source = validateUpdateSource({
542
+ updateSource: query.get("updateSource") ?? "official",
543
+ mirrorUrl: query.get("mirrorUrl") ?? ""
544
+ });
545
+ if (action === "check") {
546
+ const latest2 = await latestRelease(fetcher, source);
547
+ reply2(200, { current: VERSION, latest: latest2.version, available: compareVersions(latest2.version, VERSION) > 0 });
548
+ return;
549
+ }
550
+ const latest = await latestRelease(fetcher, source);
551
+ if (compareVersions(latest.version, VERSION) <= 0) {
552
+ progress = { phase: "done", received: 0 };
553
+ reply2(200, { application: "unchanged", message: "\u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C" });
554
+ return;
555
+ }
556
+ const service = await getManager(ctx);
557
+ progress = { phase: "downloading", received: 0 };
558
+ const archive = await download(latest.tarball, latest.version, (value) => {
559
+ progress = value;
560
+ }, fetcher, latest.integrity);
561
+ let result;
562
+ try {
563
+ progress = { ...progress, phase: "installing" };
564
+ result = await service.installBundle(archive.path);
565
+ } finally {
566
+ await archive.dispose().catch(() => {
567
+ });
568
+ }
569
+ if (result.application === "failed" || result.application === "cancelled" || result.application === "overridden") {
570
+ progress = { ...progress, phase: "error" };
571
+ reply2(400, { error: managementError(result.error?.code ?? result.application) });
572
+ return;
573
+ }
574
+ progress = { ...progress, phase: "done" };
575
+ reply2(200, { application: result.application, message: result.application === "restart-required" ? "\u65B0\u7248\u5DF2\u5B89\u88C5\uFF0C\u8BF7\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u65B0\u6253\u5F00 Desktop\u3002" : "\u66F4\u65B0\u5DF2\u5E94\u7528\uFF1B\u5982\u754C\u9762\u4ECD\u663E\u793A\u65E7\u7248\u672C\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u9875\u9762\u3002" });
576
+ } catch (error) {
577
+ if (action === "update") progress = { ...progress, phase: "error" };
578
+ const timeout = error instanceof Error && ["TimeoutError", "AbortError"].includes(error.name);
579
+ const invalidJson = error instanceof SyntaxError;
580
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
581
+ reply2(400, { error: timeout ? "npm \u66F4\u65B0\u8FDE\u63A5\u8D85\u65F6\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\uFF0C\u6216\u5207\u6362 npm \u955C\u50CF\u540E\u91CD\u8BD5\u3002" : invalidJson ? "\u66F4\u65B0\u670D\u52A1\u672A\u8FD4\u56DE\u6709\u6548 JSON\u3002\u8BF7\u68C0\u67E5\u586B\u5199\u7684\u7F51\u5740\u662F\u5426\u4E3A npm registry\u3002" : code ? managementError(code) : error instanceof Error ? error.message : "\u63D2\u4EF6\u7BA1\u7406\u5931\u8D25" });
582
+ } finally {
583
+ busy = false;
584
+ }
585
+ } });
586
+ }
587
+
588
+ // src/index.ts
589
+ var name = "dsh-listener";
590
+ var inject = ["webServer", "connection", "credentials", "settings"];
591
+ var Config = z.object({
592
+ model: z.string().default(defaults.model).volatile(),
593
+ region: z.union(["beijing", "singapore"]).default("beijing").volatile(),
594
+ workspaceId: z.string().default("").volatile(),
595
+ hotkey: z.string().default("AltRight").volatile(),
596
+ mode: z.union(["hold", "toggle"]).default("hold").volatile(),
597
+ autoSend: z.boolean().default(false).volatile(),
598
+ updateSource: z.union(["official", "mirror"]).default("official").volatile(),
599
+ mirrorUrl: z.string().default(defaults.mirrorUrl).volatile()
600
+ });
601
+ var keyRef = credentialRef("DSH_LISTENER_API_KEY");
602
+ function json(res, status, value) {
603
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
604
+ res.end(JSON.stringify(value));
605
+ }
606
+ async function body(req) {
607
+ const chunks = [];
608
+ let size = 0;
609
+ for await (const chunk of req) {
610
+ size += chunk.length;
611
+ if (size > 8192) throw new Error("\u8BBE\u7F6E\u8BF7\u6C42\u8FC7\u5927");
612
+ chunks.push(chunk);
613
+ }
614
+ return JSON.parse(Buffer.concat(chunks).toString());
615
+ }
616
+ function apply(ctx, initial = defaults) {
617
+ const wss = new WebSocketServer({ noServer: true, maxPayload: 32e3, perMessageDeflate: false });
618
+ const tasks = /* @__PURE__ */ new Map();
619
+ let disposed = false;
620
+ let writes = Promise.resolve();
621
+ const descriptor = () => ctx.settings.describe().find((s) => s.ns === name);
622
+ const preferences = () => validatePreferences(descriptor()?.value ?? initial);
623
+ const view = async () => ({
624
+ ...preferences(),
625
+ configured: (await ctx.credentials.describe(keyRef)).configured,
626
+ writable: ctx.settings.writable,
627
+ revision: descriptor()?.revision ?? 0
628
+ });
629
+ ctx.effect(() => ctx.settings.configure({ auto: false }));
630
+ const channel = mountHttpChannel(ctx, preferences, keyRef);
631
+ ctx.effect(() => channel.dispose);
632
+ ctx.effect(() => mountManagement(ctx, () => channel.active() + tasks.size));
633
+ ctx.effect(() => ctx.webServer.register({ kind: "exact", path: `${BASE}/config`, handler: async (req, res) => {
634
+ const admission = ctx.connection.admit(req);
635
+ if ("rejection" in admission) {
636
+ json(res, admission.rejection, { error: "\u8BF7\u5148\u767B\u5F55 Harness" });
637
+ return;
638
+ }
639
+ try {
640
+ if (req.method === "GET") {
641
+ json(res, 200, await view());
642
+ return;
643
+ }
644
+ if (req.method !== "POST") {
645
+ json(res, 405, { error: "\u65B9\u6CD5\u4E0D\u652F\u6301" });
646
+ return;
647
+ }
648
+ if (!req.headers["content-type"]?.startsWith("application/json")) {
649
+ json(res, 415, { error: "\u9700\u8981 JSON \u8BF7\u6C42" });
650
+ return;
651
+ }
652
+ const request = await body(req);
653
+ const reset = request?.reset === true;
654
+ if (request.reset !== void 0 && typeof request.reset !== "boolean") throw new Error("\u6E05\u9664\u64CD\u4F5C\u65E0\u6548");
655
+ const downloadOnly = request?.updateDownload !== void 0;
656
+ const source = downloadOnly ? validateUpdateSource(request.updateDownload, false) : void 0;
657
+ const next = reset ? defaults : downloadOnly ? void 0 : validatePreferences(request?.preferences);
658
+ if (reset && (downloadOnly || request.apiKey !== void 0 || request.clearKey !== void 0)) throw new Error("\u6E05\u9664\u4E0D\u80FD\u4E0E\u4FDD\u5B58\u540C\u65F6\u8FDB\u884C");
659
+ if (downloadOnly && (request.apiKey !== void 0 || request.clearKey !== void 0)) throw new Error("\u4E0B\u8F7D\u8BBE\u7F6E\u4E0D\u80FD\u4FEE\u6539\u5BC6\u94A5");
660
+ if (!Number.isInteger(request?.revision) || request.revision < 0) throw new Error("\u8BBE\u7F6E\u7248\u672C\u65E0\u6548\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5");
661
+ if (request.apiKey !== void 0 && (typeof request.apiKey !== "string" || request.apiKey.length > 2048 || !request.apiKey.trim())) throw new Error("API Key \u65E0\u6548");
662
+ if (request.clearKey !== void 0 && typeof request.clearKey !== "boolean") throw new Error("\u5BC6\u94A5\u64CD\u4F5C\u65E0\u6548");
663
+ if (request.apiKey !== void 0 && request.clearKey) throw new Error("\u4E0D\u80FD\u540C\u65F6\u4FDD\u5B58\u548C\u6E05\u9664\u5BC6\u94A5");
664
+ const write = writes.then(async () => {
665
+ if (!ctx.settings.writable) throw new Error("\u5F53\u524D\u914D\u7F6E\u53EA\u8BFB");
666
+ if (request.revision !== descriptor()?.revision) throw new Error("\u8BBE\u7F6E\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5");
667
+ if (reset && channel.active() + tasks.size > 0) throw new Error("\u8BF7\u5148\u7ED3\u675F\u5F55\u97F3");
668
+ if (request.apiKey !== void 0) await ctx.credentials.set(keyRef, request.apiKey.trim());
669
+ if (reset || request.clearKey === true) await ctx.credentials.unset(keyRef);
670
+ await ctx.settings.update(name, downloadOnly ? validatePreferences({ ...preferences(), ...source }) : next, request.revision);
671
+ });
672
+ writes = write.catch(() => void 0);
673
+ await write;
674
+ json(res, 200, await view());
675
+ } catch {
676
+ json(res, 400, { error: "\u4FDD\u5B58\u6216\u8BFB\u53D6\u5931\u8D25\uFF1A\u68C0\u67E5\u914D\u7F6E\u3001\u53EF\u5199\u6743\u9650\uFF0C\u6216\u5237\u65B0\u540E\u91CD\u8BD5\u3002\u5BC6\u94A5\u4E0E\u666E\u901A\u8BBE\u7F6E\u5206\u522B\u4FDD\u5B58\u3002" });
677
+ }
678
+ } }));
679
+ ctx.effect(() => ctx.webServer.register({ kind: "exact", path: `${BASE}/pcm-worklet.js`, handler: async (req, res) => {
680
+ const admission = ctx.connection.admit(req);
681
+ if ("rejection" in admission) {
682
+ res.writeHead(admission.rejection);
683
+ res.end();
684
+ return;
685
+ }
686
+ try {
687
+ const source = await readFile2(new URL("./pcm-worklet.js", import.meta.url), "utf8");
688
+ res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-cache" });
689
+ res.end(source);
690
+ } catch {
691
+ res.writeHead(500);
692
+ res.end("Audio worklet unavailable");
693
+ }
694
+ } }));
695
+ ctx.effect(() => ctx.webServer.registerUpgrade({ path: `${BASE}/stream`, handler: async (req, socket, head) => {
696
+ const admission = ctx.connection.admit(req);
697
+ if ("rejection" in admission) {
698
+ socket.end(`HTTP/1.1 ${admission.rejection} Rejected\r
699
+ Connection: close\r
700
+ \r
701
+ `);
702
+ return;
703
+ }
704
+ if (disposed || wss.clients.size >= 4) {
705
+ socket.end("HTTP/1.1 503 Busy\r\nConnection: close\r\n\r\n");
706
+ return;
707
+ }
708
+ wss.handleUpgrade(req, socket, head, (client) => {
709
+ const send = (event) => {
710
+ if (client.readyState === WebSocket2.OPEN) client.send(JSON.stringify(event));
711
+ };
712
+ const initialize = async () => {
713
+ let key;
714
+ try {
715
+ key = await ctx.credentials.resolve(keyRef);
716
+ } catch {
717
+ send({ type: "error", message: "Harness \u65E0\u6CD5\u8BFB\u53D6\u5DF2\u4FDD\u5B58\u7684\u767E\u70BC\u5BC6\u94A5\uFF0C\u8BF7\u5728\u8BED\u97F3\u8F93\u5165\u8BBE\u7F6E\u4E2D\u91CD\u65B0\u4FDD\u5B58" });
718
+ client.close();
719
+ return;
720
+ }
721
+ if (disposed || client.readyState !== WebSocket2.OPEN) return;
722
+ if (!key) {
723
+ send({ type: "error", message: "\u5C1A\u672A\u914D\u7F6E\u767E\u70BC API Key\uFF0C\u8BF7\u6253\u5F00\u8BED\u97F3\u8F93\u5165\u8BBE\u7F6E" });
724
+ client.close();
725
+ return;
726
+ }
727
+ const p = preferences();
728
+ const task = new BailianTask(p, key.value, (event) => {
729
+ send(event);
730
+ if (event.type === "error" || event.type === "final") {
731
+ tasks.delete(client);
732
+ client.close();
733
+ }
734
+ });
735
+ tasks.set(client, task);
736
+ client.on("message", (data, binary) => {
737
+ if (binary) {
738
+ task.sendAudio(Buffer.from(data));
739
+ return;
740
+ }
741
+ try {
742
+ const event = JSON.parse(data.toString());
743
+ if (event.type === "finish") task.finish();
744
+ else if (event.type === "cancel") {
745
+ task.cancel();
746
+ client.close();
747
+ } else {
748
+ task.cancel();
749
+ client.close(1008, "Unknown command");
750
+ }
751
+ } catch {
752
+ task.cancel();
753
+ client.close(1008, "Invalid command");
754
+ }
755
+ });
756
+ client.on("close", () => {
757
+ task.cancel();
758
+ tasks.delete(client);
759
+ });
760
+ client.on("error", () => {
761
+ task.cancel();
762
+ tasks.delete(client);
763
+ });
764
+ };
765
+ void initialize().catch(() => {
766
+ send({ type: "error", message: "\u8BED\u97F3\u4EFB\u52A1\u521D\u59CB\u5316\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u914D\u7F6E\u5E76\u91CD\u542F Harness" });
767
+ client.close();
768
+ });
769
+ });
770
+ } }));
771
+ ctx.effect(() => () => {
772
+ disposed = true;
773
+ for (const task of tasks.values()) task.cancel();
774
+ tasks.clear();
775
+ for (const client of wss.clients) client.terminate();
776
+ wss.close();
777
+ });
778
+ }
779
+ export {
780
+ Config,
781
+ apply,
782
+ inject,
783
+ name
784
+ };