@hubtalk-ai/web-sdk 0.2.0
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 +509 -0
- package/dist/audio.d.ts +16 -0
- package/dist/bundle-entry.d.ts +1 -0
- package/dist/emitter.d.ts +10 -0
- package/dist/errors.d.ts +18 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +400 -0
- package/dist/logger.d.ts +6 -0
- package/dist/types.d.ts +86 -0
- package/dist/utils.d.ts +15 -0
- package/dist/voice-client.bundle.js +39 -0
- package/dist/voice-client.d.ts +84 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { ConnectionError as A, ConnectionErrorReason as u, MediaDeviceFailure as m, Room as I, RoomEvent as a, ConnectionState as p, Track as C, DisconnectReason as S } from "livekit-client";
|
|
2
|
+
class y {
|
|
3
|
+
constructor(t) {
|
|
4
|
+
if (t) {
|
|
5
|
+
this.element = t, this.owned = !1;
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
this.element = document.createElement("div"), this.element.style.display = "none", document.body.appendChild(this.element), this.owned = !0;
|
|
9
|
+
}
|
|
10
|
+
attach(t) {
|
|
11
|
+
this.element.appendChild(t.attach());
|
|
12
|
+
}
|
|
13
|
+
detach(t) {
|
|
14
|
+
t.detach().forEach((e) => e.remove());
|
|
15
|
+
}
|
|
16
|
+
clear() {
|
|
17
|
+
this.element.replaceChildren();
|
|
18
|
+
}
|
|
19
|
+
destroy() {
|
|
20
|
+
this.clear(), this.owned && this.element.remove();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class w {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
26
|
+
}
|
|
27
|
+
on(t, e) {
|
|
28
|
+
let n = this.listeners.get(t);
|
|
29
|
+
n || (n = /* @__PURE__ */ new Set(), this.listeners.set(t, n)), n.add(e);
|
|
30
|
+
}
|
|
31
|
+
off(t, e) {
|
|
32
|
+
var n;
|
|
33
|
+
(n = this.listeners.get(t)) == null || n.delete(e);
|
|
34
|
+
}
|
|
35
|
+
once(t, e) {
|
|
36
|
+
const n = (i) => {
|
|
37
|
+
this.off(t, n), e(i);
|
|
38
|
+
};
|
|
39
|
+
this.on(t, n);
|
|
40
|
+
}
|
|
41
|
+
emit(t, e) {
|
|
42
|
+
const n = this.listeners.get(t);
|
|
43
|
+
if (n)
|
|
44
|
+
for (const i of [...n])
|
|
45
|
+
i(e);
|
|
46
|
+
}
|
|
47
|
+
clear() {
|
|
48
|
+
this.listeners.clear();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const N = {
|
|
52
|
+
INVALID_PARAMS: "accessToken и url обязательны",
|
|
53
|
+
WEBRTC_UNSUPPORTED: "браузер/контекст не поддерживает WebRTC (нужен HTTPS)",
|
|
54
|
+
MICROPHONE_PERMISSION_DENIED: "доступ к микрофону запрещён пользователем",
|
|
55
|
+
MICROPHONE_UNAVAILABLE: "микрофон недоступен или занят другим приложением",
|
|
56
|
+
TOKEN_EXPIRED: "срок действия токена истёк",
|
|
57
|
+
TOKEN_INVALID: "сервер отклонил токен",
|
|
58
|
+
CONNECTION_FAILED: "не удалось установить соединение",
|
|
59
|
+
CONNECTION_LOST: "соединение потеряно, реконнект не восстановил его",
|
|
60
|
+
ALREADY_IN_CALL: "звонок уже активен",
|
|
61
|
+
NOT_IN_CALL: "нет активного звонка",
|
|
62
|
+
SERVER_ERROR: "внутренняя ошибка сервиса сигналинга"
|
|
63
|
+
};
|
|
64
|
+
class _ extends Error {
|
|
65
|
+
constructor(t, e, n) {
|
|
66
|
+
super(N[t]), this.name = "VoiceCallError", this.code = t, this.state = e, this.cause = n;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function b(o) {
|
|
70
|
+
if (o instanceof A)
|
|
71
|
+
switch (o.reason) {
|
|
72
|
+
case u.NotAllowed:
|
|
73
|
+
return "TOKEN_INVALID";
|
|
74
|
+
case u.ServerUnreachable:
|
|
75
|
+
case u.Timeout:
|
|
76
|
+
case u.WebSocket:
|
|
77
|
+
return "CONNECTION_FAILED";
|
|
78
|
+
default:
|
|
79
|
+
return "SERVER_ERROR";
|
|
80
|
+
}
|
|
81
|
+
return "SERVER_ERROR";
|
|
82
|
+
}
|
|
83
|
+
function O(o) {
|
|
84
|
+
return m.getFailure(o) === m.PermissionDenied ? "MICROPHONE_PERMISSION_DENIED" : "MICROPHONE_UNAVAILABLE";
|
|
85
|
+
}
|
|
86
|
+
const T = "[redacted]", k = /^[\w-]+\.[\w-]+\.[\w-]+$/, D = /token/i;
|
|
87
|
+
function f(o) {
|
|
88
|
+
if (typeof o == "string")
|
|
89
|
+
return k.test(o) ? T : o;
|
|
90
|
+
if (Array.isArray(o))
|
|
91
|
+
return o.map(f);
|
|
92
|
+
if (o && typeof o == "object") {
|
|
93
|
+
const t = {};
|
|
94
|
+
for (const [e, n] of Object.entries(o))
|
|
95
|
+
t[e] = D.test(e) ? T : f(n);
|
|
96
|
+
return t;
|
|
97
|
+
}
|
|
98
|
+
return o;
|
|
99
|
+
}
|
|
100
|
+
function L(o, t) {
|
|
101
|
+
if (o === "silent")
|
|
102
|
+
return { error: () => {
|
|
103
|
+
}, debug: () => {
|
|
104
|
+
} };
|
|
105
|
+
const e = t ?? {
|
|
106
|
+
error: (n, ...i) => console.error(n, ...i),
|
|
107
|
+
debug: (n, ...i) => console.debug(n, ...i)
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
error: (n, ...i) => e.error(n, ...i.map(f)),
|
|
111
|
+
debug: (n, ...i) => {
|
|
112
|
+
o === "debug" && e.debug(n, ...i.map(f));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function P(o = !0) {
|
|
117
|
+
var t;
|
|
118
|
+
return typeof window > "u" || !window.isSecureContext || typeof RTCPeerConnection > "u" ? !1 : o ? typeof navigator < "u" && !!((t = navigator.mediaDevices) != null && t.getUserMedia) : !0;
|
|
119
|
+
}
|
|
120
|
+
function M(o) {
|
|
121
|
+
const t = o.split(".")[1];
|
|
122
|
+
if (!t) return null;
|
|
123
|
+
try {
|
|
124
|
+
const e = t.replace(/-/g, "+").replace(/_/g, "/"), n = decodeURIComponent(
|
|
125
|
+
Array.from(atob(e)).map((r) => "%" + r.charCodeAt(0).toString(16).padStart(2, "0")).join("")
|
|
126
|
+
), i = JSON.parse(n);
|
|
127
|
+
return typeof i.exp == "number" ? i.exp : null;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const v = "lk.agent.state", V = /* @__PURE__ */ new Set(["initializing", "listening", "thinking", "speaking"]), U = /* @__PURE__ */ new Set([
|
|
133
|
+
S.ROOM_DELETED,
|
|
134
|
+
S.PARTICIPANT_REMOVED,
|
|
135
|
+
S.SERVER_SHUTDOWN,
|
|
136
|
+
S.ROOM_CLOSED
|
|
137
|
+
]), E = /* @__PURE__ */ new Set(["connecting", "connected", "reconnecting"]), R = "lk.transcription", H = "lk.chat", F = "lk.segment_id", x = "lk.transcription_final", K = 1e3;
|
|
138
|
+
class B {
|
|
139
|
+
constructor(t = {}) {
|
|
140
|
+
this.emitter = new w(), this.connectionState = "idle", this.agentState = "unknown", this.room = null, this.callId = null, this.playAudio = !0, this.stoppingLocally = !1, this.reconnectSeen = !1, this.destroyed = !1, this.roomCleanup = [], this.audio = new y(t.audioContainer), this.log = L(t.logLevel ?? "error", t.logger), this.connectTimeoutMs = t.connectTimeoutMs;
|
|
141
|
+
}
|
|
142
|
+
getState() {
|
|
143
|
+
return this.connectionState;
|
|
144
|
+
}
|
|
145
|
+
getAgentState() {
|
|
146
|
+
return this.agentState;
|
|
147
|
+
}
|
|
148
|
+
getCallId() {
|
|
149
|
+
return this.callId;
|
|
150
|
+
}
|
|
151
|
+
on(t, e) {
|
|
152
|
+
this.emitter.on(t, e);
|
|
153
|
+
}
|
|
154
|
+
off(t, e) {
|
|
155
|
+
this.emitter.off(t, e);
|
|
156
|
+
}
|
|
157
|
+
once(t, e) {
|
|
158
|
+
this.emitter.once(t, e);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* `options.audio === false` — chat-only call: микрофон не публикуется,
|
|
162
|
+
* аудио агента не подписывается (см. connectRoom). Текст в обе стороны
|
|
163
|
+
* идёт как обычно — `sendText` и событие `transcript`.
|
|
164
|
+
*
|
|
165
|
+
* Аудио агента при этом ПОДПИСЫВАЕТСЯ и просто не проигрывается — это не
|
|
166
|
+
* расточительность, а единственный работающий вариант с livekit-agents
|
|
167
|
+
* 1.6.4: `_ParticipantAudioOutput.capture_frame` ждёт `_subscribed_fut`
|
|
168
|
+
* (room_io/_output.py:92), который закрывается только по
|
|
169
|
+
* `publication.wait_for_subscription()`. Без подписчика первый же кадр
|
|
170
|
+
* TTS блокируется навсегда, а вместе с ним умирает и текст: транскрипт
|
|
171
|
+
* агента идёт через TranscriptSynchronizer, привязанный к тому же
|
|
172
|
+
* аудио-выходу (room_io.py:163-166). Наблюдалось как «агент мигает
|
|
173
|
+
* thinking -> listening и молчит», при том что эхо реплик посетителя
|
|
174
|
+
* приходило — у него своя, несинхронизированная ветка вывода.
|
|
175
|
+
*
|
|
176
|
+
* ponytail: платит трафиком и TTS за то, что никто не слышит. Убирается
|
|
177
|
+
* только на стороне платформы — RoomOutputOptions(audio_enabled=False) по
|
|
178
|
+
* флагу звонка, тогда текст пойдёт сразу и синтеза не будет вовсе.
|
|
179
|
+
*/
|
|
180
|
+
async startCall(t, e = {}) {
|
|
181
|
+
if (E.has(this.connectionState))
|
|
182
|
+
throw this.raiseError("ALREADY_IN_CALL", this.connectionState);
|
|
183
|
+
const n = e.audio !== !1;
|
|
184
|
+
this.playAudio = n;
|
|
185
|
+
const { accessToken: i, url: r } = this.validateStartParams(t, n);
|
|
186
|
+
this.stoppingLocally = !1, this.reconnectSeen = !1, this.setConnectionState("connecting");
|
|
187
|
+
const c = new I();
|
|
188
|
+
if (this.room = c, this.wireRoomEvents(c), await this.connectRoom(c, r, i), this.stoppingLocally) {
|
|
189
|
+
await this.abandonConnectingCall(c);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
n && await this.publishMicrophone(c), this.callId = c.name, this.setConnectionState("connected"), this.syncAgentState(c), this.emitter.emit("callStarted", { callId: this.callId });
|
|
193
|
+
}
|
|
194
|
+
validateStartParams(t, e) {
|
|
195
|
+
const n = t.accessToken ?? t.access_token, i = t.url;
|
|
196
|
+
if (!n || !i)
|
|
197
|
+
throw this.raiseError("INVALID_PARAMS", "error");
|
|
198
|
+
if (!P(e))
|
|
199
|
+
throw this.raiseError("WEBRTC_UNSUPPORTED", "error");
|
|
200
|
+
const r = M(n);
|
|
201
|
+
if (r !== null && r * 1e3 <= Date.now())
|
|
202
|
+
throw this.raiseError("TOKEN_EXPIRED", "error");
|
|
203
|
+
return { accessToken: n, url: i };
|
|
204
|
+
}
|
|
205
|
+
async connectRoom(t, e, n) {
|
|
206
|
+
try {
|
|
207
|
+
await t.connect(
|
|
208
|
+
e,
|
|
209
|
+
n,
|
|
210
|
+
this.connectTimeoutMs === void 0 ? void 0 : {
|
|
211
|
+
peerConnectionTimeout: this.connectTimeoutMs,
|
|
212
|
+
websocketTimeout: this.connectTimeoutMs
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
} catch (i) {
|
|
216
|
+
throw this.teardownRoom(), this.raiseError(b(i), "error", i);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// stopCall() пришёл во время connecting (AC4/§1.2) — сворачиваем попытку
|
|
220
|
+
// без callStarted/error вместо продолжения к публикации микрофона
|
|
221
|
+
async abandonConnectingCall(t) {
|
|
222
|
+
await t.disconnect().catch(() => {
|
|
223
|
+
}), this.teardownRoom(), this.setConnectionState("disconnected");
|
|
224
|
+
}
|
|
225
|
+
async publishMicrophone(t) {
|
|
226
|
+
try {
|
|
227
|
+
await t.localParticipant.setMicrophoneEnabled(!0), await t.startAudio();
|
|
228
|
+
} catch (e) {
|
|
229
|
+
const n = O(e);
|
|
230
|
+
throw await t.disconnect().catch(() => {
|
|
231
|
+
}), this.teardownRoom(), this.raiseError(n, "error", e);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Отправляет текстовую реплику посетителя в разговор (VPT-108 §2.1).
|
|
236
|
+
* Своё сообщение в чат SDK не кладёт — его вернёт агент событием
|
|
237
|
+
* `transcript` с role: 'user' (§2.4): один источник правды и один порядок.
|
|
238
|
+
*/
|
|
239
|
+
async sendText(t) {
|
|
240
|
+
const e = this.room;
|
|
241
|
+
if (!e || !E.has(this.connectionState))
|
|
242
|
+
throw this.raiseError("NOT_IN_CALL", this.connectionState);
|
|
243
|
+
const n = t.trim().slice(0, K);
|
|
244
|
+
if (n)
|
|
245
|
+
try {
|
|
246
|
+
await e.localParticipant.sendText(n, { topic: H });
|
|
247
|
+
} catch (i) {
|
|
248
|
+
throw this.raiseError("SERVER_ERROR", this.connectionState, i);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async stopCall() {
|
|
252
|
+
this.stoppingLocally = !0;
|
|
253
|
+
const t = this.room;
|
|
254
|
+
!t || !E.has(this.connectionState) || this.connectionState !== "connecting" && await t.disconnect().catch((e) => this.log.debug("[voice-client] disconnect", e));
|
|
255
|
+
}
|
|
256
|
+
destroy() {
|
|
257
|
+
this.stopCall(), this.playAudio = !0, this.destroyed = !0, this.teardownRoom(), this.emitter.clear(), this.audio.destroy(), this.connectionState = "idle", this.agentState = "unknown", this.callId = null;
|
|
258
|
+
}
|
|
259
|
+
wireRoomEvents(t) {
|
|
260
|
+
const e = (s, d, h) => {
|
|
261
|
+
this.log.debug("[voice-client] remote track subscribed", {
|
|
262
|
+
participant: h.identity,
|
|
263
|
+
participantID: h.sid,
|
|
264
|
+
trackID: s.sid,
|
|
265
|
+
streamTrackID: s.mediaStreamTrack.id,
|
|
266
|
+
kind: s.kind,
|
|
267
|
+
source: d.source,
|
|
268
|
+
muted: d.isMuted
|
|
269
|
+
}), s.kind === C.Kind.Audio && this.playAudio && this.audio.attach(s);
|
|
270
|
+
}, n = (s, d, h) => {
|
|
271
|
+
this.log.debug("[voice-client] remote track unsubscribed", {
|
|
272
|
+
participant: h.identity,
|
|
273
|
+
participantID: h.sid,
|
|
274
|
+
trackID: s.sid,
|
|
275
|
+
kind: s.kind,
|
|
276
|
+
source: d.source
|
|
277
|
+
}), s.kind === C.Kind.Audio && this.audio.detach(s);
|
|
278
|
+
}, i = (s, d, h) => {
|
|
279
|
+
this.log.debug("[voice-client] remote track subscription failed", {
|
|
280
|
+
participant: d.identity,
|
|
281
|
+
participantID: d.sid,
|
|
282
|
+
trackID: s,
|
|
283
|
+
reason: h
|
|
284
|
+
});
|
|
285
|
+
}, r = (s) => {
|
|
286
|
+
this.log.debug("[voice-client] remote participant connected", {
|
|
287
|
+
room: t.name,
|
|
288
|
+
participant: s.identity,
|
|
289
|
+
participantID: s.sid
|
|
290
|
+
}), !this.destroyed && this.syncAgentState(t);
|
|
291
|
+
}, c = () => {
|
|
292
|
+
this.destroyed || this.syncAgentState(t);
|
|
293
|
+
}, l = (s) => {
|
|
294
|
+
this.destroyed || this.applyLkConnectionState(s);
|
|
295
|
+
}, g = (s) => {
|
|
296
|
+
this.log.debug("[voice-client] room disconnected", {
|
|
297
|
+
room: t.name,
|
|
298
|
+
reason: s,
|
|
299
|
+
stoppingLocally: this.stoppingLocally
|
|
300
|
+
}), !this.destroyed && this.handleDisconnected(s);
|
|
301
|
+
};
|
|
302
|
+
t.registerTextStreamHandler(R, (s, d) => {
|
|
303
|
+
if (this.destroyed) return;
|
|
304
|
+
const h = d.identity === t.localParticipant.identity ? "user" : "agent";
|
|
305
|
+
this.readTranscriptSegment(s, h);
|
|
306
|
+
}), t.on(a.TrackSubscribed, e), t.on(a.TrackUnsubscribed, n), t.on(a.TrackSubscriptionFailed, i), t.on(a.ParticipantAttributesChanged, c), t.on(a.ParticipantConnected, r), t.on(a.ParticipantDisconnected, c), t.on(a.ConnectionStateChanged, l), t.on(a.Disconnected, g), this.roomCleanup = [
|
|
307
|
+
() => t.unregisterTextStreamHandler(R),
|
|
308
|
+
() => t.off(a.TrackSubscribed, e),
|
|
309
|
+
() => t.off(a.TrackUnsubscribed, n),
|
|
310
|
+
() => t.off(a.TrackSubscriptionFailed, i),
|
|
311
|
+
() => t.off(a.ParticipantAttributesChanged, c),
|
|
312
|
+
() => t.off(a.ParticipantConnected, r),
|
|
313
|
+
() => t.off(a.ParticipantDisconnected, c),
|
|
314
|
+
() => t.off(a.ConnectionStateChanged, l),
|
|
315
|
+
() => t.off(a.Disconnected, g)
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Разбор одного сегмента транскрипта (VPT-108 §1.3). Роль определена
|
|
320
|
+
* вызывающим — сравнением identity потока с локальным участником.
|
|
321
|
+
*
|
|
322
|
+
* Одна реплика приходит несколькими потоками с общим `lk.segment_id`
|
|
323
|
+
* (тот же дефект-дубль, что закрывался в №50 §9), поэтому пузырь
|
|
324
|
+
* сшивается по нему, а не по потоку. Чанки копятся ВНУТРИ потока, а текст
|
|
325
|
+
* потока ЗАМЕНЯЕТ текст сегмента: у реплик посетителя каждый поток несёт
|
|
326
|
+
* полный текст на момент отправки, а не хвост (`_ParticipantTranscription
|
|
327
|
+
* Output(is_delta_stream=False)`), и дописывание удвоило бы реплику.
|
|
328
|
+
*
|
|
329
|
+
* `final` — из атрибута `lk.transcription_final`, а не из факта закрытия
|
|
330
|
+
* потока: дельта-поток закрывается штатно, будучи нефинальным. У потока
|
|
331
|
+
* агента `true` приходит в трейлере, и livekit-client домешивает атрибуты
|
|
332
|
+
* трейлера в `reader.info` перед закрытием — поэтому читаем атрибут ПОСЛЕ
|
|
333
|
+
* цикла чтения.
|
|
334
|
+
*/
|
|
335
|
+
readTranscriptSegment(t, e) {
|
|
336
|
+
var i;
|
|
337
|
+
const n = ((i = t.info.attributes) == null ? void 0 : i[F]) || t.info.id;
|
|
338
|
+
(async () => {
|
|
339
|
+
var c;
|
|
340
|
+
let r = "";
|
|
341
|
+
try {
|
|
342
|
+
for await (const l of t)
|
|
343
|
+
r += l, this.emitter.emit("transcript", { role: e, text: r, final: !1, id: n });
|
|
344
|
+
} catch {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
((c = t.info.attributes) == null ? void 0 : c[x]) === "true" && this.emitter.emit("transcript", { role: e, text: r, final: !0, id: n });
|
|
348
|
+
})();
|
|
349
|
+
}
|
|
350
|
+
applyLkConnectionState(t) {
|
|
351
|
+
t === p.Reconnecting || t === p.SignalReconnecting ? (this.reconnectSeen = !0, this.setConnectionState("reconnecting")) : t === p.Connected && this.connectionState === "reconnecting" && this.setConnectionState("connected");
|
|
352
|
+
}
|
|
353
|
+
handleDisconnected(t) {
|
|
354
|
+
const e = this.callId, n = this.reconnectSeen, i = this.stoppingLocally;
|
|
355
|
+
if (this.teardownRoom(), i) {
|
|
356
|
+
this.setConnectionState("disconnected"), this.emitter.emit("callEnded", { callId: e, reason: "local" });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (n) {
|
|
360
|
+
this.setConnectionState("disconnected"), this.raiseError("CONNECTION_LOST", "disconnected"), this.emitter.emit("callEnded", { callId: e, reason: "connection_lost" });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const c = t !== void 0 && U.has(t) ? "remote" : "error";
|
|
364
|
+
this.setConnectionState("disconnected"), this.emitter.emit("callEnded", { callId: e, reason: c });
|
|
365
|
+
}
|
|
366
|
+
teardownRoom() {
|
|
367
|
+
this.roomCleanup.forEach((t) => t()), this.roomCleanup = [], this.audio.clear(), this.room = null, this.agentState = "unknown";
|
|
368
|
+
}
|
|
369
|
+
syncAgentState(t) {
|
|
370
|
+
var n;
|
|
371
|
+
let e = "unknown";
|
|
372
|
+
for (const i of t.remoteParticipants.values()) {
|
|
373
|
+
const r = (n = i.attributes) == null ? void 0 : n[v];
|
|
374
|
+
if (r && V.has(r)) {
|
|
375
|
+
e = r;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
this.setAgentState(e);
|
|
380
|
+
}
|
|
381
|
+
setConnectionState(t) {
|
|
382
|
+
const e = this.connectionState;
|
|
383
|
+
e !== t && (this.connectionState = t, this.emitter.emit("connectionStateChanged", { state: t, previous: e }));
|
|
384
|
+
}
|
|
385
|
+
setAgentState(t) {
|
|
386
|
+
const e = this.agentState;
|
|
387
|
+
e !== t && (this.agentState = t, this.emitter.emit("agentStateChanged", { state: t, previous: e }), t === "speaking" ? this.emitter.emit("agentSpeakingStarted", {}) : e === "speaking" && this.emitter.emit("agentSpeakingStopped", {}));
|
|
388
|
+
}
|
|
389
|
+
/** Переводит ось соединения (если нужно), логирует и эмитит error (§1.4);
|
|
390
|
+
* возвращает VoiceCallError — вызывающий сам решает throw или нет. */
|
|
391
|
+
raiseError(t, e, n) {
|
|
392
|
+
e !== this.connectionState && this.setConnectionState(e);
|
|
393
|
+
const i = new _(t, this.connectionState, n);
|
|
394
|
+
return this.log.error(`[voice-client] ${t}`, n), this.emitter.emit("error", i), i;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
export {
|
|
398
|
+
_ as VoiceCallError,
|
|
399
|
+
B as VoiceClient
|
|
400
|
+
};
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Logger, LogLevel } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* §8 ТЗ №132: accessToken и любые поля с «token» в имени маскируются в
|
|
4
|
+
* логах безусловно — даже если интегратор передал свой logger.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createLogger(level: LogLevel, custom?: Logger): Logger;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Ось соединения (D4 ТЗ №132 §1.1) — жизненный цикл звонка. */
|
|
2
|
+
export type ConnectionState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'error';
|
|
3
|
+
/** Ось активности агента (D4 §1.1) — из атрибута участника lk.agent.state. */
|
|
4
|
+
export type AgentState = 'unknown' | 'initializing' | 'listening' | 'thinking' | 'speaking';
|
|
5
|
+
export type VoiceErrorCode = 'INVALID_PARAMS' | 'WEBRTC_UNSUPPORTED' | 'MICROPHONE_PERMISSION_DENIED' | 'MICROPHONE_UNAVAILABLE' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'CONNECTION_FAILED' | 'CONNECTION_LOST' | 'ALREADY_IN_CALL' | 'NOT_IN_CALL' | 'SERVER_ERROR';
|
|
6
|
+
export interface VoiceError {
|
|
7
|
+
code: VoiceErrorCode;
|
|
8
|
+
message: string;
|
|
9
|
+
state: ConnectionState;
|
|
10
|
+
/** Исходная ошибка браузера/livekit-client — для логов интегратора (§1.4). */
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export type CallEndedReason = 'local' | 'remote' | 'connection_lost' | 'error';
|
|
14
|
+
/**
|
|
15
|
+
* startCall принимает и camelCase, и сырой ответ POST /v1/calls/web
|
|
16
|
+
* (access_token/url + лишние поля объекта call) — §1.2 ТЗ №132.
|
|
17
|
+
*/
|
|
18
|
+
export interface StartCallParams {
|
|
19
|
+
accessToken?: string;
|
|
20
|
+
access_token?: string;
|
|
21
|
+
url: string;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Per-call options — the mode belongs to the call, not to the client: the
|
|
26
|
+
* same page offers a voice call and a chat-only one side by side.
|
|
27
|
+
*/
|
|
28
|
+
export interface StartCallOptions {
|
|
29
|
+
/**
|
|
30
|
+
* false — chat-only call: no microphone is published and the agent's audio
|
|
31
|
+
* track is not subscribed. Text keeps flowing both ways (`sendText` /
|
|
32
|
+
* `transcript`), and neither MICROPHONE_* code can occur.
|
|
33
|
+
*/
|
|
34
|
+
audio?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export type LogLevel = 'silent' | 'error' | 'debug';
|
|
37
|
+
export interface Logger {
|
|
38
|
+
error(message: string, ...args: unknown[]): void;
|
|
39
|
+
debug(message: string, ...args: unknown[]): void;
|
|
40
|
+
}
|
|
41
|
+
export interface VoiceClientOptions {
|
|
42
|
+
/** Куда вешать <audio>; по умолчанию — скрытый контейнер, создаваемый SDK. */
|
|
43
|
+
audioContainer?: HTMLElement;
|
|
44
|
+
logger?: Logger;
|
|
45
|
+
/** По умолчанию 'error' (§8: тихий лог, подробный — только явной опцией). */
|
|
46
|
+
logLevel?: LogLevel;
|
|
47
|
+
connectTimeoutMs?: number;
|
|
48
|
+
}
|
|
49
|
+
export interface CallStartedEvent {
|
|
50
|
+
callId: string;
|
|
51
|
+
}
|
|
52
|
+
export interface CallEndedEvent {
|
|
53
|
+
callId: string | null;
|
|
54
|
+
reason: CallEndedReason;
|
|
55
|
+
}
|
|
56
|
+
export interface ConnectionStateChangedEvent {
|
|
57
|
+
state: ConnectionState;
|
|
58
|
+
previous: ConnectionState;
|
|
59
|
+
}
|
|
60
|
+
export interface AgentStateChangedEvent {
|
|
61
|
+
state: AgentState;
|
|
62
|
+
previous: AgentState;
|
|
63
|
+
}
|
|
64
|
+
/** Реплика разговора из топика lk.transcription (VPT-108 §1.1). */
|
|
65
|
+
export interface TranscriptEvent {
|
|
66
|
+
/** 'user' — реплика посетителя, 'agent' — реплика агента (D1). */
|
|
67
|
+
role: 'user' | 'agent';
|
|
68
|
+
/** Накопленный текст реплики на момент события. */
|
|
69
|
+
text: string;
|
|
70
|
+
/** false — реплика ещё растёт, true — сегмент закрыт. */
|
|
71
|
+
final: boolean;
|
|
72
|
+
/** Идентификатор реплики: сшивает дельты одного сегмента в один пузырь. */
|
|
73
|
+
id: string;
|
|
74
|
+
}
|
|
75
|
+
export interface VoiceClientEventMap {
|
|
76
|
+
callStarted: CallStartedEvent;
|
|
77
|
+
callEnded: CallEndedEvent;
|
|
78
|
+
connectionStateChanged: ConnectionStateChangedEvent;
|
|
79
|
+
agentStateChanged: AgentStateChangedEvent;
|
|
80
|
+
agentSpeakingStarted: Record<string, never>;
|
|
81
|
+
agentSpeakingStopped: Record<string, never>;
|
|
82
|
+
transcript: TranscriptEvent;
|
|
83
|
+
error: VoiceError;
|
|
84
|
+
}
|
|
85
|
+
export type VoiceClientEventName = keyof VoiceClientEventMap;
|
|
86
|
+
export type VoiceClientEventHandler<E extends VoiceClientEventName> = (payload: VoiceClientEventMap[E]) => void;
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebRTC/secure-context детект (§1.4 ТЗ №132: WEBRTC_UNSUPPORTED).
|
|
3
|
+
*
|
|
4
|
+
* Secure context и RTCPeerConnection нужны любому звонку — текст LiveKit
|
|
5
|
+
* тоже везёт по WebRTC-датаканалу. getUserMedia спрашиваем только у звонка,
|
|
6
|
+
* который публикует микрофон: chat-only ({ audio: false }) работает и там,
|
|
7
|
+
* где микрофона нет вовсе.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isWebRtcSupported(requireMicrophone?: boolean): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* exp из payload JWT без проверки подписи — только для локального
|
|
12
|
+
* пред-чека TOKEN_EXPIRED до connect (§3.2/§1.4); сервер токен всё равно
|
|
13
|
+
* валидирует сам. atob — нативный, без зависимости на jwt-decode.
|
|
14
|
+
*/
|
|
15
|
+
export declare function decodeJwtExpiry(token: string): number | null;
|