@softomnitel/omnicall-kit 0.1.0-rc.0 → 0.1.1

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.
Files changed (2) hide show
  1. package/README.md +879 -23
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,34 +1,890 @@
1
1
  # @softomnitel/omnicall-kit
2
2
 
3
- Browser SDK client for OmniCall Desktop.
3
+ Browser SDK client for OmniCall Desktop (local protocol).
4
4
 
5
- **Status:** product namespaces through SDK-08 + official browser WebSocket transport defaults.
6
- Call/operator/account mutations and lifecycle are on `OmniCallClient`.
5
+ ```bash
6
+ npm install @softomnitel/omnicall-kit
7
+ ```
7
8
 
8
- Depends on `@softomnitel/omnicall-protocol` only. Must never import OmniCall Desktop Domain,
9
- Application, Electron, JsSIP, React, or Zustand.
9
+ ---
10
10
 
11
- Public surface (highlights):
11
+ # OmniCall Kit
12
12
 
13
- - `createAuthClient`pairing / PoP / capabilities
14
- - `createOmniCallClient` product API on top of auth
15
- - `createBrowserWebSocketTransport` — official `TransportPort` over browser `WebSocket`
16
- - `createBrowserScheduler` / `createBrowserJitterSource` — production timer/jitter defaults
17
- - `createIndexedDbPopKeyStore` / `createMemoryPopKeyStore` — PoP persistence
18
- - Type helpers: `OmniCallEventOf`, snapshot/capability re-exports, typed error readers
19
- (`readInteractionRequiredDetails`, …) — see `docs/guide/typescript.md`
13
+ `@softomnitel/omnicall-kit` — типизированный JavaScript/TypeScript-клиент для
14
+ CRM. Он подключает страницу CRM к уже установленному **OmniCall Desktop** на
15
+ компьютере оператора.
20
16
 
21
- Constructor options `transportFactory`, `scheduler`, and `jitter` are **optional** in
22
- browsers (defaults above). Unit tests should still inject FakeTransport + fake scheduler.
17
+ SDK получает состояние звонков и оператора, а также отправляет разрешённые
18
+ команды: начать звонок, ответить, положить трубку или изменить статус. SIP,
19
+ пароли и внутренняя телефония остаются внутри Desktop и не попадают в браузер.
23
20
 
24
- Internal production modules (not all exported as helpers):
21
+ ```ts
22
+ import {
23
+ createIndexedDbPopKeyStore,
24
+ createOmniCallClient
25
+ } from '@softomnitel/omnicall-kit';
25
26
 
26
- - explicit connection state machine
27
- - request correlation, timeouts, abort/disconnect cleanup
28
- - heartbeat and bounded jittered reconnect
29
- - snapshot cache, sequence-gap resync, redaction-safe diagnostics
27
+ const client = createOmniCallClient({ /* параметры ниже */ });
28
+ await client.connect();
29
+ await client.waitUntil((state) => state === 'ready');
30
+ const snapshot = await client.getSnapshot();
31
+ console.log(snapshot.revision);
32
+ ```
30
33
 
31
- `FakeTransport` / test helpers live under `src/internal/` for unit tests only and are
32
- excluded from the published `dist/` tarball.
34
+ ## Содержание
33
35
 
