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/client.js ADDED
@@ -0,0 +1,1095 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-listener", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/client/index.tsx
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ apply: () => apply,
25
+ inject: () => inject
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var import_react = require("react");
29
+
30
+ // src/shared.ts
31
+ var BASE = "/dsh-listener";
32
+ var DEFAULT_MODEL = "qwen-audio-3.1-asr-flash-streaming";
33
+ var SAMPLE_RATE = 16e3;
34
+ var MAX_AUDIO_BYTES = 16e3 * 2 * 120;
35
+ var MAX_BUFFER_BYTES = 16e3 * 2 * 15;
36
+ var VERSION = "0.3.10";
37
+ var defaults = {
38
+ model: DEFAULT_MODEL,
39
+ region: "beijing",
40
+ workspaceId: "",
41
+ hotkey: "AltRight",
42
+ mode: "hold",
43
+ autoSend: false,
44
+ updateSource: "official",
45
+ mirrorUrl: "https://registry.npmmirror.com"
46
+ };
47
+ function validateUpdateSource(input, requireMirror = true) {
48
+ const updateSource = input.updateSource ?? "official";
49
+ if (updateSource !== "official" && updateSource !== "mirror") throw new Error("\u66F4\u65B0\u4E0B\u8F7D\u6765\u6E90\u65E0\u6548");
50
+ if (input.mirrorUrl !== void 0 && typeof input.mirrorUrl !== "string") throw new Error("\u955C\u50CF\u7F51\u5740\u65E0\u6548");
51
+ const previousUrl = (input.mirrorUrl ?? "").trim().replace(/\/+$/, "");
52
+ const mirrorUrl = /^https:\/\/(?:gh-proxy\.(?:org|com)|ghproxy\.com)(?:\/|$)/i.test(previousUrl) ? defaults.mirrorUrl : previousUrl;
53
+ if (mirrorUrl) {
54
+ try {
55
+ const url = new URL(mirrorUrl);
56
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || mirrorUrl.length > 512) throw new Error();
57
+ } catch {
58
+ 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");
59
+ }
60
+ }
61
+ if (requireMirror && updateSource === "mirror" && !mirrorUrl) throw new Error("\u8BF7\u5148\u586B\u5199 npm \u955C\u50CF\u7F51\u5740");
62
+ return { updateSource, mirrorUrl: mirrorUrl.replace(/\/+$/, "") };
63
+ }
64
+ function validatePreferences(input) {
65
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("\u8BBE\u7F6E\u683C\u5F0F\u65E0\u6548");
66
+ const p = input;
67
+ 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");
68
+ if (p.region !== "beijing" && p.region !== "singapore") throw new Error("\u5730\u57DF\u65E0\u6548");
69
+ if (typeof p.workspaceId !== "string" || !/^[a-zA-Z0-9-]{0,64}$/.test(p.workspaceId)) throw new Error("Workspace ID \u65E0\u6548");
70
+ 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");
71
+ if (p.mode !== "hold" && p.mode !== "toggle") throw new Error("\u5F55\u97F3\u6A21\u5F0F\u65E0\u6548");
72
+ if (typeof p.autoSend !== "boolean") throw new Error("\u81EA\u52A8\u53D1\u9001\u8BBE\u7F6E\u65E0\u6548");
73
+ const source = validateUpdateSource({ updateSource: p.updateSource, mirrorUrl: p.mirrorUrl ?? defaults.mirrorUrl }, false);
74
+ return { model: p.model, region: p.region, workspaceId: p.workspaceId, hotkey: p.hotkey, mode: p.mode, autoSend: p.autoSend, ...source };
75
+ }
76
+ function workspaceFromHost(value) {
77
+ try {
78
+ const url = new URL(value.includes("://") ? value : `https://${value}`);
79
+ if (!["https:", "wss:"].includes(url.protocol) || url.username || url.password || url.port) return;
80
+ const match = /^([a-zA-Z0-9-]{1,64})\.(cn-beijing|ap-southeast-1)\.maas\.aliyuncs\.com$/.exec(url.hostname);
81
+ if (match) return { workspaceId: match[1], region: match[2] === "cn-beijing" ? "beijing" : "singapore" };
82
+ } catch {
83
+ }
84
+ }
85
+ function joinText(left, right) {
86
+ if (!left || !right || /\s$/.test(left) || /^\s/.test(right)) return left + right;
87
+ return left + (/[A-Za-z0-9]$/.test(left) && /^[A-Za-z0-9]/.test(right) ? " " : "") + right;
88
+ }
89
+
90
+ // src/client/settings.ts
91
+ var snapshot = null;
92
+ var listeners = /* @__PURE__ */ new Set();
93
+ var writes = Promise.resolve(void 0);
94
+ function write(body, revision) {
95
+ const next = writes.then(() => request({
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({ ...body, revision: snapshot?.revision ?? revision })
99
+ }));
100
+ writes = next.catch(() => void 0);
101
+ return next;
102
+ }
103
+ function publish(value) {
104
+ snapshot = value;
105
+ for (const fn of listeners) fn();
106
+ }
107
+ async function request(options) {
108
+ const response = await fetch(`${BASE}/config`, { credentials: "same-origin", ...options });
109
+ const result = await response.json();
110
+ if (!response.ok) throw new Error(result.error ?? "\u65E0\u6CD5\u8BFB\u53D6\u8BED\u97F3\u8BBE\u7F6E");
111
+ publish(result);
112
+ return result;
113
+ }
114
+ var settings = {
115
+ getSnapshot: () => snapshot,
116
+ subscribe: (fn) => {
117
+ listeners.add(fn);
118
+ return () => {
119
+ listeners.delete(fn);
120
+ };
121
+ },
122
+ refresh: () => request(),
123
+ progress: async (signal) => {
124
+ const response = await fetch(`${BASE}/manage?action=progress`, { credentials: "same-origin", signal });
125
+ if (!response.ok) throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u66F4\u65B0\u8FDB\u5EA6");
126
+ return response.json();
127
+ },
128
+ manage: async (action, source) => {
129
+ const query = new URLSearchParams({ action, ...validateUpdateSource(source ?? {}) });
130
+ const response = await fetch(`${BASE}/manage?${query}`, { method: "POST", credentials: "same-origin" });
131
+ const result = await response.json();
132
+ if (!response.ok) throw new Error(result.error ?? "\u63D2\u4EF6\u7BA1\u7406\u5931\u8D25");
133
+ return result;
134
+ },
135
+ saveDownload: (source) => write({ updateDownload: validateUpdateSource(source, false) }, snapshot?.revision ?? 0),
136
+ reset: () => write({ reset: true }, snapshot?.revision ?? 0),
137
+ save: (preferences, revision, apiKey, clearKey) => write({ preferences, ...apiKey ? { apiKey } : {}, ...clearKey ? { clearKey: true } : {} }, revision)
138
+ };
139
+
140
+ // src/client/hotkey.ts
141
+ function keyBinding(e) {
142
+ const modifiers = [];
143
+ if (e.ctrlKey && !e.code.startsWith("Control")) modifiers.push("Control");
144
+ if (e.shiftKey && !e.code.startsWith("Shift")) modifiers.push("Shift");
145
+ if (e.altKey && !e.code.startsWith("Alt")) modifiers.push("Alt");
146
+ if (e.metaKey && !e.code.startsWith("Meta")) modifiers.push("Meta");
147
+ return [...modifiers, e.code].join("+");
148
+ }
149
+ function matches(e, binding, allowRepeat = false) {
150
+ return (allowRepeat || !e.repeat) && !e.isComposing && !e.getModifierState("AltGraph") && keyBinding(e) === binding;
151
+ }
152
+ var modifierCode = /^(Control|Shift|Alt|Meta)(Left|Right)$/;
153
+ var HotkeyCapture = class {
154
+ candidate = "";
155
+ down(e) {
156
+ if (e.repeat || e.isComposing || e.getModifierState("AltGraph")) {
157
+ this.candidate = "";
158
+ return;
159
+ }
160
+ this.candidate = keyBinding(e);
161
+ if (!modifierCode.test(e.code)) {
162
+ const binding = this.candidate;
163
+ this.candidate = "";
164
+ return binding;
165
+ }
166
+ }
167
+ up(e) {
168
+ if (!this.candidate || !releasesBinding(e.code, this.candidate)) return;
169
+ const binding = this.candidate;
170
+ this.candidate = "";
171
+ return binding;
172
+ }
173
+ };
174
+ function releasesBinding(code, binding) {
175
+ const parts = binding.split("+");
176
+ return parts.at(-1) === code || modifierCode.test(code) && parts.slice(0, -1).includes(code.replace(/(Left|Right)$/, ""));
177
+ }
178
+ var HotkeyGesture = class {
179
+ constructor(action, mode) {
180
+ this.action = action;
181
+ this.mode = mode;
182
+ }
183
+ pressed = false;
184
+ pressedMode = "hold";
185
+ down() {
186
+ if (this.pressed) return;
187
+ this.pressed = true;
188
+ this.pressedMode = this.mode();
189
+ if (this.action.active()) {
190
+ if (this.pressedMode === "toggle") this.action.finish();
191
+ } else this.action.start();
192
+ }
193
+ up() {
194
+ if (!this.pressed) return;
195
+ this.pressed = false;
196
+ if (this.pressedMode === "hold" && this.action.active()) this.action.finish();
197
+ }
198
+ reset() {
199
+ this.pressed = false;
200
+ }
201
+ };
202
+
203
+ // src/client/draft.ts
204
+ function appendTranscript(actions, current, text, autoSend, startRevision) {
205
+ if (!text.trim()) return "empty";
206
+ if (current.phase !== "plain") return "blocked";
207
+ const end = current.draft.length - current.occurrences.reduce((delta, chip) => delta + chip.length - 1, 0);
208
+ const suffix = joinText(current.draft, text.trim()).slice(current.draft.length);
209
+ if (!actions.insertText(suffix, { start: end, end, draftRev: current.draftRev })) return "blocked";
210
+ if (autoSend && startRevision === current.draftRev) {
211
+ actions.submit();
212
+ return "sent";
213
+ }
214
+ return autoSend ? "edited" : "appended";
215
+ }
216
+
217
+ // src/client/audio.ts
218
+ var Recording = class {
219
+ constructor(chunk, interrupted) {
220
+ this.chunk = chunk;
221
+ this.interrupted = interrupted;
222
+ }
223
+ stream;
224
+ context;
225
+ source;
226
+ node;
227
+ cancelled = false;
228
+ stopped = false;
229
+ stopping;
230
+ async start() {
231
+ if (!navigator.mediaDevices?.getUserMedia) throw new Error("\u9EA6\u514B\u98CE\u9700\u8981 localhost \u6216 HTTPS\uFF0C\u4EE5\u53CA\u652F\u6301 Web Audio \u7684\u6D4F\u89C8\u5668");
232
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true }, video: false });
233
+ if (this.cancelled) {
234
+ stream.getTracks().forEach((track) => track.stop());
235
+ throw new Error("\u5F55\u97F3\u5DF2\u53D6\u6D88");
236
+ }
237
+ this.stream = stream;
238
+ try {
239
+ this.context = new AudioContext({ sampleRate: SAMPLE_RATE });
240
+ if (this.context.sampleRate !== SAMPLE_RATE) throw new Error("\u6D4F\u89C8\u5668\u4E0D\u652F\u6301 16 kHz \u97F3\u9891\u91C7\u96C6");
241
+ await this.context.audioWorklet.addModule(`${BASE}/pcm-worklet.js`);
242
+ if (this.cancelled) throw new Error("\u5F55\u97F3\u5DF2\u53D6\u6D88");
243
+ this.node = new AudioWorkletNode(this.context, "dsh-listener-pcm");
244
+ this.node.port.onmessage = (event) => {
245
+ if (!this.cancelled && event.data instanceof ArrayBuffer) this.chunk(event.data);
246
+ };
247
+ this.source = this.context.createMediaStreamSource(stream);
248
+ this.source.connect(this.node);
249
+ this.node.connect(this.context.destination);
250
+ for (const track of stream.getTracks()) track.onended = () => {
251
+ if (!this.cancelled && !this.stopped) this.interrupted();
252
+ };
253
+ await this.context.resume();
254
+ if (this.cancelled) throw new Error("\u5F55\u97F3\u5DF2\u53D6\u6D88");
255
+ } catch (error) {
256
+ await this.cancel();
257
+ throw error;
258
+ }
259
+ }
260
+ stop() {
261
+ if (this.stopping) return this.stopping;
262
+ this.stopping = this.finish();
263
+ return this.stopping;
264
+ }
265
+ async finish() {
266
+ this.stopped = true;
267
+ const node = this.node;
268
+ this.source?.disconnect();
269
+ this.stream?.getTracks().forEach((track) => track.stop());
270
+ try {
271
+ if (!node || this.cancelled) return;
272
+ await new Promise((resolve, reject) => {
273
+ const timer = setTimeout(() => reject(new Error("\u97F3\u9891\u6536\u5C3E\u8D85\u65F6")), 1500);
274
+ node.port.onmessage = (event) => {
275
+ if (this.cancelled) {
276
+ clearTimeout(timer);
277
+ resolve();
278
+ return;
279
+ }
280
+ if (event.data instanceof ArrayBuffer) this.chunk(event.data);
281
+ else if (event.data?.stopped) {
282
+ clearTimeout(timer);
283
+ resolve();
284
+ }
285
+ };
286
+ node.port.postMessage("stop");
287
+ });
288
+ } finally {
289
+ await this.release();
290
+ }
291
+ }
292
+ async cancel() {
293
+ this.cancelled = true;
294
+ await this.release();
295
+ }
296
+ async release() {
297
+ this.source?.disconnect();
298
+ this.node?.disconnect();
299
+ this.stream?.getTracks().forEach((track) => track.stop());
300
+ const context = this.context;
301
+ this.context = void 0;
302
+ if (context && context.state !== "closed") await context.close().catch(() => void 0);
303
+ }
304
+ };
305
+
306
+ // src/client/channel.ts
307
+ var HttpVoiceChannel = class {
308
+ static OPEN = 1;
309
+ readyState = 1;
310
+ bufferedAmount = 0;
311
+ onmessage = null;
312
+ onerror = null;
313
+ onclose = null;
314
+ id = "";
315
+ abort = new AbortController();
316
+ startup;
317
+ uploads;
318
+ constructor(_url) {
319
+ this.startup = this.start();
320
+ this.uploads = this.startup;
321
+ void this.startup.catch((error) => this.fail(error));
322
+ }
323
+ async request(action, options = {}, timeout = 15e3) {
324
+ const query = new URLSearchParams({ action, ...this.id ? { id: this.id } : {} });
325
+ const response = await fetch(`${BASE}/channel?${query}`, {
326
+ credentials: "same-origin",
327
+ ...options,
328
+ signal: AbortSignal.any([this.abort.signal, AbortSignal.timeout(timeout)])
329
+ });
330
+ const result = await response.json();
331
+ if (!response.ok) throw new Error(`Harness \u8BED\u97F3\u63A5\u53E3 HTTP ${response.status}\uFF1A${result.error ?? "\u8BF7\u6C42\u5931\u8D25"}`);
332
+ return result;
333
+ }
334
+ async start() {
335
+ const result = await this.request("start", { method: "POST" });
336
+ if (typeof result.id !== "string") throw new Error("\u5F55\u97F3\u4F1A\u8BDD\u54CD\u5E94\u65E0\u6548");
337
+ this.id = result.id;
338
+ if (this.readyState !== 1) {
339
+ this.cancelRemote();
340
+ return;
341
+ }
342
+ void this.poll().catch((error) => this.fail(error));
343
+ }
344
+ async poll() {
345
+ let after = 0;
346
+ while (this.readyState === 1) {
347
+ const query = new URLSearchParams({ action: "events", id: this.id, after: String(after) });
348
+ const response = await fetch(`${BASE}/channel?${query}`, { credentials: "same-origin", signal: AbortSignal.any([this.abort.signal, AbortSignal.timeout(2e4)]) });
349
+ const result = await response.json();
350
+ if (!response.ok) throw new Error(`Harness \u4E8B\u4EF6\u63A5\u53E3 HTTP ${response.status}\uFF1A${result.error ?? "\u8BF7\u6C42\u5931\u8D25"}`);
351
+ if (this.readyState !== 1) return;
352
+ if (!Array.isArray(result.events)) throw new Error("\u8BED\u97F3\u4E8B\u4EF6\u54CD\u5E94\u65E0\u6548");
353
+ for (const item of result.events) {
354
+ if (!Number.isSafeInteger(item.seq) || item.seq <= after) throw new Error("\u8BED\u97F3\u4E8B\u4EF6\u987A\u5E8F\u65E0\u6548");
355
+ after = item.seq;
356
+ this.onmessage?.({ data: JSON.stringify(item.event) });
357
+ if (this.readyState !== 1) return;
358
+ }
359
+ if (result.ended) {
360
+ this.readyState = 3;
361
+ this.onclose?.();
362
+ return;
363
+ }
364
+ }
365
+ }
366
+ send(data) {
367
+ if (this.readyState !== 1) return;
368
+ if (typeof data === "string" && JSON.parse(data).type === "cancel") {
369
+ this.close();
370
+ return;
371
+ }
372
+ const bytes = typeof data === "string" ? 0 : data.byteLength;
373
+ this.bufferedAmount += bytes;
374
+ if (this.bufferedAmount > MAX_BUFFER_BYTES) {
375
+ this.fail(new Error("\u97F3\u9891\u4E0A\u4F20\u7F51\u7EDC\u8FC7\u6162\uFF0C\u5DF2\u53D6\u6D88"));
376
+ return;
377
+ }
378
+ this.uploads = this.uploads.then(async () => {
379
+ if (this.readyState !== 1) return;
380
+ if (typeof data === "string") await this.request("finish", { method: "POST" });
381
+ else await this.request("audio", { method: "POST", headers: { "Content-Type": "application/octet-stream" }, body: data });
382
+ }).catch((error) => this.fail(error)).finally(() => {
383
+ this.bufferedAmount -= bytes;
384
+ });
385
+ }
386
+ fail(error) {
387
+ if (this.readyState !== 1) return;
388
+ this.onmessage?.({ data: JSON.stringify({ type: "error", message: error instanceof Error ? error.message : "Harness HTTP \u8BED\u97F3\u8FDE\u63A5\u5931\u8D25" }) });
389
+ this.close();
390
+ }
391
+ cancelRemote() {
392
+ if (!this.id) return;
393
+ void fetch(`${BASE}/channel?action=cancel&id=${encodeURIComponent(this.id)}`, { method: "POST", credentials: "same-origin", keepalive: true, signal: AbortSignal.timeout(3e3) }).catch(() => void 0);
394
+ }
395
+ close() {
396
+ if (this.readyState === 3) return;
397
+ this.readyState = 3;
398
+ this.abort.abort();
399
+ this.cancelRemote();
400
+ }
401
+ };
402
+
403
+ // src/client/session.ts
404
+ var VoiceSession = class {
405
+ constructor(final) {
406
+ this.final = final;
407
+ }
408
+ state = { phase: "idle", preview: "", message: "" };
409
+ listeners = /* @__PURE__ */ new Set();
410
+ generation = 0;
411
+ capture;
412
+ socket;
413
+ ready = false;
414
+ queue = [];
415
+ queuedBytes = 0;
416
+ totalBytes = 0;
417
+ finishing = false;
418
+ finishSent = false;
419
+ timer;
420
+ limit;
421
+ getSnapshot = () => this.state;
422
+ subscribe = (listener) => {
423
+ this.listeners.add(listener);
424
+ return () => this.listeners.delete(listener);
425
+ };
426
+ active = () => ["requesting", "recording"].includes(this.state.phase);
427
+ busy = () => this.active() || this.state.phase === "finishing";
428
+ set(patch) {
429
+ this.state = { ...this.state, ...patch };
430
+ for (const listener of this.listeners) listener();
431
+ }
432
+ async start() {
433
+ if (this.busy()) return;
434
+ const run = ++this.generation;
435
+ this.ready = false;
436
+ this.finishing = false;
437
+ this.finishSent = false;
438
+ this.queue = [];
439
+ this.queuedBytes = 0;
440
+ this.totalBytes = 0;
441
+ this.set({ phase: "requesting", preview: "", message: "\u6B63\u5728\u8BF7\u6C42\u9EA6\u514B\u98CE\u2026" });
442
+ const fail = (message) => {
443
+ if (this.generation === run) this.fail(message);
444
+ };
445
+ const capture = new Recording((chunk) => {
446
+ if (this.generation === run) this.audio(chunk);
447
+ }, () => fail("\u9EA6\u514B\u98CE\u4E2D\u65AD\uFF0C\u5F55\u97F3\u5DF2\u53D6\u6D88"));
448
+ this.capture = capture;
449
+ try {
450
+ const socket = new HttpVoiceChannel();
451
+ this.socket = socket;
452
+ this.timer = setTimeout(() => fail("\u8FDE\u63A5\u6216\u9EA6\u514B\u98CE\u6388\u6743\u8D85\u65F6\uFF0C\u5DF2\u53D6\u6D88"), 15e3);
453
+ socket.onmessage = (event) => {
454
+ if (this.generation !== run) return;
455
+ try {
456
+ const message = JSON.parse(event.data);
457
+ if (message.type === "ready") {
458
+ this.ready = true;
459
+ for (const chunk of this.queue) socket.send(chunk);
460
+ this.queue = [];
461
+ this.queuedBytes = 0;
462
+ this.flushFinish();
463
+ } else if (message.type === "partial") {
464
+ if (typeof message.text !== "string") throw new Error("Invalid transcript");
465
+ this.set({ preview: message.text });
466
+ } else if (message.type === "final") {
467
+ if (!this.finishSent || typeof message.text !== "string") throw new Error("Unexpected final");
468
+ const text = message.text;
469
+ this.cancel();
470
+ if (text.trim()) this.final(text);
471
+ } else if (message.type === "error") fail(typeof message.message === "string" ? message.message : "\u8BC6\u522B\u5931\u8D25");
472
+ else throw new Error("Unknown voice event");
473
+ } catch {
474
+ fail("\u8BED\u97F3\u670D\u52A1\u54CD\u5E94\u65E0\u6548");
475
+ }
476
+ };
477
+ let diagnosing = false;
478
+ socket.onerror = () => {
479
+ diagnosing = true;
480
+ void fetch(`${BASE}/config`, { credentials: "same-origin", signal: AbortSignal.timeout(3e3) }).then((response) => {
481
+ if (response.status === 401) fail("Harness \u767B\u5F55\u5DF2\u5931\u6548\uFF08HTTP 401\uFF09\uFF0C\u8BF7\u5173\u95ED\u5E76\u91CD\u65B0\u6253\u5F00 Desktop \u9875\u9762\u3002");
482
+ else if (response.status === 403) fail("Harness \u62D2\u7EDD\u5F53\u524D\u9875\u9762\u6765\u6E90\uFF08HTTP 403\uFF09\uFF0C\u8BF7\u4ECE Desktop \u81EA\u5E26\u9875\u9762\u6253\u5F00\u3002");
483
+ else if (response.ok) fail("\u8BED\u97F3\u8BBE\u7F6E\u63A5\u53E3\u6B63\u5E38\uFF0C\u4F46 Harness \u97F3\u9891\u4E0A\u4F20\u901A\u9053\u4E0D\u53EF\u7528\u3002\u8BF7\u66F4\u65B0\u63D2\u4EF6\u5E76\u91CD\u65B0\u6253\u5F00 Desktop \u9875\u9762\u3002");
484
+ else fail(`Harness \u8BED\u97F3\u63A5\u53E3\u4E0D\u53EF\u7528\uFF08HTTP ${response.status}\uFF09\uFF0C\u8BF7\u68C0\u67E5\u63D2\u4EF6\u662F\u5426\u542F\u7528\u5E76\u91CD\u542F Desktop\u3002`);
485
+ }).catch(() => fail("\u65E0\u6CD5\u8BBF\u95EE Harness \u672C\u5730\u8BED\u97F3\u63A5\u53E3\uFF0C\u8BF7\u68C0\u67E5 Desktop \u5185\u6838\u662F\u5426\u8FD0\u884C\uFF0C\u5E76\u91CD\u542F Desktop\u3002"));
486
+ };
487
+ socket.onclose = () => {
488
+ if (!diagnosing) fail("\u8FDE\u63A5\u4E2D\u65AD\uFF0C\u5F55\u97F3\u5DF2\u53D6\u6D88");
489
+ };
490
+ await capture.start();
491
+ if (this.generation !== run) {
492
+ await capture.cancel();
493
+ return;
494
+ }
495
+ this.set({ phase: "recording", message: "" });
496
+ clearTimeout(this.timer);
497
+ this.timer = setTimeout(() => {
498
+ if (!this.ready) fail("\u767E\u70BC\u8FDE\u63A5\u8D85\u65F6");
499
+ }, 15e3);
500
+ this.limit = setTimeout(() => {
501
+ void this.finish();
502
+ }, 12e4);
503
+ } catch (error) {
504
+ fail(error instanceof Error ? error.message : "\u65E0\u6CD5\u5F00\u542F\u9EA6\u514B\u98CE");
505
+ }
506
+ }
507
+ async finish() {
508
+ if (this.state.phase === "requesting") {
509
+ this.cancel("\u9EA6\u514B\u98CE\u5C1A\u672A\u5C31\u7EEA\uFF0C\u672C\u6B21\u5F55\u97F3\u5DF2\u53D6\u6D88");
510
+ return;
511
+ }
512
+ if (this.state.phase !== "recording") return;
513
+ const run = this.generation;
514
+ this.set({ phase: "finishing", message: "\u6B63\u5728\u8BC6\u522B\u2026" });
515
+ clearTimeout(this.limit);
516
+ try {
517
+ await this.capture?.stop();
518
+ if (this.generation !== run) return;
519
+ this.finishing = true;
520
+ clearTimeout(this.timer);
521
+ this.timer = setTimeout(() => this.fail("\u7B49\u5F85\u8BC6\u522B\u7ED3\u679C\u8D85\u65F6"), 2e4);
522
+ if (!this.totalBytes) {
523
+ this.cancel();
524
+ return;
525
+ }
526
+ this.flushFinish();
527
+ } catch (error) {
528
+ if (this.generation === run) this.fail(error instanceof Error ? error.message : "\u505C\u6B62\u5F55\u97F3\u5931\u8D25");
529
+ }
530
+ }
531
+ cancel(message = "") {
532
+ ++this.generation;
533
+ clearTimeout(this.timer);
534
+ clearTimeout(this.limit);
535
+ void this.capture?.cancel();
536
+ this.capture = void 0;
537
+ const socket = this.socket;
538
+ this.socket = void 0;
539
+ if (socket) {
540
+ socket.onmessage = null;
541
+ socket.onclose = null;
542
+ socket.onerror = null;
543
+ if (socket.readyState === 1) socket.send(JSON.stringify({ type: "cancel" }));
544
+ socket.close();
545
+ }
546
+ this.queue = [];
547
+ this.queuedBytes = 0;
548
+ this.set({ phase: "idle", preview: "", message });
549
+ }
550
+ fail(message) {
551
+ this.cancel();
552
+ this.set({ phase: "error", message });
553
+ }
554
+ audio(chunk) {
555
+ const remaining = MAX_AUDIO_BYTES - this.totalBytes;
556
+ if (remaining <= 0) {
557
+ if (this.state.phase === "recording") void this.finish();
558
+ return;
559
+ }
560
+ if (chunk.byteLength > remaining) chunk = chunk.slice(0, remaining);
561
+ this.totalBytes += chunk.byteLength;
562
+ if (this.ready && this.socket?.readyState === 1) {
563
+ if (this.socket.bufferedAmount > MAX_BUFFER_BYTES) {
564
+ this.fail("\u4E0A\u4F20\u7F51\u7EDC\u8FC7\u6162\uFF0C\u5DF2\u53D6\u6D88");
565
+ return;
566
+ }
567
+ this.socket.send(chunk);
568
+ } else {
569
+ this.queuedBytes += chunk.byteLength;
570
+ if (this.queuedBytes > MAX_BUFFER_BYTES) {
571
+ this.fail("\u767E\u70BC\u8FDE\u63A5\u8FC7\u6162\uFF0C\u5DF2\u53D6\u6D88");
572
+ return;
573
+ }
574
+ this.queue.push(chunk);
575
+ }
576
+ if (this.totalBytes === MAX_AUDIO_BYTES && this.state.phase === "recording") void this.finish();
577
+ }
578
+ flushFinish() {
579
+ if (this.finishing && this.ready && !this.finishSent && this.socket?.readyState === 1) {
580
+ this.finishSent = true;
581
+ this.socket.send(JSON.stringify({ type: "finish" }));
582
+ }
583
+ }
584
+ };
585
+
586
+ // src/client/index.tsx
587
+ var import_jsx_runtime = require("react/jsx-runtime");
588
+ var inject = ["slots"];
589
+ var sessions = /* @__PURE__ */ new Set();
590
+ var owner;
591
+ var style = `
592
+ .listener-button{border:0;border-radius:8px;padding:7px;display:inline-flex;align-items:center;gap:5px;color:#858585;background:transparent;cursor:pointer;font:inherit}
593
+ .listener-button:hover{background:var(--dsw-alias-bg-layer-2,#8882)}.listener-button:disabled{opacity:.5;cursor:default}
594
+ .listener-button[data-recording=true]{color:#16a34a;background:#16a34a18}.listener-button:focus-visible,.listener-settings input:focus-visible,.listener-settings select:focus-visible{outline:2px solid #16a34a;outline-offset:2px}
595
+ .listener-control{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.listener-status{font-size:12px;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
596
+ .listener-settings{max-width:480px;padding:12px;color:inherit;font:inherit;font-size:13px}.listener-settings h2{margin:0 0 6px;font-size:16px}.listener-settings p{line-height:1.5;opacity:.8;margin:8px 0}.listener-settings details{margin:10px 0;font-size:12px;opacity:.85}
597
+ .listener-field{display:grid;grid-template-columns:135px 1fr;gap:12px;align-items:center;margin:16px 0}.listener-field input:not([type=checkbox]),.listener-field select{box-sizing:border-box;width:100%;padding:9px 10px;color:inherit;background:var(--dsw-alias-bg-layer-2,#8881);border:1px solid #8885;border-radius:7px;font:inherit}.listener-field input[type=checkbox]{width:18px;height:18px;accent-color:#16a34a}
598
+ .listener-actions{display:flex;gap:10px;margin-top:20px}.listener-action{padding:8px 14px;border:1px solid #8885;border-radius:7px;background:transparent;color:inherit;cursor:pointer;font:inherit}.listener-primary{background:#15803d;color:white;border-color:#15803d}.listener-action:disabled{opacity:.5;cursor:default}
599
+ .listener-dialog{padding:0;border:1px solid #8885;border-radius:12px;max-width:min(680px,calc(100vw - 32px));max-height:85vh;color:inherit;background:var(--dsw-alias-bg-layer-1,Canvas);overflow:auto}.listener-dialog::backdrop{background:#0006}.listener-dialog-close{display:flex;justify-content:flex-end;padding:12px 16px 0}
600
+ .listener-settings .listener-field{grid-template-columns:105px 1fr;gap:8px;margin:10px 0}.listener-settings .listener-field input:not([type=checkbox]),.listener-settings .listener-field select{padding:6px 8px;font-size:13px}.listener-settings .listener-action{padding:5px 10px;font-size:13px}.listener-settings .listener-actions{margin-top:12px}.listener-popup{width:min(380px,calc(100vw - 48px));padding:16px;font-size:13px}.listener-popup h2{font-size:16px;margin:0 0 10px}.listener-popup p{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.5}.listener-popup-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:12px}
601
+ .listener-progress{width:100%;height:8px;accent-color:#16a34a}
602
+ .listener-shortcut{display:flex;align-items:center;gap:8px}.listener-shortcut input{min-width:0;flex:1}.listener-shortcut button{flex:none}.listener-settings .listener-hint{font-size:12px;margin:5px 0 0}
603
+ @media(max-width:500px){.listener-field{grid-template-columns:1fr;gap:6px}.listener-settings{padding:12px}}
604
+ `;
605
+ function Mic() {
606
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true", children: [
607
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "9", y: "2", width: "6", height: "12", rx: "3" }),
608
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 10v2a7 7 0 0014 0v-2M12 19v3M8 22h8" })
609
+ ] });
610
+ }
611
+ function VoiceButton({ sessionId, useInput, inputActions }) {
612
+ const input = useInput((value) => value);
613
+ const config = (0, import_react.useSyncExternalStore)(settings.subscribe, settings.getSnapshot);
614
+ const latest = (0, import_react.useRef)({ input, config, inputActions });
615
+ latest.current = { input, config, inputActions };
616
+ const start = (0, import_react.useRef)({ revision: 0, autoSend: false });
617
+ const [notice, setNotice] = (0, import_react.useState)("");
618
+ const [pending, setPending] = (0, import_react.useState)("");
619
+ const [showSettings, setShowSettings] = (0, import_react.useState)(false);
620
+ const button = (0, import_react.useRef)(null);
621
+ const [session] = (0, import_react.useState)(() => new VoiceSession((text) => {
622
+ if (owner === session) owner = void 0;
623
+ const { input: input2, inputActions: inputActions2 } = latest.current;
624
+ const result = appendTranscript(inputActions2, input2, text, start.current.autoSend, start.current.revision);
625
+ if (result === "blocked") {
626
+ setPending(text);
627
+ setNotice("\u8F93\u5165\u6846\u6682\u4E0D\u53EF\u7F16\u8F91\uFF0C\u8BC6\u522B\u6587\u5B57\u5DF2\u4FDD\u7559");
628
+ } else if (result === "empty") setNotice("");
629
+ else if (result === "edited") setNotice("\u8349\u7A3F\u5DF2\u7F16\u8F91\uFF0C\u8BED\u97F3\u5DF2\u8FFD\u52A0\uFF0C\u8BF7\u624B\u52A8\u53D1\u9001");
630
+ else setNotice(result === "sent" ? "\u5DF2\u63D0\u4EA4\u53D1\u9001" : "\u8BED\u97F3\u5DF2\u8FFD\u52A0");
631
+ }));
632
+ const state = (0, import_react.useSyncExternalStore)(session.subscribe, session.getSnapshot);
633
+ (0, import_react.useEffect)(() => {
634
+ if (state.phase === "error") setNotice(state.message);
635
+ }, [state.phase, state.message]);
636
+ const operations = (0, import_react.useRef)({ begin: () => {
637
+ }, finish: () => {
638
+ } });
639
+ operations.current = {
640
+ begin: () => {
641
+ const { config: config2, input: input2 } = latest.current;
642
+ if (!config2?.configured || input2.phase !== "plain" || owner && owner !== session && owner.busy() || session.busy()) return;
643
+ start.current = { revision: input2.draftRev, autoSend: config2.autoSend };
644
+ setNotice("");
645
+ setPending("");
646
+ owner = session;
647
+ void session.start();
648
+ },
649
+ finish: () => {
650
+ void session.finish();
651
+ }
652
+ };
653
+ (0, import_react.useEffect)(() => {
654
+ sessions.add(session);
655
+ const gesture = new HotkeyGesture({ start: () => operations.current.begin(), finish: () => operations.current.finish(), active: session.active }, () => latest.current.config?.mode ?? "hold");
656
+ let pressedCode = "";
657
+ const down = (event) => {
658
+ if (event.defaultPrevented || !document.hasFocus() || document.hidden || !button.current?.getClientRects().length) return;
659
+ if (event.target?.closest?.('[data-listener-settings], [role="dialog"]')) return;
660
+ if (!latest.current.config?.configured || !matches(event, latest.current.config.hotkey, true)) return;
661
+ if (owner && owner !== session && owner.busy()) return;
662
+ event.preventDefault();
663
+ if (event.repeat) return;
664
+ pressedCode = latest.current.config.hotkey;
665
+ gesture.down();
666
+ };
667
+ const up = (event) => {
668
+ if (!pressedCode || !releasesBinding(event.code, pressedCode)) return;
669
+ event.preventDefault();
670
+ pressedCode = "";
671
+ gesture.up();
672
+ };
673
+ const cancel = () => {
674
+ gesture.reset();
675
+ pressedCode = "";
676
+ if (session.busy()) session.cancel("\u9875\u9762\u5931\u53BB\u7126\u70B9\uFF0C\u5DF2\u53D6\u6D88\u5F55\u97F3");
677
+ if (owner === session) owner = void 0;
678
+ };
679
+ const visibility = () => {
680
+ if (document.hidden) cancel();
681
+ };
682
+ window.addEventListener("keydown", down);
683
+ window.addEventListener("keyup", up);
684
+ window.addEventListener("blur", cancel);
685
+ document.addEventListener("visibilitychange", visibility);
686
+ return () => {
687
+ window.removeEventListener("keydown", down);
688
+ window.removeEventListener("keyup", up);
689
+ window.removeEventListener("blur", cancel);
690
+ document.removeEventListener("visibilitychange", visibility);
691
+ session.cancel();
692
+ sessions.delete(session);
693
+ if (owner === session) owner = void 0;
694
+ };
695
+ }, [sessionId, session]);
696
+ const label = state.phase === "recording" ? "\u505C\u6B62\u5F55\u97F3" : state.phase === "requesting" ? "\u53D6\u6D88\u9EA6\u514B\u98CE\u8BF7\u6C42" : state.phase === "finishing" ? "\u6B63\u5728\u8BC6\u522B" : "\u5F00\u59CB\u8BED\u97F3\u8F93\u5165";
697
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "listener-control", children: [
698
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
699
+ "button",
700
+ {
701
+ ref: button,
702
+ type: "button",
703
+ className: "listener-button",
704
+ "aria-label": label,
705
+ "aria-pressed": session.active(),
706
+ "data-recording": state.phase === "recording",
707
+ title: config?.configured ? `${label}\uFF08${config.hotkey}\uFF09${state.preview ? `\uFF1A${state.preview}` : ""}\uFF1B\u53F3\u952E\u6253\u5F00\u8BED\u97F3\u8F93\u5165\u8BBE\u7F6E` : "\u70B9\u51FB\u914D\u7F6E\u8BED\u97F3\u8F93\u5165",
708
+ disabled: state.phase === "finishing" || input.phase !== "plain",
709
+ onContextMenu: (event) => {
710
+ event.preventDefault();
711
+ if (!session.busy()) setShowSettings(true);
712
+ },
713
+ onClick: () => {
714
+ if (!config?.configured) setShowSettings(true);
715
+ else if (session.active()) operations.current.finish();
716
+ else operations.current.begin();
717
+ },
718
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Mic, {})
719
+ }
720
+ ),
721
+ showSettings && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SettingsDialog, { onClose: () => setShowSettings(false) }),
722
+ notice && !["\u8BED\u97F3\u5DF2\u8FFD\u52A0", "\u5DF2\u63D0\u4EA4\u53D1\u9001"].includes(notice) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageDialog, { message: notice, onClose: () => setNotice(""), onSettings: () => {
723
+ setNotice("");
724
+ setShowSettings(true);
725
+ }, children: pending && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
726
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: pending }),
727
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "listener-action", type: "button", onClick: () => {
728
+ const result = appendTranscript(inputActions, latest.current.input, pending, false, 0);
729
+ if (result !== "blocked") {
730
+ setPending("");
731
+ setNotice("\u8BED\u97F3\u5DF2\u8FFD\u52A0");
732
+ }
733
+ }, children: "\u8FFD\u52A0\u8BC6\u522B\u6587\u5B57" }),
734
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "listener-action", type: "button", onClick: () => {
735
+ void (navigator.clipboard?.writeText(pending) ?? Promise.reject()).then(() => setNotice("\u5DF2\u590D\u5236\u8BC6\u522B\u6587\u5B57")).catch(() => setNotice("\u65E0\u6CD5\u590D\u5236\uFF0C\u8BF7\u4F7F\u7528\u8FFD\u52A0\u6309\u94AE"));
736
+ }, children: "\u590D\u5236" })
737
+ ] }) })
738
+ ] });
739
+ }
740
+ function MessageDialog({ message, onClose, onSettings, children }) {
741
+ const dialog = (0, import_react.useRef)(null);
742
+ (0, import_react.useEffect)(() => {
743
+ dialog.current?.showModal();
744
+ }, []);
745
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dialog", { ref: dialog, className: "listener-dialog listener-popup", "aria-label": "\u8BED\u97F3\u8F93\u5165\u63D0\u793A", onCancel: onClose, onClose, children: [
746
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "\u8BED\u97F3\u8F93\u5165" }),
747
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", children: message }),
748
+ children,
749
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "listener-popup-actions", children: [
750
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "listener-action", onClick: onSettings, children: "\u68C0\u67E5\u8BBE\u7F6E" }),
751
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "listener-action", onClick: onClose, children: "\u5173\u95ED" })
752
+ ] })
753
+ ] });
754
+ }
755
+ function SettingsDialog({ onClose }) {
756
+ const dialog = (0, import_react.useRef)(null);
757
+ (0, import_react.useEffect)(() => {
758
+ dialog.current?.showModal();
759
+ }, []);
760
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dialog", { ref: dialog, className: "listener-dialog", "aria-label": "\u8BED\u97F3\u8F93\u5165\u8BBE\u7F6E", onCancel: onClose, onClose, children: [
761
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "listener-dialog-close", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "listener-action", onClick: onClose, children: "\u5173\u95ED" }) }),
762
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VoiceSettings, {})
763
+ ] });
764
+ }
765
+ function UpdateIndicator({ progress }) {
766
+ if (!progress || progress.phase === "idle") return null;
767
+ const percent = progress.total ? Math.min(100, progress.received / progress.total * 100) : void 0;
768
+ let value;
769
+ let label;
770
+ switch (progress.phase) {
771
+ case "checking":
772
+ label = "\u6B63\u5728\u68C0\u67E5\u7248\u672C\u2026";
773
+ break;
774
+ case "installing":
775
+ label = "\u4E0B\u8F7D\u5B8C\u6210\uFF0C\u6B63\u5728\u5B89\u88C5\u2026";
776
+ break;
777
+ case "done":
778
+ value = 100;
779
+ label = "\u66F4\u65B0\u5B8C\u6210";
780
+ break;
781
+ case "downloading":
782
+ value = percent;
783
+ label = `\u5DF2\u4E0B\u8F7D ${(progress.received / 1024).toFixed(1)} KB`;
784
+ if (progress.total) label += ` / ${(progress.total / 1024).toFixed(1)} KB\uFF08${Math.floor(percent)}%\uFF09`;
785
+ break;
786
+ default:
787
+ label = "\u66F4\u65B0\u672A\u5B8C\u6210";
788
+ }
789
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "aria-live": "polite", children: [
790
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("progress", { className: "listener-progress", "aria-label": "\u66F4\u65B0\u8FDB\u5EA6", max: 100, value }),
791
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: label })
792
+ ] });
793
+ }
794
+ function VoiceSettings() {
795
+ const saved = (0, import_react.useSyncExternalStore)(settings.subscribe, settings.getSnapshot);
796
+ const [form, setForm] = (0, import_react.useState)(defaults);
797
+ const [key, setKey] = (0, import_react.useState)("");
798
+ const [capture, setCapture] = (0, import_react.useState)(false);
799
+ const [busy, setBusy] = (0, import_react.useState)(false);
800
+ const [saving, setSaving] = (0, import_react.useState)(false);
801
+ const [message, setMessage] = (0, import_react.useState)("");
802
+ const [error, setError] = (0, import_react.useState)("");
803
+ const [available, setAvailable] = (0, import_react.useState)(false);
804
+ const [latestVersion, setLatestVersion] = (0, import_react.useState)("");
805
+ const [managementMessage, setManagementMessage] = (0, import_react.useState)("");
806
+ const [updating, setUpdating] = (0, import_react.useState)(false);
807
+ const [progress, setProgress] = (0, import_react.useState)(null);
808
+ const pendingDownload = (0, import_react.useRef)(null);
809
+ const dirty = (0, import_react.useRef)(false);
810
+ const [revision, setRevision] = (0, import_react.useState)(0);
811
+ (0, import_react.useEffect)(() => {
812
+ void settings.refresh().catch((error2) => setError(error2.message));
813
+ }, []);
814
+ (0, import_react.useEffect)(() => () => {
815
+ if (pendingDownload.current) void settings.saveDownload(pendingDownload.current).catch(() => {
816
+ });
817
+ }, []);
818
+ (0, import_react.useEffect)(() => {
819
+ if (saved) {
820
+ setRevision(saved.revision);
821
+ if (!dirty.current) setForm({ ...saved, mirrorUrl: saved.mirrorUrl || defaults.mirrorUrl });
822
+ }
823
+ }, [saved]);
824
+ (0, import_react.useEffect)(() => {
825
+ if (!saved || busy) return;
826
+ if (form.updateSource === saved.updateSource && form.mirrorUrl === saved.mirrorUrl) {
827
+ pendingDownload.current = null;
828
+ return;
829
+ }
830
+ if (!saved.writable) return;
831
+ let source;
832
+ try {
833
+ source = validateUpdateSource(form, false);
834
+ } catch {
835
+ pendingDownload.current = null;
836
+ return;
837
+ }
838
+ if (source.updateSource === saved.updateSource && source.mirrorUrl === saved.mirrorUrl) {
839
+ pendingDownload.current = null;
840
+ return;
841
+ }
842
+ pendingDownload.current = source;
843
+ let live = true;
844
+ const timer = setTimeout(() => {
845
+ void settings.saveDownload(source).then(() => {
846
+ if (pendingDownload.current?.updateSource === source.updateSource && pendingDownload.current.mirrorUrl === source.mirrorUrl) pendingDownload.current = null;
847
+ }).catch(() => {
848
+ if (live) setError("\u4E0B\u8F7D\u8BBE\u7F6E\u672A\u4FDD\u5B58\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002");
849
+ });
850
+ }, 500);
851
+ return () => {
852
+ live = false;
853
+ clearTimeout(timer);
854
+ };
855
+ }, [form.updateSource, form.mirrorUrl, saved, busy]);
856
+ (0, import_react.useEffect)(() => {
857
+ if (!updating) return;
858
+ const controller = new AbortController();
859
+ let timer;
860
+ const poll = async () => {
861
+ try {
862
+ const value = await settings.progress(AbortSignal.any([controller.signal, AbortSignal.timeout(5e3)]));
863
+ if (!controller.signal.aborted) setProgress(value);
864
+ } catch {
865
+ }
866
+ if (!controller.signal.aborted) timer = setTimeout(() => {
867
+ void poll();
868
+ }, 700);
869
+ };
870
+ void poll();
871
+ return () => {
872
+ controller.abort();
873
+ clearTimeout(timer);
874
+ };
875
+ }, [updating]);
876
+ (0, import_react.useEffect)(() => {
877
+ if (!capture) return;
878
+ const recorder = new HotkeyCapture();
879
+ const choose = (binding) => {
880
+ try {
881
+ validatePreferences({ ...defaults, hotkey: binding });
882
+ } catch {
883
+ setMessage("\u8BF7\u9009\u62E9\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u7A7A\u683C\u3001F1\u2013F12 \u6216\u4FEE\u9970\u952E\u7EC4\u5408");
884
+ return;
885
+ }
886
+ dirty.current = true;
887
+ setForm((p) => ({ ...p, hotkey: binding }));
888
+ setCapture(false);
889
+ setMessage("\u5FEB\u6377\u952E\u5DF2\u9009\u62E9\uFF0C\u70B9\u51FB\u4FDD\u5B58\u751F\u6548");
890
+ };
891
+ const keydown = (event) => {
892
+ event.preventDefault();
893
+ event.stopImmediatePropagation();
894
+ if (event.repeat || event.isComposing) return;
895
+ const binding = recorder.down(event);
896
+ if (event.getModifierState("AltGraph")) {
897
+ setMessage("AltGr \u7528\u4E8E\u5B57\u7B26\u8F93\u5165\uFF0C\u8BF7\u9009\u62E9\u5176\u4ED6\u5FEB\u6377\u952E");
898
+ return;
899
+ }
900
+ if (binding) choose(binding);
901
+ };
902
+ const keyup = (event) => {
903
+ event.preventDefault();
904
+ event.stopImmediatePropagation();
905
+ const binding = recorder.up(event);
906
+ if (binding) choose(binding);
907
+ };
908
+ const blur = () => setCapture(false);
909
+ window.addEventListener("keydown", keydown, true);
910
+ window.addEventListener("keyup", keyup, true);
911
+ window.addEventListener("blur", blur);
912
+ return () => {
913
+ window.removeEventListener("keydown", keydown, true);
914
+ window.removeEventListener("keyup", keyup, true);
915
+ window.removeEventListener("blur", blur);
916
+ };
917
+ }, [capture]);
918
+ const update = (key2, value) => {
919
+ dirty.current = true;
920
+ setForm((p) => ({ ...p, [key2]: value }));
921
+ };
922
+ const applySavedSettings = (value) => {
923
+ dirty.current = false;
924
+ setForm(value);
925
+ setRevision(value.revision);
926
+ setKey("");
927
+ };
928
+ const saveConfiguration = () => {
929
+ setBusy(true);
930
+ setSaving(true);
931
+ setMessage("");
932
+ void settings.save(form, revision, key.trim() || void 0).then((value) => {
933
+ applySavedSettings(value);
934
+ setMessage("\u8BBE\u7F6E\u5DF2\u4FDD\u5B58");
935
+ }).catch((error2) => setError(error2.message)).finally(() => {
936
+ setBusy(false);
937
+ setSaving(false);
938
+ });
939
+ };
940
+ const resetConfiguration = () => {
941
+ pendingDownload.current = null;
942
+ setBusy(true);
943
+ setCapture(false);
944
+ setMessage("");
945
+ void settings.reset().then((value) => {
946
+ applySavedSettings(value);
947
+ setAvailable(false);
948
+ setProgress(null);
949
+ setManagementMessage("");
950
+ setMessage("\u5DF2\u6E05\u9664\u4FDD\u5B58\u7684\u914D\u7F6E");
951
+ }).catch((error2) => setError(error2.message)).finally(() => setBusy(false));
952
+ };
953
+ const manage = (action) => {
954
+ setProgress(action === "update" ? { phase: "checking", received: 0 } : null);
955
+ setUpdating(action === "update");
956
+ setBusy(true);
957
+ setManagementMessage(action === "check" ? "\u6B63\u5728\u68C0\u67E5\u66F4\u65B0\u2026" : "\u6B63\u5728\u4E0B\u8F7D\u5E76\u5B89\u88C5\u65B0\u7248\uFF0C\u8BF7\u7B49\u5F85\u2026");
958
+ void settings.manage(action, form).then((result) => {
959
+ if (action === "check") {
960
+ setAvailable(!!result.available);
961
+ setLatestVersion(result.latest ?? "");
962
+ setManagementMessage(result.available ? `\u53D1\u73B0\u65B0\u7248 ${result.latest}` : "\u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C");
963
+ } else {
964
+ setManagementMessage(result.message ?? "\u64CD\u4F5C\u5DF2\u5B8C\u6210");
965
+ setAvailable(false);
966
+ setProgress((p) => ({ ...p, received: p?.received ?? 0, phase: "done" }));
967
+ }
968
+ }).catch((error2) => {
969
+ setManagementMessage("");
970
+ setProgress(null);
971
+ setError(error2.message);
972
+ }).finally(() => {
973
+ setBusy(false);
974
+ setUpdating(false);
975
+ });
976
+ };
977
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "listener-settings", "data-listener-settings": true, children: [
978
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "\u8BED\u97F3\u8F93\u5165" }),
979
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "dsh-listener: \u8F7B\u91CF\u5316\u4E91\u7AEF\u5B9E\u65F6\u8BC6\u522B\u8BED\u97F3" }),
980
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "listener-field", children: [
981
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "API Key" }),
982
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "password", autoComplete: "off", value: key, placeholder: saved?.configured ? "\u5DF2\u914D\u7F6E\uFF1B\u7559\u7A7A\u4FDD\u7559\u539F\u5BC6\u94A5" : "\u8F93\u5165\u767E\u70BC API Key", onChange: (e) => setKey(e.target.value) })
983
+ ] }),
984
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "listener-field", children: [
985
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u5730\u57DF" }),
986
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { value: form.region, onChange: (e) => update("region", e.target.value), children: [
987
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "beijing", children: "\u5317\u4EAC" }),
988
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "singapore", children: "\u65B0\u52A0\u5761" })
989
+ ] })
990
+ ] }),
991
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "listener-field", children: [
992
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Workspace ID" }),
993
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
994
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "aria-label": "Workspace ID", "aria-describedby": "listener-workspace-hint", value: form.workspaceId, placeholder: "\u7A7A\u95F4 ID\uFF0C\u6216\u7C98\u8D34\u5B8C\u6574 API Host", onChange: (e) => {
995
+ const value = e.target.value.trim(), workspace = workspaceFromHost(value);
996
+ if (workspace) {
997
+ dirty.current = true;
998
+ setForm((p) => ({ ...p, ...workspace }));
999
+ setMessage("\u5DF2\u4ECE API Host \u63D0\u53D6\u7A7A\u95F4 ID \u548C\u5730\u57DF\uFF0C\u70B9\u51FB\u4FDD\u5B58\u751F\u6548");
1000
+ } else update("workspaceId", value);
1001
+ } }),
1002
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { id: "listener-workspace-hint", className: "listener-hint", children: "\u5730\u57DF\u548C Workspace ID \u5FC5\u987B\u4E0E\u767E\u70BC\u5BC6\u94A5\u4E00\u81F4\u3002" })
1003
+ ] })
1004
+ ] }),
1005
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "listener-field", children: [
1006
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u8BC6\u522B\u6A21\u578B" }),
1007
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1008
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "aria-label": "\u8BC6\u522B\u6A21\u578B", "aria-describedby": "listener-model-hint", value: form.model, onChange: (e) => update("model", e.target.value.trim()) }),
1009
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { id: "listener-model-hint", className: "listener-hint", children: "\u6A21\u578B\u9700\u652F\u6301 DashScope \u6D41\u5F0F\u8BC6\u522B\u534F\u8BAE\uFF0C\u97F3\u9891\u4E0D\u5199\u5165\u78C1\u76D8\u3002" })
1010
+ ] })
1011
+ ] }),
1012
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "listener-field", children: [
1013
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u5FEB\u6377\u952E" }),
1014
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "listener-shortcut", children: [
1015
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { "aria-label": "\u5FEB\u6377\u952E", value: form.hotkey, readOnly: true }),
1016
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "listener-action", onClick: () => setCapture(!capture), children: capture ? "\u8BF7\u6309\u5FEB\u6377\u952E" : "\u5F55\u5165\u5FEB\u6377\u952E" })
1017
+ ] })
1018
+ ] }),
1019
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "listener-field", children: [
1020
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u5F55\u97F3\u6A21\u5F0F" }),
1021
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { value: form.mode, onChange: (e) => update("mode", e.target.value), children: [
1022
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "hold", children: "\u957F\u6309\uFF1A\u6309\u4E0B\u5F00\u59CB\uFF0C\u677E\u5F00\u505C\u6B62" }),
1023
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "toggle", children: "\u70B9\u6309\uFF1A\u518D\u6B21\u6309\u4E0B\u505C\u6B62" })
1024
+ ] })
1025
+ ] }),
1026
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "listener-field", children: [
1027
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u81EA\u52A8\u53D1\u9001" }),
1028
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: form.autoSend, onChange: (e) => update("autoSend", e.target.checked) })
1029
+ ] }),
1030
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "listener-actions", children: [
1031
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "listener-action listener-primary", type: "button", disabled: busy || !saved?.writable, onClick: saveConfiguration, children: saving ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58" }),
1032
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "listener-action", disabled: busy || !saved?.writable, onClick: resetConfiguration, children: "\u6E05\u9664\u5DF2\u4FDD\u5B58\u914D\u7F6E" }),
1033
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "listener-action", disabled: busy, onClick: () => {
1034
+ dirty.current = false;
1035
+ void settings.refresh().catch((error2) => setError(error2.message));
1036
+ }, children: "\u5237\u65B0" })
1037
+ ] }),
1038
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", children: saved && !saved.writable ? "\u5F53\u524D Harness \u914D\u7F6E\u53EA\u8BFB\u3002" : message }),
1039
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { open: true, children: [
1040
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("summary", { children: [
1041
+ "\u63D2\u4EF6\u7BA1\u7406 \xB7 v",
1042
+ VERSION
1043
+ ] }),
1044
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "listener-hint", children: "\u66F4\u65B0\u6765\u6E90\uFF1Anpm\u3002\u53D1\u5E03\u65B0\u7248\u540E\uFF0C\u70B9\u51FB\u68C0\u67E5\u66F4\u65B0\u5373\u53EF\u53D1\u73B0\u3002" }),
1045
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "listener-field", children: [
1046
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u4E0B\u8F7D\u6765\u6E90" }),
1047
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { disabled: busy || !saved?.writable, value: form.updateSource, onChange: (e) => {
1048
+ update("updateSource", e.target.value);
1049
+ setAvailable(false);
1050
+ setManagementMessage("");
1051
+ }, children: [
1052
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "official", children: "npm \u5B98\u65B9" }),
1053
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "mirror", children: "npm \u955C\u50CF" })
1054
+ ] })
1055
+ ] }),
1056
+ form.updateSource === "mirror" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "listener-field", children: [
1057
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u955C\u50CF\u7F51\u5740" }),
1058
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "url", disabled: busy || !saved?.writable, value: form.mirrorUrl, placeholder: "https://registry.npmmirror.com", onChange: (e) => {
1059
+ update("mirrorUrl", e.target.value);
1060
+ setAvailable(false);
1061
+ setManagementMessage("");
1062
+ } })
1063
+ ] }) }),
1064
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "listener-actions", children: [
1065
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "listener-action", disabled: busy, onClick: () => manage("check"), children: "\u68C0\u67E5\u66F4\u65B0" }),
1066
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "listener-action", disabled: busy || !available, onClick: () => manage("update"), children: available ? `\u66F4\u65B0\u81F3 ${latestVersion}` : "\u66F4\u65B0" })
1067
+ ] }),
1068
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", "aria-label": "\u66F4\u65B0\u72B6\u6001", children: managementMessage }),
1069
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(UpdateIndicator, { progress })
1070
+ ] }),
1071
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageDialog, { message: error, onClose: () => setError(""), onSettings: () => setError("") })
1072
+ ] });
1073
+ }
1074
+ function apply(ctx) {
1075
+ ctx.effect(() => {
1076
+ const sheet = document.createElement("style");
1077
+ sheet.textContent = style;
1078
+ document.head.append(sheet);
1079
+ return () => sheet.remove();
1080
+ });
1081
+ ctx.slots.inject("conversation.input.right", () => ctx.slots.register({ name: "conversation.input.right", id: "dsh-listener", order: 90 }, VoiceButton));
1082
+ const bundleSlots = ctx.slots;
1083
+ bundleSlots.inject("plugins.bundle.config", () => bundleSlots.register({
1084
+ name: "plugins.bundle.config",
1085
+ key: "dsh-listener"
1086
+ }, ({ view }) => view === "summary" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VoiceSettings, {})));
1087
+ ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({ name: "settings.plugins.tab", id: "dsh-listener", order: 65, label: () => "\u8BED\u97F3\u8F93\u5165" }, VoiceSettings));
1088
+ ctx.effect(() => () => {
1089
+ for (const session of sessions) session.cancel();
1090
+ sessions.clear();
1091
+ owner = void 0;
1092
+ });
1093
+ void settings.refresh().catch(() => void 0);
1094
+ }
1095
+ return module.exports; } });