@h-ai/serv 0.1.0-alpha.35 → 0.1.0-alpha.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/dist/features/ai.d.ts +4 -4
- package/dist/features/iam.d.ts +2 -0
- package/dist/features/iam.js +6 -2
- package/dist/features/iam.js.map +1 -1
- package/dist/index.d.ts +77 -6
- package/dist/index.js +277 -75
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { servM, mapHaiError, requireAuth, normalizeServLocale, buildHaiErrorBody
|
|
|
3
3
|
import { implement } from '@orpc/server';
|
|
4
4
|
import process2 from 'process';
|
|
5
5
|
import { serve } from '@hono/node-server';
|
|
6
|
-
import { AUDIO_WS_PATH } from '@h-ai/
|
|
6
|
+
import { AUDIO_WS_PATH, IAM_AUTH_ROUTES, AudioWsClientMessageSchema } from '@h-ai/api-contract';
|
|
7
7
|
import { core, ok, err, HaiCommonError } from '@h-ai/core';
|
|
8
8
|
import { createNodeWebSocket } from '@hono/node-ws';
|
|
9
9
|
import { OpenAPIHandler } from '@orpc/openapi/fetch';
|
|
@@ -11,7 +11,6 @@ import { RPCHandler } from '@orpc/server/fetch';
|
|
|
11
11
|
import { Hono } from 'hono';
|
|
12
12
|
import { Buffer } from 'buffer';
|
|
13
13
|
import { z } from 'zod';
|
|
14
|
-
import { IAM_AUTH_ROUTES } from '@h-ai/api-contract';
|
|
15
14
|
import { getCookie } from 'hono/cookie';
|
|
16
15
|
import { readFile } from 'fs/promises';
|
|
17
16
|
import { OpenAPIGenerator } from '@orpc/openapi';
|
|
@@ -29,25 +28,39 @@ function toFetch(app) {
|
|
|
29
28
|
var logger = core.logger.child({ module: "serv", scope: "audio-ws" });
|
|
30
29
|
var DEFAULT_MAX_MESSAGE_BYTES = 1048576;
|
|
31
30
|
var DEFAULT_MAX_BUFFERED_BYTES = 10 * 1024 * 1024;
|
|
31
|
+
var DEFAULT_MAX_TEXT_BYTES = 1 * 1024 * 1024;
|
|
32
32
|
var DEFAULT_MAX_SESSION_MS = 5 * 60 * 1e3;
|
|
33
|
+
var DEFAULT_PRE_AUTH_TIMEOUT_MS = 5e3;
|
|
34
|
+
var DEFAULT_MAX_PENDING_MESSAGES = 256;
|
|
35
|
+
var DEFAULT_MAX_SEND_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
36
|
+
var MAX_QUEUE_ITEMS = 4096;
|
|
37
|
+
var CODE_PROTOCOL = "hai:ai:050";
|
|
38
|
+
var CODE_TICKET = "hai:ai:054";
|
|
39
|
+
var CODE_TOO_LARGE = "hai:ai:058";
|
|
40
|
+
var CODE_TIMEOUT = "hai:ai:057";
|
|
33
41
|
function registerAudioWsRoute(app, path, upgradeWebSocket, deps) {
|
|
34
|
-
const
|
|
35
|
-
|
|
42
|
+
const limits = {
|
|
43
|
+
maxMessageBytes: deps.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES,
|
|
44
|
+
maxBufferedBytes: deps.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES,
|
|
45
|
+
maxTextBytes: deps.maxTextBytes ?? DEFAULT_MAX_TEXT_BYTES,
|
|
46
|
+
maxPendingMessages: deps.maxPendingMessages ?? DEFAULT_MAX_PENDING_MESSAGES,
|
|
47
|
+
maxSendBufferBytes: deps.maxSendBufferBytes ?? DEFAULT_MAX_SEND_BUFFER_BYTES
|
|
48
|
+
};
|
|
36
49
|
const maxSessionMs = deps.maxSessionMs ?? DEFAULT_MAX_SESSION_MS;
|
|
50
|
+
const preAuthTimeoutMs = deps.preAuthTimeoutMs ?? DEFAULT_PRE_AUTH_TIMEOUT_MS;
|
|
37
51
|
app.get(path, upgradeWebSocket((c) => {
|
|
38
|
-
const
|
|
39
|
-
const conn = new AudioConnection(deps,
|
|
52
|
+
const ticket = c.req.query("ticket");
|
|
53
|
+
const conn = new AudioConnection(deps, ticket, limits);
|
|
40
54
|
let sessionTimer;
|
|
41
55
|
return {
|
|
42
56
|
onOpen(_evt, ws) {
|
|
57
|
+
conn.begin(ws, preAuthTimeoutMs);
|
|
43
58
|
sessionTimer = setTimeout(() => {
|
|
44
|
-
conn.fail(ws,
|
|
59
|
+
conn.fail(ws, CODE_TIMEOUT, "audio session timeout");
|
|
45
60
|
}, maxSessionMs);
|
|
46
61
|
},
|
|
47
62
|
onMessage(evt, ws) {
|
|
48
|
-
conn.
|
|
49
|
-
logger.debug("audio ws message handling failed", { error: error instanceof Error ? error.message : String(error) });
|
|
50
|
-
});
|
|
63
|
+
conn.enqueue(ws, evt.data);
|
|
51
64
|
},
|
|
52
65
|
onClose() {
|
|
53
66
|
if (sessionTimer)
|
|
@@ -63,19 +76,26 @@ function registerAudioWsRoute(app, path, upgradeWebSocket, deps) {
|
|
|
63
76
|
}));
|
|
64
77
|
}
|
|
65
78
|
var AsyncQueue = class {
|
|
79
|
+
constructor(maxItems) {
|
|
80
|
+
this.maxItems = maxItems;
|
|
81
|
+
}
|
|
66
82
|
items = [];
|
|
67
83
|
waiting = null;
|
|
68
84
|
ended = false;
|
|
85
|
+
/** 入队;超过上限返回 false(背压信号,调用方据此关闭连接)。 */
|
|
69
86
|
push(item) {
|
|
70
87
|
if (this.ended)
|
|
71
|
-
return;
|
|
88
|
+
return true;
|
|
72
89
|
if (this.waiting) {
|
|
73
90
|
const w = this.waiting;
|
|
74
91
|
this.waiting = null;
|
|
75
92
|
w({ value: item, done: false });
|
|
76
|
-
|
|
77
|
-
this.items.push(item);
|
|
93
|
+
return true;
|
|
78
94
|
}
|
|
95
|
+
if (this.items.length >= this.maxItems)
|
|
96
|
+
return false;
|
|
97
|
+
this.items.push(item);
|
|
98
|
+
return true;
|
|
79
99
|
}
|
|
80
100
|
end() {
|
|
81
101
|
if (this.ended)
|
|
@@ -105,92 +125,230 @@ var AsyncQueue = class {
|
|
|
105
125
|
}
|
|
106
126
|
};
|
|
107
127
|
var AudioConnection = class {
|
|
108
|
-
constructor(deps,
|
|
128
|
+
constructor(deps, ticket, limits) {
|
|
109
129
|
this.deps = deps;
|
|
110
|
-
this.
|
|
111
|
-
this.
|
|
112
|
-
this.
|
|
130
|
+
this.ticket = ticket;
|
|
131
|
+
this.limits = limits;
|
|
132
|
+
this.audioQueue = new AsyncQueue(MAX_QUEUE_ITEMS);
|
|
133
|
+
this.textQueue = new AsyncQueue(MAX_QUEUE_ITEMS);
|
|
113
134
|
}
|
|
114
|
-
|
|
135
|
+
state = "connecting";
|
|
115
136
|
start;
|
|
116
|
-
|
|
117
|
-
|
|
137
|
+
session;
|
|
138
|
+
grant;
|
|
139
|
+
authorizedRequest;
|
|
140
|
+
doneReceived = false;
|
|
141
|
+
audioQueue;
|
|
142
|
+
textQueue;
|
|
118
143
|
audioBuffer = [];
|
|
119
144
|
receivedBytes = 0;
|
|
145
|
+
receivedTextBytes = 0;
|
|
146
|
+
/** 合成文本段 ID 去重(同一会话内唯一)。 */
|
|
147
|
+
seenSegmentIds = /* @__PURE__ */ new Set();
|
|
120
148
|
controller = new AbortController();
|
|
121
149
|
closed = false;
|
|
150
|
+
preAuthTimer;
|
|
151
|
+
/** 待处理消息数(含鉴权任务):积压过多即关闭。 */
|
|
152
|
+
pending = 0;
|
|
153
|
+
/** 消息串行处理链:确保同一连接的消息按到达顺序处理,鉴权完成前不处理业务帧。 */
|
|
154
|
+
queue = Promise.resolve();
|
|
155
|
+
/** 连接建立:立即鉴权(串行链首个任务)并启动预鉴权超时。 */
|
|
156
|
+
begin(ws, preAuthTimeoutMs) {
|
|
157
|
+
this.preAuthTimer = setTimeout(() => {
|
|
158
|
+
if (this.state === "connecting" || this.state === "authenticated" || this.state === "awaiting_start")
|
|
159
|
+
this.fail(ws, CODE_TIMEOUT, "pre-auth timeout: authenticate and send start in time");
|
|
160
|
+
}, preAuthTimeoutMs);
|
|
161
|
+
this.chain(ws, () => this.authenticate(ws));
|
|
162
|
+
}
|
|
163
|
+
/** 将任务追加到串行处理链(含积压上限保护)。 */
|
|
164
|
+
chain(ws, task) {
|
|
165
|
+
if (this.closed)
|
|
166
|
+
return;
|
|
167
|
+
this.pending++;
|
|
168
|
+
if (this.pending > this.limits.maxPendingMessages) {
|
|
169
|
+
this.pending--;
|
|
170
|
+
this.fail(ws, CODE_TOO_LARGE, "too many pending messages");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.queue = this.queue.then(() => task()).catch((error) => {
|
|
174
|
+
logger.debug("audio ws task failed", { error: error instanceof Error ? error.message : String(error) });
|
|
175
|
+
if (!this.closed)
|
|
176
|
+
this.failFromError(ws, error);
|
|
177
|
+
}).finally(() => {
|
|
178
|
+
this.pending--;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** 将消息追加到串行处理链,保证按序处理(鉴权完成前不处理业务帧)。 */
|
|
182
|
+
enqueue(ws, data) {
|
|
183
|
+
this.chain(ws, () => this.handleMessage(ws, data));
|
|
184
|
+
}
|
|
185
|
+
/** 连接建立即执行:校验并原子消费 ticket,成功后进入 awaiting_start。 */
|
|
186
|
+
async authenticate(ws) {
|
|
187
|
+
if (this.closed)
|
|
188
|
+
return;
|
|
189
|
+
if (!this.ticket) {
|
|
190
|
+
this.fail(ws, CODE_TICKET, "missing audio ticket");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const verified = await this.deps.verifyTicket(this.ticket);
|
|
194
|
+
if (this.closed)
|
|
195
|
+
return;
|
|
196
|
+
if (!verified.success) {
|
|
197
|
+
this.fail(ws, String(verified.error.code), verified.error.message);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.session = verified.data.session;
|
|
201
|
+
this.grant = verified.data.grant;
|
|
202
|
+
this.state = "awaiting_start";
|
|
203
|
+
}
|
|
122
204
|
async handleMessage(ws, data) {
|
|
123
205
|
if (this.closed)
|
|
124
206
|
return;
|
|
125
207
|
const bytes = toBytes(data);
|
|
126
208
|
if (bytes) {
|
|
127
|
-
|
|
128
|
-
this.fail(ws, "hai:ai:050", "audio frame before start");
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (bytes.length > this.maxMessageBytes) {
|
|
132
|
-
this.fail(ws, "hai:ai:058", "audio frame too large");
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
this.receivedBytes += bytes.length;
|
|
136
|
-
if (this.receivedBytes > this.maxBufferedBytes) {
|
|
137
|
-
this.fail(ws, "hai:ai:058", "buffered audio exceeds limit");
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (this.start?.stream)
|
|
141
|
-
this.audioQueue.push(bytes);
|
|
142
|
-
else
|
|
143
|
-
this.audioBuffer.push(bytes);
|
|
209
|
+
this.handleAudioFrame(ws, bytes);
|
|
144
210
|
return;
|
|
145
211
|
}
|
|
146
212
|
if (typeof data !== "string")
|
|
147
213
|
return;
|
|
148
|
-
if (data
|
|
149
|
-
this.fail(ws,
|
|
214
|
+
if (Buffer.byteLength(data, "utf8") > this.limits.maxMessageBytes) {
|
|
215
|
+
this.fail(ws, CODE_TOO_LARGE, "message too large");
|
|
150
216
|
return;
|
|
151
217
|
}
|
|
152
|
-
let
|
|
218
|
+
let raw;
|
|
153
219
|
try {
|
|
154
|
-
|
|
220
|
+
raw = JSON.parse(data);
|
|
155
221
|
} catch {
|
|
156
|
-
this.fail(ws,
|
|
222
|
+
this.fail(ws, CODE_PROTOCOL, "invalid json message");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const parsed = AudioWsClientMessageSchema.safeParse(raw);
|
|
226
|
+
if (!parsed.success) {
|
|
227
|
+
this.fail(ws, CODE_PROTOCOL, `invalid message: ${parsed.error.issues[0]?.message ?? "schema validation failed"}`);
|
|
157
228
|
return;
|
|
158
229
|
}
|
|
230
|
+
const message = parsed.data;
|
|
159
231
|
if (message.type === "start") {
|
|
160
232
|
await this.onStart(ws, message);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
this.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (this.state === "connecting" || this.state === "authenticated" || this.state === "awaiting_start") {
|
|
236
|
+
this.fail(ws, CODE_PROTOCOL, "message before start");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (message.type === "text")
|
|
240
|
+
this.handleTextFrame(ws, message);
|
|
241
|
+
else if (message.type === "done")
|
|
242
|
+
this.handleDone(ws);
|
|
243
|
+
}
|
|
244
|
+
/** 处理音频二进制帧(仅识别操作、输入未结束时接受)。 */
|
|
245
|
+
handleAudioFrame(ws, bytes) {
|
|
246
|
+
if (this.state !== "transcribing") {
|
|
247
|
+
this.fail(ws, CODE_PROTOCOL, "unexpected audio frame");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (bytes.length > this.limits.maxMessageBytes) {
|
|
251
|
+
this.fail(ws, CODE_TOO_LARGE, "audio frame too large");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
this.receivedBytes += bytes.length;
|
|
255
|
+
if (this.receivedBytes > this.limits.maxBufferedBytes) {
|
|
256
|
+
this.fail(ws, CODE_TOO_LARGE, "buffered audio exceeds limit");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (this.start?.stream) {
|
|
260
|
+
if (!this.audioQueue.push(bytes))
|
|
261
|
+
this.fail(ws, CODE_TOO_LARGE, "audio backpressure limit exceeded");
|
|
262
|
+
} else {
|
|
263
|
+
this.audioBuffer.push(bytes);
|
|
170
264
|
}
|
|
171
265
|
}
|
|
266
|
+
/** 处理文本控制帧(仅合成操作、输入未结束时接受;segmentId 唯一)。 */
|
|
267
|
+
handleTextFrame(ws, message) {
|
|
268
|
+
if (this.state !== "synthesizing") {
|
|
269
|
+
this.fail(ws, CODE_PROTOCOL, "unexpected text frame");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (this.seenSegmentIds.has(message.segmentId)) {
|
|
273
|
+
this.fail(ws, CODE_PROTOCOL, `duplicate segmentId: ${message.segmentId}`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
this.receivedTextBytes += Buffer.byteLength(message.text, "utf8");
|
|
277
|
+
if (this.receivedTextBytes > this.limits.maxTextBytes) {
|
|
278
|
+
this.fail(ws, CODE_TOO_LARGE, "buffered text exceeds limit");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
this.seenSegmentIds.add(message.segmentId);
|
|
282
|
+
if (!this.textQueue.push({ id: message.segmentId, text: message.text }))
|
|
283
|
+
this.fail(ws, CODE_TOO_LARGE, "text backpressure limit exceeded");
|
|
284
|
+
}
|
|
285
|
+
/** 处理输入结束帧(只允许一次)。 */
|
|
286
|
+
handleDone(ws) {
|
|
287
|
+
if (this.doneReceived) {
|
|
288
|
+
this.fail(ws, CODE_PROTOCOL, "duplicate done");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.doneReceived = true;
|
|
292
|
+
this.audioQueue.end();
|
|
293
|
+
this.textQueue.end();
|
|
294
|
+
if (this.state === "transcribing" || this.state === "synthesizing")
|
|
295
|
+
this.state = "input_completed";
|
|
296
|
+
if (this.start && !this.start.stream && this.start.operation === "transcribe")
|
|
297
|
+
void this.runBufferedTranscribe(ws);
|
|
298
|
+
}
|
|
172
299
|
async onStart(ws, message) {
|
|
173
|
-
if (this.
|
|
174
|
-
this.fail(ws,
|
|
300
|
+
if (this.state === "connecting" || this.state === "authenticated") {
|
|
301
|
+
this.fail(ws, CODE_TICKET, "not authenticated");
|
|
175
302
|
return;
|
|
176
303
|
}
|
|
177
|
-
this.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
304
|
+
if (this.state !== "awaiting_start") {
|
|
305
|
+
this.fail(ws, CODE_PROTOCOL, "session already started");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const session = this.session;
|
|
309
|
+
if (!session) {
|
|
310
|
+
this.fail(ws, CODE_TICKET, "not authenticated");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const authorized = this.deps.authorize ? await this.deps.authorize(session, message, this.grant) : this.defaultAuthorize(message);
|
|
314
|
+
if (this.closed)
|
|
315
|
+
return;
|
|
316
|
+
if (!authorized.success) {
|
|
317
|
+
this.fail(ws, String(authorized.error.code), authorized.error.message);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const confirmed = {
|
|
321
|
+
...message,
|
|
322
|
+
operation: authorized.data.operation,
|
|
323
|
+
model: authorized.data.model,
|
|
324
|
+
voice: authorized.data.voice,
|
|
325
|
+
instruction: authorized.data.instruction,
|
|
326
|
+
format: authorized.data.format,
|
|
327
|
+
sampleRate: authorized.data.sampleRate
|
|
328
|
+
};
|
|
329
|
+
this.start = confirmed;
|
|
330
|
+
this.authorizedRequest = authorized.data;
|
|
331
|
+
this.state = confirmed.operation === "synthesize" ? "synthesizing" : "transcribing";
|
|
332
|
+
if (this.preAuthTimer) {
|
|
333
|
+
clearTimeout(this.preAuthTimer);
|
|
334
|
+
this.preAuthTimer = void 0;
|
|
189
335
|
}
|
|
190
|
-
if (
|
|
191
|
-
void this.runSynthesize(ws,
|
|
192
|
-
else if (
|
|
193
|
-
void this.runStreamTranscribe(ws,
|
|
336
|
+
if (confirmed.operation === "synthesize")
|
|
337
|
+
void this.runSynthesize(ws, confirmed);
|
|
338
|
+
else if (confirmed.operation === "transcribe" && confirmed.stream)
|
|
339
|
+
void this.runStreamTranscribe(ws, confirmed);
|
|
340
|
+
}
|
|
341
|
+
/** 无 authorize 回调时的默认授权:仅保留操作与格式,并交叉校验票据 grant。 */
|
|
342
|
+
defaultAuthorize(message) {
|
|
343
|
+
if (this.grant?.operation && this.grant.operation !== message.operation) {
|
|
344
|
+
return err(HaiCommonError.FORBIDDEN, "operation not permitted by ticket");
|
|
345
|
+
}
|
|
346
|
+
return ok({
|
|
347
|
+
operation: message.operation,
|
|
348
|
+
// 无 authorize 回调时不透传客户端 model/voice/instruction;仅采用票据 grant 绑定的 model
|
|
349
|
+
model: this.grant?.model,
|
|
350
|
+
format: message.format
|
|
351
|
+
});
|
|
194
352
|
}
|
|
195
353
|
/** 流式识别:桥接持续音频输入,流式返回临时/最终结果与语音起止事件 */
|
|
196
354
|
async runStreamTranscribe(ws, start) {
|
|
@@ -208,6 +366,8 @@ var AudioConnection = class {
|
|
|
208
366
|
signal: this.controller.signal
|
|
209
367
|
});
|
|
210
368
|
for await (const event of stream) {
|
|
369
|
+
if (this.closed)
|
|
370
|
+
return;
|
|
211
371
|
if (event.type === "transcript")
|
|
212
372
|
this.send(ws, { type: "transcript", text: event.text, final: event.final });
|
|
213
373
|
else if (event.type === "speech_started")
|
|
@@ -238,6 +398,8 @@ var AudioConnection = class {
|
|
|
238
398
|
model: start.model,
|
|
239
399
|
signal: this.controller.signal
|
|
240
400
|
});
|
|
401
|
+
if (this.closed)
|
|
402
|
+
return;
|
|
241
403
|
if (!result.success) {
|
|
242
404
|
this.fail(ws, String(result.error.code), result.error.message);
|
|
243
405
|
return;
|
|
@@ -248,7 +410,7 @@ var AudioConnection = class {
|
|
|
248
410
|
this.failFromError(ws, error);
|
|
249
411
|
}
|
|
250
412
|
}
|
|
251
|
-
/**
|
|
413
|
+
/** 语音合成:桥接文本输入,流式返回音频二进制帧(带发送缓冲背压) */
|
|
252
414
|
async runSynthesize(ws, start) {
|
|
253
415
|
try {
|
|
254
416
|
const stream = this.deps.ai.audio.synthesizeStream({
|
|
@@ -260,25 +422,44 @@ var AudioConnection = class {
|
|
|
260
422
|
model: start.model,
|
|
261
423
|
signal: this.controller.signal
|
|
262
424
|
});
|
|
263
|
-
for await (const
|
|
264
|
-
|
|
425
|
+
for await (const event of stream) {
|
|
426
|
+
if (this.closed)
|
|
427
|
+
return;
|
|
428
|
+
if (event.type === "audio") {
|
|
429
|
+
if (sendBufferedAmount(ws) > this.limits.maxSendBufferBytes) {
|
|
430
|
+
this.fail(ws, CODE_TOO_LARGE, "client send buffer exceeded high-water mark");
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
ws.send(toArrayBuffer(event.data));
|
|
434
|
+
} else {
|
|
435
|
+
this.send(ws, event);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
265
438
|
this.end(ws);
|
|
266
439
|
} catch (error) {
|
|
267
440
|
this.failFromError(ws, error);
|
|
268
441
|
}
|
|
269
442
|
}
|
|
270
443
|
fail(ws, code, message) {
|
|
444
|
+
if (this.closed)
|
|
445
|
+
return;
|
|
446
|
+
this.state = "failed";
|
|
271
447
|
this.send(ws, { type: "error", code, message });
|
|
272
448
|
this.dispose();
|
|
273
449
|
ws.close();
|
|
274
450
|
}
|
|
275
451
|
failFromError(ws, error) {
|
|
452
|
+
if (this.closed)
|
|
453
|
+
return;
|
|
276
454
|
const hai = error;
|
|
277
|
-
const code = typeof hai?.code === "string" ? hai.code :
|
|
455
|
+
const code = typeof hai?.code === "string" ? hai.code : CODE_TICKET;
|
|
278
456
|
const message = error instanceof Error ? error.message : String(error);
|
|
279
457
|
this.fail(ws, code, message);
|
|
280
458
|
}
|
|
281
459
|
end(ws) {
|
|
460
|
+
if (this.closed)
|
|
461
|
+
return;
|
|
462
|
+
this.state = "completed";
|
|
282
463
|
this.send(ws, { type: "end" });
|
|
283
464
|
this.dispose();
|
|
284
465
|
ws.close();
|
|
@@ -291,11 +472,26 @@ var AudioConnection = class {
|
|
|
291
472
|
if (this.closed)
|
|
292
473
|
return;
|
|
293
474
|
this.closed = true;
|
|
475
|
+
if (this.state !== "completed" && this.state !== "failed")
|
|
476
|
+
this.state = "cancelled";
|
|
477
|
+
if (this.preAuthTimer) {
|
|
478
|
+
clearTimeout(this.preAuthTimer);
|
|
479
|
+
this.preAuthTimer = void 0;
|
|
480
|
+
}
|
|
294
481
|
this.audioQueue.end();
|
|
295
482
|
this.textQueue.end();
|
|
296
483
|
this.controller.abort();
|
|
484
|
+
if (this.session && this.authorizedRequest && this.deps.onSessionEnd) {
|
|
485
|
+
void Promise.resolve(this.deps.onSessionEnd(this.session, this.authorizedRequest)).catch((error) => {
|
|
486
|
+
logger.warn("audio ws session end hook failed", { error: error instanceof Error ? error.message : String(error) });
|
|
487
|
+
});
|
|
488
|
+
}
|
|
297
489
|
}
|
|
298
490
|
};
|
|
491
|
+
function sendBufferedAmount(ws) {
|
|
492
|
+
const raw = ws.raw;
|
|
493
|
+
return typeof raw?.bufferedAmount === "number" ? raw.bufferedAmount : 0;
|
|
494
|
+
}
|
|
299
495
|
function toBytes(data) {
|
|
300
496
|
if (typeof data === "string")
|
|
301
497
|
return void 0;
|
|
@@ -927,10 +1123,16 @@ function createApp(options) {
|
|
|
927
1123
|
const audioPath = `${http.apiPrefix}${options.audio.path ?? AUDIO_WS_PATH}`;
|
|
928
1124
|
registerAudioWsRoute(app, audioPath, upgradeWebSocket, {
|
|
929
1125
|
ai: options.audio.ai,
|
|
930
|
-
|
|
1126
|
+
verifyTicket: options.audio.verifyTicket,
|
|
1127
|
+
authorize: options.audio.authorize,
|
|
1128
|
+
onSessionEnd: options.audio.onSessionEnd,
|
|
931
1129
|
maxMessageBytes: options.audio.maxMessageBytes,
|
|
932
1130
|
maxBufferedBytes: options.audio.maxBufferedBytes,
|
|
933
|
-
|
|
1131
|
+
maxTextBytes: options.audio.maxTextBytes,
|
|
1132
|
+
maxSessionMs: options.audio.maxSessionMs,
|
|
1133
|
+
preAuthTimeoutMs: options.audio.preAuthTimeoutMs,
|
|
1134
|
+
maxPendingMessages: options.audio.maxPendingMessages,
|
|
1135
|
+
maxSendBufferBytes: options.audio.maxSendBufferBytes
|
|
934
1136
|
});
|
|
935
1137
|
audioWsInjectors.set(app, injectWebSocket);
|
|
936
1138
|
}
|