34
- See `docs/guide/transport.md` for the transport contract.
36
+ - [Что нужно для работы](#что-нужно-для-работы)
37
+ - [Установка](#установка)
38
+ - [Быстрый старт](#быстрый-старт)
39
+ - [Основные понятия](#основные-понятия)
40
+ - [Состояния и события](#состояния-и-события)
41
+ - [API Reference](#api-reference)
42
+ - [Рецепты](#рецепты)
43
+ - [Ошибки и FAQ](#ошибки-и-faq)
44
+ - [Миграция и совместимость](#миграция-и-совместимость)
45
+
46
+ ## Что нужно для работы
47
+
48
+ 1. OmniCall Desktop должен быть установлен, запущен и иметь включённый SDK
49
+ gateway.
50
+ 2. Страница CRM должна работать в Chromium или Edge на Chromium.
51
+ 3. Для сборки проекта нужен Node.js `>=20.19.0` и npm `>=10`.
52
+ 4. В браузере нужны Web Crypto и IndexedDB. Они хранят криптографическую
53
+ идентичность браузера.
54
+ 5. Если CRM работает по HTTPS, браузер может спросить разрешение на связь с
55
+ локальной программой. Объясните это действие оператору в интерфейсе.
56
+
57
+ Firefox и Safari пока не входят в заявленную матрицу поддержки.
58
+
59
+ ## Установка
60
+
61
+ ```bash
62
+ npm install @softomnitel/omnicall-kit
63
+ ```
64
+
65
+ Для воспроизводимой production-сборки зафиксируйте версию, доступную в вашем
66
+ закрытом npm registry:
67
+
68
+ ```bash
69
+ npm install @softomnitel/omnicall-kit@0.1.1
70
+ ```
71
+
72
+ Пакет ESM-only. Импортируйте его через `import`, а не `require`.
73
+
74
+ ## Быстрый старт
75
+
76
+ ### 1. Создайте хранилище идентичности браузера
77
+
78
+ При первом подключении оператор подтверждает, что CRM может работать с Desktop.
79
+ Этот процесс называется **pairing**. После подтверждения SDK сохраняет
80
+ криптографическую идентичность браузера в IndexedDB. Не храните её в
81
+ `localStorage` или `sessionStorage`.
82
+
83
+ ```ts
84
+ import { createIndexedDbPopKeyStore } from '@softomnitel/omnicall-kit';
85
+
86
+ const keyStore = createIndexedDbPopKeyStore({
87
+ // Стабильная строка для одной установки CRM в одном браузере.
88
+ installId: 'crm-production'
89
+ });
90
+ ```
91
+
92
+ ### 2. Создайте клиент
93
+
94
+ `origin` — точный адрес CRM из браузера: схема, домен и порт. Например,
95
+ `https://crm.example`. Desktop не принимает маски и части строк.
96
+
97
+ ```ts
98
+ import { createOmniCallClient } from '@softomnitel/omnicall-kit';
99
+
100
+ const client = createOmniCallClient({
101
+ url: 'ws://127.0.0.1:17341/omnicall/v1/ws',
102
+ origin: window.location.origin,
103
+ application: { name: 'my-crm', version: '1.0.0' },
104
+ sdkVersion: '0.1.1',
105
+ requestedProfile: 'call_controller',
106
+ requestedCapabilities: [
107
+ 'session.read.redacted',
108
+ 'window.show',
109
+ 'call.originate',
110
+ 'call.control',
111
+ 'operator.status.write',
112
+ 'session.logout'
113
+ ],
114
+ keyStore
115
+ });
116
+ ```
117
+
118
+ `requestedCapabilities` — это просьба о правах, а не их выдача. Desktop выдаёт
119
+ только права, разрешённые для этого Origin. Права `account.activate` и
120
+ `window.hide` нельзя запросить при pairing: оператор выдаёт их отдельно в
121
+ настройках Desktop.
122
+
123
+ ### 3. Покажите оператору, что pairing ожидает подтверждения
124
+
125
+ ```ts
126
+ client.onPairingRequired((info) => {
127
+ console.info(
128
+ `Подтвердите подключение ${info.origin} в окне OmniCall Desktop.`
129
+ );
130
+ });
131
+
132
+ client.onStateChange((state) => {
133
+ console.info(`Состояние SDK: ${state}`);
134
+ });
135
+ ```
136
+
137
+ Подписки возвращают функцию отписки. Вызовите её при размонтировании компонента
138
+ или закрытии вкладки.
139
+
140
+ ### 4. Подключитесь и получите состояние
141
+
142
+ `connect()` начинает сетевое подключение и возвращает `Promise`. `Promise`
143
+ означает результат, который появится позже. `await` ждёт этот результат, не
144
+ блокируя браузер.
145
+
146
+ ```ts
147
+ await client.connect();
148
+ await client.waitUntil((state) => state === 'ready', 60_000);
149
+
150
+ const snapshot = await client.getSnapshot();
151
+ console.log('Версия состояния:', snapshot.revision);
152
+ console.log('Звонки:', snapshot.sections.calls);
153
+ ```
154
+
155
+ Если состояние стало `pairing_required`, оператор должен подтвердить CRM в
156
+ OmniCall Desktop. Не создавайте второй клиент и не повторяйте `connect()` в
157
+ цикле.
158
+
159
+ ## Основные понятия
160
+
161
+ | Понятие | Что это означает для CRM |
162
+ | --- | --- |
163
+ | OmniCall Desktop | Программа на компьютере оператора. Она владеет телефонией и учётной записью. |
164
+ | pairing | Первое подтверждение доступа конкретного сайта к Desktop. |
165
+ | capability | Право на одну группу действий, например `call.originate`. Проверяйте выданные права перед показом кнопки. |
166
+ | snapshot | Полный согласованный снимок состояния Desktop. Это главный источник данных для интерфейса CRM. |
167
+ | event | Уведомление об изменении, например `call:incoming`. Событие помогает быстро обновить UI, но не заменяет snapshot. |
168
+ | revision | Номер версии snapshot. Перед командой, меняющей состояние, передайте актуальный номер в `expectedRevision`. |
169
+
170
+ Поток данных выглядит так:
171
+
172
+ ```text
173
+ CRM в браузере
174
+ → OmniCall Kit
175
+ → локальный WebSocket на 127.0.0.1
176
+ → OmniCall Desktop
177
+ → телефония и операторская платформа
178
+ ```
179
+
180
+ SDK не является вторым телефоном. Он не даёт CRM SIP-пароль, не открывает
181
+ внутренний протокол операторской платформы и не поддерживает legacy
182
+ `window.Softphone`.
183
+
184
+ ### Snapshot, события и revision
185
+
186
+ После `ready` вызовите `getSnapshot()`. Далее подпишитесь на события. После
187
+ reconnect снова получите snapshot.
188
+
189
+ Команда изменения состояния принимает `expectedRevision`. Если Desktop уже
190
+ изменился в другой вкладке, SDK отклонит команду с кодом `stale_state`. Получите
191
+ новый snapshot и дайте пользователю повторить намеренное действие.
192
+
193
+ ```ts
194
+ async function getRevision(): Promise<number> {
195
+ return client.getRevision() ?? (await client.getSnapshot()).revision;
196
+ }
197
+
198
+ const result = await client.calls.originate({
199
+ destination: '+74951234567',
200
+ expectedRevision: await getRevision()
201
+ });
202
+
203
+ console.log(result.callId, result.revision);
204
+ ```
205
+
206
+ Не повторяйте автоматически `originate`, `hangup`, `logout` или
207
+ `activateProfile` после reconnect. Эти действия могут сработать дважды.
208
+
209
+ ## Состояния и события
210
+
211
+ ### Состояния подключения
212
+
213
+ | Состояние | Что показать пользователю |
214
+ | --- | --- |
215
+ | `idle` | Кнопку подключения. |
216
+ | `connecting`, `handshaking`, `authenticating` | Индикатор процесса. |
217
+ | `pairing_required` | Инструкцию подтвердить CRM в Desktop. |
218
+ | `ready` | Основной интерфейс CRM. |
219
+ | `reconnecting` | Неблокирующее сообщение о восстановлении связи. |
220
+ | `revoked` | Очистите UI сессии и предложите пройти pairing снова. |
221
+ | `incompatible` | Предложите обновить CRM SDK или Desktop. |
222
+ | `failed` | Покажите безопасное описание ошибки из `getConnectError()`. |
223
+ | `closed` | Разрешите пользователю подключиться снова. |
224
+
225
+ ### События
226
+
227
+ Подписывайтесь только на имена из `PUBLIC_EVENT_TYPES`.
228
+
229
+ | Событие | Назначение |
230
+ | --- | --- |
231
+ | `call:incoming` | Показать входящий звонок и кнопки ответа или отклонения. |
232
+ | `call:outgoing`, `call:ringing`, `call:answered` | Обновить ход исходящего или активного звонка. |
233
+ | `call:ended`, `call:failed` | Убрать карточку звонка или показать ошибку. |
234
+ | `call:held`, `call:resumed`, `call:muted`, `call:unmuted` | Обновить кнопки управления разговором. |
235
+ | `call:acd-context` | Получить публичный контекст очереди, если выдано право `ocp.acd_context.read`. |
236
+ | `registration:changed` | Обновить индикатор регистрации телефонии. |
237
+ | `account:session-activated`, `account:session-ended` | Обновить состояние учётной записи. |
238
+ | `operator:session-changed`, `operator:status-changed` | Обновить статус оператора и отложенную смену статуса. |
239
+ | `operator:campaign-offered`, `operator:campaign-cleared` | Обновить данные кампании. |
240
+ | `window:visibility-changed` | Обновить состояние окна Desktop. |
241
+ | `sdk:server-shutdown` | Показать сообщение, что Desktop завершает работу. |
242
+
243
+ ```ts
244
+ const stop = client.subscribe('call:incoming', (event) => {
245
+ console.log('Входящий звонок:', event.payload.callId);
246
+ });
247
+
248
+ // Позднее, например при размонтировании UI:
249
+ stop();
250
+ ```
251
+
252
+ ## API Reference
253
+
254
+ ### `createOmniCallClient(options)`
255
+
256
+ Создаёт полный клиент CRM. Конструктор не открывает сеть; вызовите `connect()`.
257
+
258
+ ```ts
259
+ function createOmniCallClient(options: OmniCallClientOptions): OmniCallClient;
260
+ ```
261
+
262
+ `OmniCallClientOptions` равен `AuthClientOptions`.
263
+
264
+ | Поле | Тип | Обязательно | Описание |
265
+ | --- | --- | --- | --- |
266
+ | `url` | `string` | Да | WebSocket URL SDK gateway Desktop. |
267
+ | `origin` | `string` | Да | Точный Origin CRM. |
268
+ | `application` | `ApplicationIdentity` | Да | Имя и версия вашего приложения. |
269
+ | `sdkVersion` | `string` | Да | Версия SDK, совместимая с Desktop. |
270
+ | `requestedProfile` | `PairingProfile` | Да | `presentation`, `operator` или `call_controller`. |
271
+ | `requestedCapabilities` | `readonly CapabilityId[]` | Нет | Запрашиваемые неприоритетные права. |
272
+ | `keyStore` | `PopKeyStore` | Да | Хранилище идентичности pairing. |
273
+ | `transportFactory`, `scheduler`, `jitter` | соответствующие интерфейсы | Нет | Заменяйте только в тестах или особой среде. Браузерные значения используются по умолчанию. |
274
+ | `diagnostics` | `DiagnosticsSink` | Нет | Получатель безопасных диагностических событий. |
275
+ | `defaultRequestTimeoutMs` | `number` | Нет | Таймаут команд в миллисекундах. |
276
+ | `reconnect` | `ReconnectPolicy` | Нет | Политика ограниченных повторных подключений. |
277
+ | `heartbeat` | `HeartbeatPolicy` | Нет | Настройка проверки живости соединения. |
278
+
279
+ Возвращает `OmniCallClient`. Ошибки настройки и соединения приходят через
280
+ `Promise` методов клиента как `OmniCallClientError`.
281
+
282
+ ### `OmniCallClient`
283
+
284
+ Полный клиент состоит из lifecycle-методов и namespaces `calls`, `window`,
285
+ `operator`, `account`.
286
+
287
+ #### `client.connect()`
288
+
289
+ ```ts
290
+ (): Promise<void>
291
+ ```
292
+
293
+ Открывает соединение и запускает pairing либо восстановление сессии. Promise
294
+ может быть отклонён ошибкой соединения. После успешного вызова всё ещё ждите
295
+ `ready` через `waitUntil()` или `onStateChange()`.
296
+
297
+ #### `client.disconnect()`
298
+
299
+ ```ts
300
+ (): void
301
+ ```
302
+
303
+ Закрывает только соединение SDK, отменяет таймеры и ожидающие запросы. Не
304
+ завершает звонок, не выполняет logout и не закрывает Desktop.
305
+
306
+ #### `client.getState()`
307
+
308
+ ```ts
309
+ (): ConnectionState
310
+ ```
311
+
312
+ Возвращает текущее состояние из таблицы выше. Не выполняет сетевой запрос.
313
+
314
+ #### `client.waitUntil(predicate, timeoutMs?)`
315
+
316
+ ```ts
317
+ (predicate: (state: ConnectionState) => boolean, timeoutMs?: number)
318
+ => Promise<ConnectionState>
319
+ ```
320
+
321
+ Ждёт состояние, подходящее условию. `timeoutMs` не обязателен. При превышении
322
+ времени Promise отклоняется.
323
+
324
+ ```ts
325
+ await client.waitUntil((state) => state === 'ready', 60_000);
326
+ ```
327
+
328
+ #### `client.onStateChange(listener)` и `client.onPairingRequired(listener)`
329
+
330
+ ```ts
331
+ (listener: (state: ConnectionState) => void): () => void
332
+ (listener: (info: PairingRequiredInfo) => void): () => void
333
+ ```
334
+
335
+ Подписывают на изменение состояния или ожидание pairing. Оба метода возвращают
336
+ функцию отписки. `PairingRequiredInfo` содержит `origin`, `requestedProfile` и
337
+ может содержать `clientId`.
338
+
339
+ #### `client.getSession()` и `client.getGrantedCapabilities()`
340
+
341
+ ```ts
342
+ (): AuthSessionSnapshot | undefined
343
+ (): readonly CapabilityId[]
344
+ ```
345
+
346
+ `getSession()` возвращает текущую сессию после авторизации либо `undefined`.
347
+ `getGrantedCapabilities()` возвращает права, которые Desktop действительно
348
+ выдал. Проверяйте их до показа кнопок.
349
+
350
+ ```ts
351
+ const canDial = client.getGrantedCapabilities().includes('call.originate');
352
+ ```
353
+
354
+ #### `client.getSnapshot()`, `getCachedSnapshot()` и `getRevision()`
355
+
356
+ ```ts
357
+ (): Promise<SnapshotMessage>
358
+ (): SnapshotMessage | undefined
359
+ (): number | undefined
360
+ ```
361
+
362
+ `getSnapshot()` запрашивает свежий снимок. `getCachedSnapshot()` не обращается в
363
+ сеть. `getRevision()` возвращает номер кэшированного снимка. Используйте свежий
364
+ snapshot перед мутацией.
365
+
366
+ #### `client.subscribe(type, listener)`
367
+
368
+ ```ts
369
+ <T extends PublicEventType>(
370
+ type: T,
371
+ listener: (event: OmniCallEventOf<T>) => void
372
+ ): () => void
373
+ ```
374
+
375
+ Подписывает на одно публичное событие и возвращает функцию отписки. Тип полезной
376
+ нагрузки выводится из значения `type`.
377
+
378
+ #### `client.getConnectError()` и `client.preauthDropCount()`
379
+
380
+ ```ts
381
+ (): OmniCallClientError | undefined
382
+ (): number
383
+ ```
384
+
385
+ Первый метод возвращает последнюю ошибку подключения. Второй возвращает
386
+ диагностический счётчик сообщений, отброшенных до авторизации. Не используйте его
387
+ как бизнес-метрику CRM.
388
+
389
+ ### `client.calls`
390
+
391
+ Все методы ниже возвращают `Promise<CallMutationResult>`, где есть `callId` и
392
+ новый `revision`. Все требуют актуальный `expectedRevision`.
393
+
394
+ | Метод и сигнатура | Назначение | Право |
395
+ | --- | --- | --- |
396
+ | `originate({ destination, expectedRevision })` | Начать исходящий звонок на строку `destination`. | `call.originate` |
397
+ | `answer({ callId, expectedRevision })` | Ответить на входящий звонок. | `call.answer` или `call.control` |
398
+ | `reject({ callId, expectedRevision })` | Отклонить входящий звонок. | `call.reject` или `call.control` |
399
+ | `hangup({ callId, expectedRevision })` | Завершить звонок. | `call.hangup` или `call.control` |
400
+ | `hold({ callId, expectedRevision })` | Поставить звонок на удержание. | `call.hold` или `call.control` |
401
+ | `resume({ callId, expectedRevision })` | Снять звонок с удержания. | `call.hold` или `call.control` |
402
+ | `mute({ callId, expectedRevision })` | Выключить микрофон. | `call.mute` или `call.control` |
403
+ | `unmute({ callId, expectedRevision })` | Включить микрофон. | `call.mute` или `call.control` |
404
+ | `sendDtmf({ callId, digits, expectedRevision })` | Отправить тональные цифры `digits`. | Только `call.control` |
405
+
406
+ Параметр `callId` — идентификатор звонка из snapshot или события. Возможные
407
+ ошибки: `forbidden`, `not_ready`, `not_found`, `stale_state`, `conflict` и
408
+ `operation_failed`.
409
+
410
+ Каждый метод использует один из двух контрактов:
411
+
412
+ ```ts
413
+ type CallActionInput = { callId: string; expectedRevision: number };
414
+ type OriginateInput = { destination: string; expectedRevision: number };
415
+ type DtmfInput = { callId: string; digits: string; expectedRevision: number };
416
+ type CallAction = (input: CallActionInput) => Promise<CallMutationResult>;
417
+ ```
418
+
419
+ #### `client.calls.originate(input)`
420
+
421
+ ```ts
422
+ (input: OriginateInput): Promise<CallMutationResult>
423
+ ```
424
+
425
+ Набирает `destination`. Передавайте номер в формате, который поддерживает
426
+ ваша телефония. Проверьте `call.originate`; при `operation_failed` с
427
+ `failure_kind: 'sip_not_registered'` сначала восстановите регистрацию Desktop.
428
+
429
+ ```ts
430
+ const revision = client.getRevision() ?? (await client.getSnapshot()).revision;
431
+ const result = await client.calls.originate({
432
+ destination: '+74951234567',
433
+ expectedRevision: revision
434
+ });
435
+ console.log(result.callId);
436
+ ```
437
+
438
+ #### `client.calls.answer(input)`
439
+
440
+ ```ts
441
+ (input: CallActionInput): Promise<CallMutationResult>
442
+ ```
443
+
444
+ Отвечает на звонок `callId`. Вызывайте только для входящего звонка из snapshot
445
+ или `call:incoming`. Если звонок уже завершён в другой вкладке, получите
446
+ `not_found`, `conflict` или `stale_state`.
447
+
448
+ ```ts
449
+ await client.calls.answer({ callId: 'call-123', expectedRevision: await getRevision() });
450
+ ```
451
+
452
+ #### `client.calls.reject(input)`
453
+
454
+ ```ts
455
+ (input: CallActionInput): Promise<CallMutationResult>
456
+ ```
457
+
458
+ Отклоняет входящий звонок. Аргументы и ошибки совпадают с `answer()`.
459
+
460
+ ```ts
461
+ await client.calls.reject({ callId: 'call-123', expectedRevision: await getRevision() });
462
+ ```
463
+
464
+ #### `client.calls.hangup(input)`
465
+
466
+ ```ts
467
+ (input: CallActionInput): Promise<CallMutationResult>
468
+ ```
469
+
470
+ Завершает звонок. Не вызывайте его автоматически в обработчике `disconnect()`.
471
+
472
+ ```ts
473
+ await client.calls.hangup({ callId: 'call-123', expectedRevision: await getRevision() });
474
+ ```
475
+
476
+ #### `client.calls.hold(input)` и `client.calls.resume(input)`
477
+
478
+ ```ts
479
+ (input: CallActionInput): Promise<CallMutationResult>
480
+ ```
481
+
482
+ `hold()` ставит активный звонок на удержание, а `resume()` возвращает его в
483
+ разговор. Оба требуют `call.hold` или `call.control`.
484
+
485
+ ```ts
486
+ await client.calls.hold({ callId: 'call-123', expectedRevision: await getRevision() });
487
+ await client.calls.resume({ callId: 'call-123', expectedRevision: await getRevision() });
488
+ ```
489
+
490
+ #### `client.calls.mute(input)` и `client.calls.unmute(input)`
491
+
492
+ ```ts
493
+ (input: CallActionInput): Promise<CallMutationResult>
494
+ ```
495
+
496
+ `mute()` выключает, а `unmute()` включает микрофон оператора. Оба требуют
497
+ `call.mute` или `call.control`.
498
+
499
+ ```ts
500
+ await client.calls.mute({ callId: 'call-123', expectedRevision: await getRevision() });
501
+ await client.calls.unmute({ callId: 'call-123', expectedRevision: await getRevision() });
502
+ ```
503
+
504
+ #### `client.calls.sendDtmf(input)`
505
+
506
+ ```ts
507
+ (input: DtmfInput): Promise<CallMutationResult>
508
+ ```
509
+
510
+ Отправляет тональные цифры `digits` во время подходящего звонка. Требует только
511
+ `call.control`; не используйте для передачи секретов.
512
+
513
+ ```ts
514
+ await client.calls.sendDtmf({
515
+ callId: 'call-123',
516
+ digits: '123#',
517
+ expectedRevision: await getRevision()
518
+ });
519
+ ```
520
+
521
+ ### `client.window`
522
+
523
+ | Метод | Сигнатура и результат | Условие |
524
+ | --- | --- | --- |
525
+ | `show()` | `(): Promise<{ visible: boolean; revision: number }>` | Право `window.show`. |
526
+ | `hide(input)` | `({ expectedRevision: number }) => Promise<{ visible: boolean; revision: number }>` | Привилегированное право `window.hide`; Desktop может вернуть `conflict`, если идёт разговор. |
527
+ | `getState()` | `(): Promise<{ visible: boolean; revision: number }>` | Только чтение состояния окна. |
528
+
529
+ ```ts
530
+ await client.window.show();
531
+ ```
532
+
533
+ #### `client.window.show()`
534
+
535
+ ```ts
536
+ (): Promise<{ visible: boolean; revision: number }>
537
+ ```
538
+
539
+ Показывает и фокусирует окно Desktop. Требует `window.show`. Результат содержит
540
+ фактическую видимость и новую версию состояния.
541
+
542
+ #### `client.window.hide(input)`
543
+
544
+ ```ts
545
+ ({ expectedRevision }: { expectedRevision: number })
546
+ => Promise<{ visible: boolean; revision: number }>
547
+ ```
548
+
549
+ Скрывает окно. Требует отдельно выданное привилегированное `window.hide`.
550
+ Desktop отклоняет запрос с `conflict`, когда скрытие небезопасно, например во
551
+ время разговора.
552
+
553
+ ```ts
554
+ await client.window.hide({ expectedRevision: await getRevision() });
555
+ ```
556
+
557
+ #### `client.window.getState()`
558
+
559
+ ```ts
560
+ (): Promise<{ visible: boolean; revision: number }>
561
+ ```
562
+
563
+ Запрашивает видимость окна, ничего не меняя.
564
+
565
+ ```ts
566
+ const { visible } = await client.window.getState();
567
+ ```
568
+
569
+ ### `client.account`
570
+
571
+ #### `client.account.logout(input)`
572
+
573
+ ```ts
574
+ ({ reasonId?, expectedRevision }: {
575
+ reasonId?: number;
576
+ expectedRevision: number;
577
+ }) => Promise<LogoutResult>
578
+ ```
579
+
580
+ Завершает операторскую сессию. Возвращает `{ loggedOut: true, revision }`.
581
+ Desktop может потребовать причину и вернуть `interaction_required`. В этом случае
582
+ получите причины через `client.operator.getReasons()`, покажите их пользователю и
583
+ повторите logout со свежим revision и выбранным `reasonId`.
584
+
585
+ #### `client.account.activateProfile(input)`
586
+
587
+ ```ts
588
+ ({ login, expectedRevision, mode? }: {
589
+ login: string;
590
+ expectedRevision: number;
591
+ mode?: 'sip_only' | 'ocp';
592
+ }) => Promise<ActivateProfileResult>
593
+ ```
594
+
595
+ Активирует ранее сохранённый в Desktop профиль без передачи пароля в CRM.
596
+ `mode` по умолчанию определяет Desktop. Возвращает `activated`, `mode`,
597
+ необязательные `profileLabel`, `alreadyAuthenticated` и `revision`. Требует
598
+ выданное сервером право `account.activate`.
599
+
600
+ ### `client.operator`
601
+
602
+ | Метод | Сигнатура и результат | Ошибки и ограничения |
603
+ | --- | --- | --- |
604
+ | `getReasons()` | `(): Promise<{ reasons: OperatorReason[]; revision: number }>` | Получает причины ready, break и logout. |
605
+ | `changeStatus(input)` | `({ target: 'ready' \| 'break', reasonId?, expectedRevision }) => Promise<OperatorStatusChangeResult>` | Требует `operator.status.write`. Результат содержит `kind: 'applied' \| 'reserved'`. |
606
+ | `finishAppeal(input)` | `({ expectedRevision }) => Promise<OperatorFinishAppealResult>` | Доступен в состоянии post-call processing. |
607
+
608
+ `reserved` означает: Desktop запомнил запрошенный статус и применит его после
609
+ текущего разговора. Не создавайте в CRM отдельную команду резервирования.
610
+
611
+ #### `client.operator.getReasons()`
612
+
613
+ ```ts
614
+ (): Promise<OperatorReasonsResult>
615
+ ```
616
+
617
+ Возвращает причины статусов и logout с полями `id`, `label`, `kind`, а также
618
+ `revision`. Используйте `id` выбранной оператором причины в следующей команде.
619
+
620
+ ```ts
621
+ const { reasons } = await client.operator.getReasons();
622
+ console.log(reasons.filter((reason) => reason.kind === 'break'));
623
+ ```
624
+
625
+ #### `client.operator.changeStatus(input)`
626
+
627
+ ```ts
628
+ ({ target, reasonId?, expectedRevision }: {
629
+ target: 'ready' | 'break';
630
+ reasonId?: number;
631
+ expectedRevision: number;
632
+ }) => Promise<OperatorStatusChangeResult>
633
+ ```
634
+
635
+ Меняет статус на `ready` или `break`. Возвращает `accepted: true`, реальный
636
+ `targetStatus`, `reasonId`, `revision` и `kind`. При активном разговоре `kind`
637
+ может быть `reserved`, а не `applied`.
638
+
639
+ ```ts
640
+ await client.operator.changeStatus({
641
+ target: 'ready',
642
+ expectedRevision: await getRevision()
643
+ });
644
+ ```
645
+
646
+ #### `client.operator.finishAppeal(input)`
647
+
648
+ ```ts
649
+ ({ expectedRevision }: { expectedRevision: number })
650
+ => Promise<OperatorFinishAppealResult>
651
+ ```
652
+
653
+ Заканчивает постобработку обращения. Вызывайте только когда публичный статус
654
+ оператора — `post_call_processing`; в остальных случаях Desktop вернёт ошибку
655
+ состояния или конфликта.
656
+
657
+ ```ts
658
+ await client.operator.finishAppeal({ expectedRevision: await getRevision() });
659
+ ```
660
+
661
+ ## Ошибки
662
+
663
+ ### `OmniCallClientError`
664
+
665
+ ```ts
666
+ new OmniCallClientError({
667
+ code: ProtocolErrorCode,
668
+ retryable: boolean,
669
+ currentRevision?: number,
670
+ details?: WireJsonObject
671
+ });
672
+ ```
673
+
674
+ Это класс ошибок SDK. Свойства `code`, `retryable`, `currentRevision` и `details`
675
+ помогают выбрать действие. Не выводите весь `details` в production-логи: там
676
+ могут быть чувствительные данные.
677
+
678
+ | Проверка и reader | Когда применять |
679
+ | --- | --- |
680
+ | `isOmniCallClientError(value)` | Проверить любую ошибку SDK. |
681
+ | `isConflictError(error)` + `readConflictErrorDetails(details)` | Обработать `conflict`. |
682
+ | `isInteractionRequiredError(error)` + `readInteractionRequiredDetails(details)` | Выбрать причину logout. |
683
+ | `isOperationFailedError(error)` + `readOperationFailedDetails(details)` | Узнать `failure_kind`, например `sip_not_registered`. |
684
+ | `isOriginBlockedError(value)` | Обработать заблокированный Origin. |
685
+
686
+ ```ts
687
+ import {
688
+ isOmniCallClientError,
689
+ isOperationFailedError,
690
+ readOperationFailedDetails
691
+ } from '@softomnitel/omnicall-kit';
692
+
693
+ try {
694
+ await client.calls.originate({
695
+ destination: '+74951234567',
696
+ expectedRevision: await getRevision()
697
+ });
698
+ } catch (error: unknown) {
699
+ if (isOperationFailedError(error)) {
700
+ console.warn(readOperationFailedDetails(error.details)?.failure_kind);
701
+ } else if (isOmniCallClientError(error)) {
702
+ console.warn(error.code, error.retryable);
703
+ } else {
704
+ throw error;
705
+ }
706
+ }
707
+ ```
708
+
709
+ ## Вспомогательные фабрики и низкоуровневые интерфейсы
710
+
711
+ Это API для тестов, нестандартных runtime-сред и диагностики. В обычной браузерной
712
+ CRM ничего из этого передавать в `createOmniCallClient()` не нужно.
713
+
714
+ | Экспорт | Сигнатура | Для чего нужен |
715
+ | --- | --- | --- |
716
+ | `createAuthClient(options)` | `(AuthClientOptions) => AuthClient` | Клиент только pairing и lifecycle без `calls`, `window`, `operator`, `account`. |
717
+ | `createIndexedDbPopKeyStore({ installId })` | `({ installId: string }) => PopKeyStore` | Постоянное браузерное хранилище pairing. |
718
+ | `createMemoryPopKeyStore(initial?)` | `(StoredPopIdentity?) => PopKeyStore & { peek() }` | Временное хранилище для тестов. |
719
+ | `createBrowserWebSocketTransport(options?)` | `({ webSocket? }?) => TransportPort` | Стандартный транспорт браузера. |
720
+ | `createBrowserScheduler()` | `() => Scheduler` | Реальные таймеры браузера. |
721
+ | `createBrowserJitterSource()` | `() => JitterSource` | Случайная задержка reconnect. |
722
+ | `createFakeScheduler(startMs?)` | `(number?) => FakeScheduler` | Управляемые таймеры в тестах. |
723
+ | `createFixedJitterSource(value)` | `(number) => JitterSource` | Предсказуемый jitter в тестах. |
724
+ | `createRecordingDiagnosticsSink()` | `() => DiagnosticsSink & { events; clear() }` | Сбор безопасных диагностических событий. |
725
+
726
+ `PopKeyStore` имеет методы `load()`, `save(identity)` и `clear()`. Его
727
+ `StoredPopIdentity` содержит `clientId`, открытый ключ, непередаваемый
728
+ `CryptoKey`, профиль и выданные права. Не сериализуйте приватный `CryptoKey` и не
729
+ отправляйте его на сервер CRM.
730
+
731
+ `TransportPort` определяет `connect(url)`, `send(data)`, `close(code?, reason?)`
732
+ и подписки `onOpen`, `onMessage`, `onClose`, `onError`. Он работает только со
733
+ строковыми сообщениями. Не добавляйте в него собственный JSON-парсер или цикл
734
+ reconnect: этим управляет сессия SDK.
735
+
736
+ `Scheduler` определяет `now()` и `setTimeout(callback, delayMs)`. Возвращаемый
737
+ `TimerHandle` имеет `clear()`. `FakeScheduler` добавляет `advanceBy`,
738
+ `advanceByAsync`, `pendingTimerCount` и `clearAll`.
739
+
740
+ ### Политики и диагностика
741
+
742
+ ```ts
743
+ type ReconnectPolicy = {
744
+ maxAttempts: number;
745
+ initialDelayMs: number;
746
+ maxDelayMs: number;
747
+ jitterRatio: number;
748
+ };
749
+
750
+ type HeartbeatPolicy = {
751
+ enabled: boolean;
752
+ intervalMs: number;
753
+ timeoutMs: number;
754
+ };
755
+ ```
756
+
757
+ `DiagnosticsSink.emit(event)` получает `DiagnosticEvent`: уровень
758
+ `debug | info | warn | error`, код, состояние и необязательные `requestId`,
759
+ `commandType`, длительность и ошибку. Логируйте коды и идентификаторы запросов,
760
+ но не логируйте номера телефонов, токены или полный payload.
761
+
762
+ ## Типы, константы и полный индекс экспорта
763
+
764
+ Ниже перечислены все публичные экспорты пакета. Для структур сообщений протокола
765
+ `SnapshotMessage`, `SnapshotSections`, `SnapshotCallSummary`,
766
+ `ApplicationIdentity`, `CapabilityId`, `ProtocolErrorCode`,
767
+ `PublicOperatorStatus` и `WireJsonObject` источником истины остаётся
768
+ [`etc/api/sdk.api.md`](../../etc/api/sdk.api.md): они re-exported из протокольного
769
+ пакета.
770
+
771
+ | Группа | Экспорты |
772
+ | --- | --- |
773
+ | Клиент и auth | `OmniCallClient`, `OmniCallClientOptions`, `AuthClient`, `AuthClientOptions`, `AuthSessionSnapshot`, `PairingRequiredInfo`, `CONNECTION_STATES`, `ConnectionState` |
774
+ | Команды и результаты | `OmniCallCallsApi`, `OmniCallWindowApi`, `OmniCallAccountApi`, `OmniCallOperatorApi`, `CallMutationResult`, `LogoutResult`, `ActivateProfileMode`, `ActivateProfileResult`, `OperatorReason`, `OperatorReasonsResult`, `OperatorStatusChangeKind`, `OperatorStatusChangeResult`, `OperatorFinishAppealResult` |
775
+ | События | `PUBLIC_EVENT_TYPES`, `PublicEventType`, `OmniCallEvent`, `OmniCallEventOf` |
776
+ | Ошибки | `OmniCallClientError`, `ConflictErrorDetails`, `InteractionRequiredDetails`, `OperationFailedDetails`, пять type guard/readers из раздела ошибок |
777
+ | Хранилище | `PopKeyStore`, `StoredPopIdentity`, `createIndexedDbPopKeyStore`, `createMemoryPopKeyStore` |
778
+ | Транспорт | `BrowserWebSocketConstructor`, `BrowserWebSocketLike`, `CreateBrowserWebSocketTransportOptions`, `TransportFactory`, `TransportPort`, `TransportCloseInfo`, `TransportErrorInfo`, `createBrowserWebSocketTransport` |
779
+ | Время и reconnect | `Scheduler`, `TimerHandle`, `FakeScheduler`, `JitterSource`, `ReconnectPolicy`, `HeartbeatPolicy`, `createBrowserScheduler`, `createBrowserJitterSource`, `createFakeScheduler`, `createFixedJitterSource` |
780
+ | Диагностика | `DiagnosticsSink`, `DiagnosticEvent`, `DiagnosticLevel`, `DiagnosticResult`, `createRecordingDiagnosticsSink` |
781
+ | Протокольные re-export | `CapabilityId`, `ProtocolErrorCode`, `PublicOperatorStatus`, `SnapshotMessage`, `SnapshotSections`, `SnapshotCallSummary`, `WireJsonObject` |
782
+ | Константы activation | `SDK_ACTIVATE_CONSENT_TTL_MS`, `SDK_ACTIVATE_SIP_ONLY_AUTH_BUDGET_MS`, `SDK_ACTIVATE_OCP_AUTH_BUDGET_MS`, `SDK_ACTIVATE_CLIENT_TIMEOUT_MS` |
783
+
784
+ Четыре константы activation задают верхние границы ожидания согласия и
785
+ авторизации в миллисекундах. Используйте их, если вашему UI нужно показать
786
+ таймер; не меняйте их смысл локальными таймаутами.
787
+
788
+ ## Рецепты
789
+
790
+ ### Обновить UI после reconnect
791
+
792
+ ```ts
793
+ client.onStateChange((state) => {
794
+ if (state === 'ready') {
795
+ void client.getSnapshot().then((snapshot) => {
796
+ console.log('Обновите UI из снимка', snapshot.revision);
797
+ });
798
+ }
799
+ });
800
+ ```
801
+
802
+ ### Безопасно сменить статус оператора
803
+
804
+ ```ts
805
+ const expectedRevision =
806
+ client.getRevision() ?? (await client.getSnapshot()).revision;
807
+
808
+ const result = await client.operator.changeStatus({
809
+ target: 'break',
810
+ expectedRevision
811
+ });
812
+
813
+ if (result.kind === 'reserved') {
814
+ console.info('Перерыв начнётся после текущего разговора.');
815
+ }
816
+ ```
817
+
818
+ ### Logout с выбором причины
819
+
820
+ ```ts
821
+ const { reasons } = await client.operator.getReasons();
822
+ const reason = reasons.find((item) => item.kind === 'logout');
823
+
824
+ if (reason !== undefined) {
825
+ await client.account.logout({
826
+ reasonId: reason.id,
827
+ expectedRevision: await getRevision()
828
+ });
829
+ }
830
+ ```
831
+
832
+ ## Ошибки и FAQ
833
+
834
+ ### `stale_state`: команда отклонена
835
+
836
+ Другая вкладка или Desktop изменили состояние после вашего snapshot. Вызовите
837
+ `getSnapshot()`, обновите UI и попросите пользователя повторить действие. Не
838
+ повторяйте команду со старым `expectedRevision`.
839
+
840
+ ### `forbidden`: кнопка не работает
841
+
842
+ Desktop не выдал capability или оператор запретил Origin. Проверьте
843
+ `getGrantedCapabilities()`. Для `window.hide` и `account.activate` оператор
844
+ должен выдать право в Origin matrix Desktop.
845
+
846
+ ### `pairing_required` не заканчивается
847
+
848
+ Оператор ещё не подтвердил CRM в OmniCall Desktop. Покажите адрес из
849
+ `PairingRequiredInfo.origin` и не скрывайте это состояние под бесконечным
850
+ спиннером.
851
+
852
+ ### `local_network_permission_required` или `_denied`
853
+
854
+ Браузер не разрешил HTTPS-странице обратиться к локальной программе. Объясните,
855
+ где выдать разрешение, затем разрешите оператору подключиться снова.
856
+
857
+ ### Нужен ли отдельный WebSocket-клиент?
858
+
859
+ Нет. Используйте `createOmniCallClient()` и транспорт SDK по умолчанию. Свой
860
+ WebSocket-клиент обходит проверку сообщений и может нарушить reconnect.
861
+
862
+ ### Можно ли хранить SIP-пароль или ключ OCP в CRM?
863
+
864
+ Нет. Эти секреты остаются в OmniCall Desktop. Для ранее сохранённого профиля
865
+ используйте `activateProfile({ login, ... })` после явной выдачи capability.
866
+
867
+ ## Миграция и совместимость
868
+
869
+ SDK не совместим с legacy `window.Softphone` и не предоставляет HTTP fallback.
870
+ Переносите интеграцию на `createOmniCallClient()`, snapshot, публичные события и
871
+ namespaces клиента.
872
+
873
+ При `incompatible_version` остановите функции телефонии и предложите обновить
874
+ пакет или Desktop. Добавление необязательных полей совместимо. Удаление,
875
+ переименование или изменение смысла API требует новой major-версии. Не опирайтесь
876
+ на недокументированные поля сетевых сообщений.
877
+
878
+ Подробности: [upgrade-deprecation.md](../../docs/guide/upgrade-deprecation.md).
879
+
880
+ ## Лицензия
881
+
882
+ В package.json указано UNLICENSED. Это не open-source лицензия: не предполагайте право на свободное распространение или изменение вне согласованного контура. Уточните условия у владельца пакета.
883
+
884
+ ## Дополнительные материалы
885
+
886
+ - [Отчёт аудита документации](../../docs/guide/README-AUDIT-RU.md)
887
+ - [Полный API report, созданный API Extractor](../../etc/api/sdk.api.md)
888
+ - [Подробный русскоязычный гайд](../../docs/guide/RU-DEVELOPER-GUIDE.md)
889
+ - [Пример pairing CRM](../../examples/crm-pairing-lite/)
890
+ - [Поддержка, релизы и откат](../../docs/guide/release-and-support.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softomnitel/omnicall-kit",
3
- "version": "0.1.0-rc.0",
3
+ "version": "0.1.1",
4
4
  "description": "Browser client for OmniCall Desktop local protocol (OmniCallClient read path + call control)",
5
5
  "type": "module",
6
6
  "private": false,
@@ -28,7 +28,7 @@
28
28
  "clean": "node -e \"import('node:fs').then((fs)=>fs.promises.rm('dist',{recursive:true,force:true}))\""
29
29
  },
30
30
  "dependencies": {
31
- "@softomnitel/omnicall-protocol": "0.1.0-rc.0"
31
+ "@softomnitel/omnicall-protocol": "0.1.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public",