@h-ai/serv 0.1.0-alpha.36 → 0.1.0-alpha.38
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 +10 -8
- 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 +47 -9
- package/dist/index.js +230 -73
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/dist/index.js
CHANGED
|
@@ -3,6 +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, IAM_AUTH_ROUTES, AudioWsClientMessageSchema } from '@h-ai/api-contract';
|
|
6
7
|
import { core, ok, err, HaiCommonError } from '@h-ai/core';
|
|
7
8
|
import { createNodeWebSocket } from '@hono/node-ws';
|
|
8
9
|
import { OpenAPIHandler } from '@orpc/openapi/fetch';
|
|
@@ -10,7 +11,6 @@ import { RPCHandler } from '@orpc/server/fetch';
|
|
|
10
11
|
import { Hono } from 'hono';
|
|
11
12
|
import { Buffer } from 'buffer';
|
|
12
13
|
import { z } from 'zod';
|
|
13
|
-
import { IAM_AUTH_ROUTES } from '@h-ai/api-contract';
|
|
14
14
|
import { getCookie } from 'hono/cookie';
|
|
15
15
|
import { readFile } from 'fs/promises';
|
|
16
16
|
import { OpenAPIGenerator } from '@orpc/openapi';
|
|
@@ -30,19 +30,33 @@ var DEFAULT_MAX_MESSAGE_BYTES = 1048576;
|
|
|
30
30
|
var DEFAULT_MAX_BUFFERED_BYTES = 10 * 1024 * 1024;
|
|
31
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
|
-
|
|
36
|
-
|
|
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
|
+
};
|
|
37
49
|
const maxSessionMs = deps.maxSessionMs ?? DEFAULT_MAX_SESSION_MS;
|
|
50
|
+
const preAuthTimeoutMs = deps.preAuthTimeoutMs ?? DEFAULT_PRE_AUTH_TIMEOUT_MS;
|
|
38
51
|
app.get(path, upgradeWebSocket((c) => {
|
|
39
52
|
const ticket = c.req.query("ticket");
|
|
40
|
-
const conn = new AudioConnection(deps, ticket,
|
|
53
|
+
const conn = new AudioConnection(deps, ticket, limits);
|
|
41
54
|
let sessionTimer;
|
|
42
55
|
return {
|
|
43
56
|
onOpen(_evt, ws) {
|
|
57
|
+
conn.begin(ws, preAuthTimeoutMs);
|
|
44
58
|
sessionTimer = setTimeout(() => {
|
|
45
|
-
conn.fail(ws,
|
|
59
|
+
conn.fail(ws, CODE_TIMEOUT, "audio session timeout");
|
|
46
60
|
}, maxSessionMs);
|
|
47
61
|
},
|
|
48
62
|
onMessage(evt, ws) {
|
|
@@ -62,19 +76,26 @@ function registerAudioWsRoute(app, path, upgradeWebSocket, deps) {
|
|
|
62
76
|
}));
|
|
63
77
|
}
|
|
64
78
|
var AsyncQueue = class {
|
|
79
|
+
constructor(maxItems) {
|
|
80
|
+
this.maxItems = maxItems;
|
|
81
|
+
}
|
|
65
82
|
items = [];
|
|
66
83
|
waiting = null;
|
|
67
84
|
ended = false;
|
|
85
|
+
/** 入队;超过上限返回 false(背压信号,调用方据此关闭连接)。 */
|
|
68
86
|
push(item) {
|
|
69
87
|
if (this.ended)
|
|
70
|
-
return;
|
|
88
|
+
return true;
|
|
71
89
|
if (this.waiting) {
|
|
72
90
|
const w = this.waiting;
|
|
73
91
|
this.waiting = null;
|
|
74
92
|
w({ value: item, done: false });
|
|
75
|
-
|
|
76
|
-
this.items.push(item);
|
|
93
|
+
return true;
|
|
77
94
|
}
|
|
95
|
+
if (this.items.length >= this.maxItems)
|
|
96
|
+
return false;
|
|
97
|
+
this.items.push(item);
|
|
98
|
+
return true;
|
|
78
99
|
}
|
|
79
100
|
end() {
|
|
80
101
|
if (this.ended)
|
|
@@ -104,105 +125,194 @@ var AsyncQueue = class {
|
|
|
104
125
|
}
|
|
105
126
|
};
|
|
106
127
|
var AudioConnection = class {
|
|
107
|
-
constructor(deps, ticket,
|
|
128
|
+
constructor(deps, ticket, limits) {
|
|
108
129
|
this.deps = deps;
|
|
109
130
|
this.ticket = ticket;
|
|
110
|
-
this.
|
|
111
|
-
this.
|
|
112
|
-
this.
|
|
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
137
|
session;
|
|
138
|
+
grant;
|
|
117
139
|
authorizedRequest;
|
|
118
|
-
|
|
119
|
-
|
|
140
|
+
doneReceived = false;
|
|
141
|
+
audioQueue;
|
|
142
|
+
textQueue;
|
|
120
143
|
audioBuffer = [];
|
|
121
144
|
receivedBytes = 0;
|
|
122
145
|
receivedTextBytes = 0;
|
|
146
|
+
/** 合成文本段 ID 去重(同一会话内唯一)。 */
|
|
147
|
+
seenSegmentIds = /* @__PURE__ */ new Set();
|
|
123
148
|
controller = new AbortController();
|
|
124
149
|
closed = false;
|
|
150
|
+
preAuthTimer;
|
|
151
|
+
/** 待处理消息数(含鉴权任务):积压过多即关闭。 */
|
|
152
|
+
pending = 0;
|
|
125
153
|
/** 消息串行处理链:确保同一连接的消息按到达顺序处理,鉴权完成前不处理业务帧。 */
|
|
126
154
|
queue = Promise.resolve();
|
|
127
|
-
/**
|
|
128
|
-
|
|
129
|
-
this.
|
|
130
|
-
|
|
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) });
|
|
131
175
|
if (!this.closed)
|
|
132
176
|
this.failFromError(ws, error);
|
|
177
|
+
}).finally(() => {
|
|
178
|
+
this.pending--;
|
|
133
179
|
});
|
|
134
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
|
+
}
|
|
135
204
|
async handleMessage(ws, data) {
|
|
136
205
|
if (this.closed)
|
|
137
206
|
return;
|
|
138
207
|
const bytes = toBytes(data);
|
|
139
208
|
if (bytes) {
|
|
140
|
-
|
|
141
|
-
this.fail(ws, "hai:ai:050", "audio frame before start");
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (bytes.length > this.maxMessageBytes) {
|
|
145
|
-
this.fail(ws, "hai:ai:058", "audio frame too large");
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
this.receivedBytes += bytes.length;
|
|
149
|
-
if (this.receivedBytes > this.maxBufferedBytes) {
|
|
150
|
-
this.fail(ws, "hai:ai:058", "buffered audio exceeds limit");
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (this.start?.stream)
|
|
154
|
-
this.audioQueue.push(bytes);
|
|
155
|
-
else
|
|
156
|
-
this.audioBuffer.push(bytes);
|
|
209
|
+
this.handleAudioFrame(ws, bytes);
|
|
157
210
|
return;
|
|
158
211
|
}
|
|
159
212
|
if (typeof data !== "string")
|
|
160
213
|
return;
|
|
161
|
-
if (data
|
|
162
|
-
this.fail(ws,
|
|
214
|
+
if (Buffer.byteLength(data, "utf8") > this.limits.maxMessageBytes) {
|
|
215
|
+
this.fail(ws, CODE_TOO_LARGE, "message too large");
|
|
163
216
|
return;
|
|
164
217
|
}
|
|
165
|
-
let
|
|
218
|
+
let raw;
|
|
166
219
|
try {
|
|
167
|
-
|
|
220
|
+
raw = JSON.parse(data);
|
|
168
221
|
} catch {
|
|
169
|
-
this.fail(ws,
|
|
222
|
+
this.fail(ws, CODE_PROTOCOL, "invalid json message");
|
|
170
223
|
return;
|
|
171
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"}`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const message = parsed.data;
|
|
172
231
|
if (message.type === "start") {
|
|
173
232
|
await this.onStart(ws, message);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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);
|
|
264
|
+
}
|
|
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;
|
|
188
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);
|
|
189
298
|
}
|
|
190
299
|
async onStart(ws, message) {
|
|
191
|
-
if (this.
|
|
192
|
-
this.fail(ws,
|
|
300
|
+
if (this.state === "connecting" || this.state === "authenticated") {
|
|
301
|
+
this.fail(ws, CODE_TICKET, "not authenticated");
|
|
193
302
|
return;
|
|
194
303
|
}
|
|
195
|
-
if (
|
|
196
|
-
this.fail(ws,
|
|
304
|
+
if (this.state !== "awaiting_start") {
|
|
305
|
+
this.fail(ws, CODE_PROTOCOL, "session already started");
|
|
197
306
|
return;
|
|
198
307
|
}
|
|
199
|
-
const
|
|
200
|
-
if (!
|
|
201
|
-
this.fail(ws,
|
|
308
|
+
const session = this.session;
|
|
309
|
+
if (!session) {
|
|
310
|
+
this.fail(ws, CODE_TICKET, "not authenticated");
|
|
202
311
|
return;
|
|
203
312
|
}
|
|
204
|
-
this.session
|
|
205
|
-
|
|
313
|
+
const authorized = this.deps.authorize ? await this.deps.authorize(session, message, this.grant) : this.defaultAuthorize(message);
|
|
314
|
+
if (this.closed)
|
|
315
|
+
return;
|
|
206
316
|
if (!authorized.success) {
|
|
207
317
|
this.fail(ws, String(authorized.error.code), authorized.error.message);
|
|
208
318
|
return;
|
|
@@ -216,14 +326,30 @@ var AudioConnection = class {
|
|
|
216
326
|
format: authorized.data.format,
|
|
217
327
|
sampleRate: authorized.data.sampleRate
|
|
218
328
|
};
|
|
219
|
-
this.started = true;
|
|
220
329
|
this.start = confirmed;
|
|
221
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;
|
|
335
|
+
}
|
|
222
336
|
if (confirmed.operation === "synthesize")
|
|
223
337
|
void this.runSynthesize(ws, confirmed);
|
|
224
338
|
else if (confirmed.operation === "transcribe" && confirmed.stream)
|
|
225
339
|
void this.runStreamTranscribe(ws, confirmed);
|
|
226
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
|
+
});
|
|
352
|
+
}
|
|
227
353
|
/** 流式识别:桥接持续音频输入,流式返回临时/最终结果与语音起止事件 */
|
|
228
354
|
async runStreamTranscribe(ws, start) {
|
|
229
355
|
try {
|
|
@@ -240,6 +366,8 @@ var AudioConnection = class {
|
|
|
240
366
|
signal: this.controller.signal
|
|
241
367
|
});
|
|
242
368
|
for await (const event of stream) {
|
|
369
|
+
if (this.closed)
|
|
370
|
+
return;
|
|
243
371
|
if (event.type === "transcript")
|
|
244
372
|
this.send(ws, { type: "transcript", text: event.text, final: event.final });
|
|
245
373
|
else if (event.type === "speech_started")
|
|
@@ -270,6 +398,8 @@ var AudioConnection = class {
|
|
|
270
398
|
model: start.model,
|
|
271
399
|
signal: this.controller.signal
|
|
272
400
|
});
|
|
401
|
+
if (this.closed)
|
|
402
|
+
return;
|
|
273
403
|
if (!result.success) {
|
|
274
404
|
this.fail(ws, String(result.error.code), result.error.message);
|
|
275
405
|
return;
|
|
@@ -280,7 +410,7 @@ var AudioConnection = class {
|
|
|
280
410
|
this.failFromError(ws, error);
|
|
281
411
|
}
|
|
282
412
|
}
|
|
283
|
-
/**
|
|
413
|
+
/** 语音合成:桥接文本输入,流式返回音频二进制帧(带发送缓冲背压) */
|
|
284
414
|
async runSynthesize(ws, start) {
|
|
285
415
|
try {
|
|
286
416
|
const stream = this.deps.ai.audio.synthesizeStream({
|
|
@@ -293,10 +423,17 @@ var AudioConnection = class {
|
|
|
293
423
|
signal: this.controller.signal
|
|
294
424
|
});
|
|
295
425
|
for await (const event of stream) {
|
|
296
|
-
if (
|
|
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
|
+
}
|
|
297
433
|
ws.send(toArrayBuffer(event.data));
|
|
298
|
-
else
|
|
434
|
+
} else {
|
|
299
435
|
this.send(ws, event);
|
|
436
|
+
}
|
|
300
437
|
}
|
|
301
438
|
this.end(ws);
|
|
302
439
|
} catch (error) {
|
|
@@ -304,17 +441,25 @@ var AudioConnection = class {
|
|
|
304
441
|
}
|
|
305
442
|
}
|
|
306
443
|
fail(ws, code, message) {
|
|
444
|
+
if (this.closed)
|
|
445
|
+
return;
|
|
446
|
+
this.state = "failed";
|
|
307
447
|
this.send(ws, { type: "error", code, message });
|
|
308
448
|
this.dispose();
|
|
309
449
|
ws.close();
|
|
310
450
|
}
|
|
311
451
|
failFromError(ws, error) {
|
|
452
|
+
if (this.closed)
|
|
453
|
+
return;
|
|
312
454
|
const hai = error;
|
|
313
|
-
const code = typeof hai?.code === "string" ? hai.code :
|
|
455
|
+
const code = typeof hai?.code === "string" ? hai.code : CODE_TICKET;
|
|
314
456
|
const message = error instanceof Error ? error.message : String(error);
|
|
315
457
|
this.fail(ws, code, message);
|
|
316
458
|
}
|
|
317
459
|
end(ws) {
|
|
460
|
+
if (this.closed)
|
|
461
|
+
return;
|
|
462
|
+
this.state = "completed";
|
|
318
463
|
this.send(ws, { type: "end" });
|
|
319
464
|
this.dispose();
|
|
320
465
|
ws.close();
|
|
@@ -327,6 +472,12 @@ var AudioConnection = class {
|
|
|
327
472
|
if (this.closed)
|
|
328
473
|
return;
|
|
329
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
|
+
}
|
|
330
481
|
this.audioQueue.end();
|
|
331
482
|
this.textQueue.end();
|
|
332
483
|
this.controller.abort();
|
|
@@ -337,6 +488,10 @@ var AudioConnection = class {
|
|
|
337
488
|
}
|
|
338
489
|
}
|
|
339
490
|
};
|
|
491
|
+
function sendBufferedAmount(ws) {
|
|
492
|
+
const raw = ws.raw;
|
|
493
|
+
return typeof raw?.bufferedAmount === "number" ? raw.bufferedAmount : 0;
|
|
494
|
+
}
|
|
340
495
|
function toBytes(data) {
|
|
341
496
|
if (typeof data === "string")
|
|
342
497
|
return void 0;
|
|
@@ -928,7 +1083,6 @@ function isEncryptedPayloadShape(value) {
|
|
|
928
1083
|
}
|
|
929
1084
|
|
|
930
1085
|
// src/serv-app.ts
|
|
931
|
-
var DEFAULT_AUDIO_WS_PATH = "/ai/audio";
|
|
932
1086
|
var API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
933
1087
|
var audioWsInjectors = /* @__PURE__ */ new WeakMap();
|
|
934
1088
|
function createApp(options) {
|
|
@@ -966,7 +1120,7 @@ function createApp(options) {
|
|
|
966
1120
|
mountRefreshCookieRoutes(app, http.apiPrefix, options.refreshCookie, options.iam);
|
|
967
1121
|
if (options.audio) {
|
|
968
1122
|
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
|
969
|
-
const audioPath = `${http.apiPrefix}${options.audio.path ??
|
|
1123
|
+
const audioPath = `${http.apiPrefix}${options.audio.path ?? AUDIO_WS_PATH}`;
|
|
970
1124
|
registerAudioWsRoute(app, audioPath, upgradeWebSocket, {
|
|
971
1125
|
ai: options.audio.ai,
|
|
972
1126
|
verifyTicket: options.audio.verifyTicket,
|
|
@@ -975,7 +1129,10 @@ function createApp(options) {
|
|
|
975
1129
|
maxMessageBytes: options.audio.maxMessageBytes,
|
|
976
1130
|
maxBufferedBytes: options.audio.maxBufferedBytes,
|
|
977
1131
|
maxTextBytes: options.audio.maxTextBytes,
|
|
978
|
-
maxSessionMs: options.audio.maxSessionMs
|
|
1132
|
+
maxSessionMs: options.audio.maxSessionMs,
|
|
1133
|
+
preAuthTimeoutMs: options.audio.preAuthTimeoutMs,
|
|
1134
|
+
maxPendingMessages: options.audio.maxPendingMessages,
|
|
1135
|
+
maxSendBufferBytes: options.audio.maxSendBufferBytes
|
|
979
1136
|
});
|
|
980
1137
|
audioWsInjectors.set(app, injectWebSocket);
|
|
981
1138
|
}
|