@vibexnpm/talkx 8.7.0 → 8.10.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/dist/index.d.ts +63 -5
- package/dist/talkflow-sdk.esm.js +1 -1
- package/dist/talkflow-sdk.esm.js.map +1 -1
- package/dist/talkflow-sdk.standalone.js +1 -1
- package/dist/talkflow-sdk.standalone.js.map +1 -1
- package/dist/talkflow-sdk.umd.js +1 -1
- package/dist/talkflow-sdk.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/TalkFlowClient.js +9 -0
- package/src/chat/ChatClient.js +11 -0
- package/src/constants.js +10 -1
- package/src/core/ConnectionManager.js +68 -6
- package/src/talkflow/eventForwarding.js +4 -0
- package/types/__typetests__/aiUnavailable.types.ts +48 -0
- package/types/__typetests__/webReading.types.ts +18 -0
- package/types/index.d.ts +63 -5
package/package.json
CHANGED
package/src/TalkFlowClient.js
CHANGED
|
@@ -211,6 +211,15 @@ class TalkFlowClient extends EventEmitter {
|
|
|
211
211
|
return this.connectionManager ? this.connectionManager.isConnected() : false;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* 회사 구독 대기 중인가 — 서버가 웹소켓을 NOT_SUBSCRIBED 로 거부해 연결을 멈췄다(결제 전·해지 회사).
|
|
216
|
+
* 결제가 확인되면(`GET /api/v1/company/billing` 의 `subscription === 'ACTIVE'`) 같은 토큰으로 {@link #connect} 를 다시 부른다.
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
isSubscriptionRequired() {
|
|
220
|
+
return this.connectionManager ? this.connectionManager.isSubscriptionRequired() : false;
|
|
221
|
+
}
|
|
222
|
+
|
|
214
223
|
_enableRefreshSession() {
|
|
215
224
|
if (this.authSession) return;
|
|
216
225
|
this.options.sessionMode = 'refresh';
|
package/src/chat/ChatClient.js
CHANGED
|
@@ -147,6 +147,7 @@ class ChatClient extends EventEmitter {
|
|
|
147
147
|
PLANNING: '기획 중…',
|
|
148
148
|
DOCUMENT_ANALYSIS: '문서 분석 중…',
|
|
149
149
|
VIDEO_ANALYSIS: '영상 분석 중…',
|
|
150
|
+
WEB_READING: '페이지 읽는 중…',
|
|
150
151
|
WRITING: '작성 중…'
|
|
151
152
|
};
|
|
152
153
|
// PHASE 의 expiresAt 누락/이상 시 fallback TTL (ms) — 서버 ttl(최소 30s) 보다 약간 길게.
|
|
@@ -3067,6 +3068,16 @@ class ChatClient extends EventEmitter {
|
|
|
3067
3068
|
unreadVersion: event.unreadVersion
|
|
3068
3069
|
});
|
|
3069
3070
|
break;
|
|
3071
|
+
case RoomListEventType.AI_UNAVAILABLE:
|
|
3072
|
+
// PM 을 직접 불렀는데 회사 AI 가 막혀 답하지 않았다 — 부른 사람에게만 온다. 잠깐 알리고 사라지게(토스트).
|
|
3073
|
+
// 리스트·미독은 건드리지 않는다. 막힌 상태를 계속 보여 줄 띠는 getMyProfile().aiBlocked 로 그린다.
|
|
3074
|
+
this.emit('aiUnavailable', {
|
|
3075
|
+
roomId: event.roomId,
|
|
3076
|
+
messageId: event.messageId,
|
|
3077
|
+
aiBlocked: event.aiBlocked,
|
|
3078
|
+
timestamp: event.timestamp
|
|
3079
|
+
});
|
|
3080
|
+
break;
|
|
3070
3081
|
default:
|
|
3071
3082
|
this.logger.warn('Unknown room list event type:', event.eventType);
|
|
3072
3083
|
}
|
package/src/constants.js
CHANGED
|
@@ -22,6 +22,8 @@ export const ErrorTypes = {
|
|
|
22
22
|
CONNECTION_FAILED: 'CONNECTION_FAILED',
|
|
23
23
|
CONNECTION_LOST: 'CONNECTION_LOST',
|
|
24
24
|
CONNECTION_TIMEOUT: 'CONNECTION_TIMEOUT',
|
|
25
|
+
// 회사 구독 없음 — 서버가 NOT_SUBSCRIBED 로 거부했다(결제 전·해지). 재연결하지 않고 connect() 를 기다린다.
|
|
26
|
+
SUBSCRIPTION_REQUIRED: 'SUBSCRIPTION_REQUIRED',
|
|
25
27
|
|
|
26
28
|
// Authentication errors
|
|
27
29
|
JWT_INVALID: 'JWT_INVALID',
|
|
@@ -353,7 +355,14 @@ export const RoomListEventType = {
|
|
|
353
355
|
* unreadVersion 으로 순서 역전(읽음 0 이 신규 메시지보다 먼저 도착)을 방어한다.
|
|
354
356
|
* SDK 는 이를 roomListUnreadUpdated 이벤트로 emit 한다.
|
|
355
357
|
*/
|
|
356
|
-
ROOM_UNREAD_UPDATED: 'ROOM_UNREAD_UPDATED'
|
|
358
|
+
ROOM_UNREAD_UPDATED: 'ROOM_UNREAD_UPDATED',
|
|
359
|
+
/**
|
|
360
|
+
* PM 을 직접(@PM) 불렀는데 회사 AI 가 막혀(크레딧·결제) 답하지 않았다 → 부른 사람에게만 온다. 방에는 아무것도 남지 않는다.
|
|
361
|
+
* 잠깐 알리고 사라지게(토스트) 쓴다. 리스트·미독은 건드리지 않는다. 사유는 aiBlocked(REST 402 와 같은 코드).
|
|
362
|
+
* 막힌 상태를 계속 보여 줄 띠는 getMyProfile() 의 aiBlocked 로 그린다.
|
|
363
|
+
* SDK 는 이를 aiUnavailable 이벤트로 emit 한다.
|
|
364
|
+
*/
|
|
365
|
+
AI_UNAVAILABLE: 'AI_UNAVAILABLE'
|
|
357
366
|
};
|
|
358
367
|
|
|
359
368
|
/**
|
|
@@ -10,6 +10,11 @@ import { ConnectionState, ErrorTypes, WebSocketPaths, DefaultConfig, LogLevel }
|
|
|
10
10
|
import { Client as StompClient, ReconnectionTimeMode, StompSocketState } from '@stomp/stompjs';
|
|
11
11
|
import SockJS from 'sockjs-client';
|
|
12
12
|
|
|
13
|
+
/** 서버 구독 관문의 거부 코드(STOMP ERROR 본문 code · HTTP 402) — 결제 전·해지 회사. */
|
|
14
|
+
const SUBSCRIPTION_REQUIRED_CODE = 'NOT_SUBSCRIBED';
|
|
15
|
+
/** 서버가 구독 상태를 잠시 확인하지 못했다(503) — 대기가 아니라 일시 실패다. */
|
|
16
|
+
const SUBSCRIPTION_CHECK_UNAVAILABLE_CODE = 'SUBSCRIPTION_CHECK_UNAVAILABLE';
|
|
17
|
+
|
|
13
18
|
// STOMP Client 가져오기 (CDN 사용 시 전역 객체 우선)
|
|
14
19
|
const getStompClient = () => {
|
|
15
20
|
// noinspection JSUnresolvedReference
|
|
@@ -82,6 +87,9 @@ class ConnectionManager extends EventEmitter {
|
|
|
82
87
|
this._recoveryTimer = null;
|
|
83
88
|
this._terminalAuthError = false;
|
|
84
89
|
this._recoveryEligible = false;
|
|
90
|
+
// 회사 구독 대기 — 서버가 CONNECT·SUBSCRIBE·SEND 를 NOT_SUBSCRIBED 로 거부했다(결제 전·해지 회사).
|
|
91
|
+
// 인증 거부와 다르다: 토큰은 그대로 유효하고, 결제가 끝나면 앱이 같은 토큰으로 connect() 를 다시 부른다.
|
|
92
|
+
this._subscriptionRequired = false;
|
|
85
93
|
|
|
86
94
|
this._installLifecycleListeners();
|
|
87
95
|
}
|
|
@@ -153,7 +161,7 @@ class ConnectionManager extends EventEmitter {
|
|
|
153
161
|
* @private
|
|
154
162
|
*/
|
|
155
163
|
_onResume(reason) {
|
|
156
|
-
if (!this.resumeOnVisibility || this._intentionalDisconnect || this._terminalAuthError) {
|
|
164
|
+
if (!this.resumeOnVisibility || this._intentionalDisconnect || this._terminalAuthError || this._subscriptionRequired) {
|
|
157
165
|
return;
|
|
158
166
|
}
|
|
159
167
|
|
|
@@ -248,7 +256,10 @@ class ConnectionManager extends EventEmitter {
|
|
|
248
256
|
this.stompClient = null;
|
|
249
257
|
}
|
|
250
258
|
|
|
251
|
-
|
|
259
|
+
// 구독 대기면 다시 붙지 않는다 — refresh 세션의 토큰 갱신(AuthSession 'access-token-renewed')·저빈도 복구·탭 복귀가 모두
|
|
260
|
+
// 이리로 오고, 정리를 기다리는 동안 옛 클라이언트가 NOT_SUBSCRIBED 로 대기에 들어간 경우도 여기서 걸린다. 대기는 앱이 connect() 를
|
|
261
|
+
// 다시 불러야 풀린다.
|
|
262
|
+
if (this._intentionalDisconnect || this._terminalAuthError || this._subscriptionRequired) return;
|
|
252
263
|
|
|
253
264
|
// connect() 가 CONNECTED/CONNECTING 이면 조기 반환하므로 상태를 먼저 내려둔다.
|
|
254
265
|
this._setState(ConnectionState.DISCONNECTED);
|
|
@@ -407,6 +418,8 @@ class ConnectionManager extends EventEmitter {
|
|
|
407
418
|
}
|
|
408
419
|
this._cancelReconnectRecovery();
|
|
409
420
|
this._terminalAuthError = false;
|
|
421
|
+
// 구독 대기는 앱이 connect() 를 다시 불러야 풀린다(결제 확인 뒤). 아직 구독이 없으면 서버가 다시 거부해 대기로 돌아온다.
|
|
422
|
+
this._subscriptionRequired = false;
|
|
410
423
|
this._setState(ConnectionState.CONNECTING);
|
|
411
424
|
this.reconnectAttempts = 0;
|
|
412
425
|
this._intentionalDisconnect = false;
|
|
@@ -476,10 +489,20 @@ class ConnectionManager extends EventEmitter {
|
|
|
476
489
|
* @private
|
|
477
490
|
*/
|
|
478
491
|
_onStompError(frame) {
|
|
492
|
+
let code = null;
|
|
493
|
+
let serverMessage = null;
|
|
479
494
|
try {
|
|
480
495
|
const payload = JSON.parse(frame.body || frame.headers?.message || '{}');
|
|
481
|
-
|
|
496
|
+
code = payload.code ?? null;
|
|
497
|
+
serverMessage = typeof payload.message === 'string' ? payload.message : null;
|
|
498
|
+
this._authRejected = ['ACCESS_UNAUTHORIZED', 'SESSION_REVOKED', 'TOKEN_INVALID'].includes(code);
|
|
482
499
|
} catch { /* Non-authentication STOMP errors keep the normal reconnect policy. */ }
|
|
500
|
+
// 회사 구독 없음 — 재연결해도 같은 거부다. 인증 거부로 기록하지 않고 «구독 대기» 로 멈춘다.
|
|
501
|
+
// 구독 확인 일시 실패(SUBSCRIPTION_CHECK_UNAVAILABLE)는 아래 기본 경로 — 평소처럼 다시 붙는다.
|
|
502
|
+
if (code === SUBSCRIPTION_REQUIRED_CODE) {
|
|
503
|
+
this._stopForSubscription(serverMessage);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
483
506
|
this.logger.error('STOMP error:', frame);
|
|
484
507
|
this._setState(ConnectionState.ERROR);
|
|
485
508
|
|
|
@@ -490,7 +513,9 @@ class ConnectionManager extends EventEmitter {
|
|
|
490
513
|
};
|
|
491
514
|
|
|
492
515
|
if (this._authRejected) this._recordAuthRejection();
|
|
493
|
-
|
|
516
|
+
// 구독 확인 일시 실패는 첫 연결이라도 대기 중인 connect() 를 끝내지 않는다 — STOMP 가 곧 다시 붙는다. 여기서 reject 하면
|
|
517
|
+
// 앱은 실패로 알고 방 목록 구독 등 후속 초기화를 건너뛰는데 연결은 붙어 «반쪽 연결» 이 된다.
|
|
518
|
+
const terminal = code !== SUBSCRIPTION_CHECK_UNAVAILABLE_CODE && !this._canRecoverConnection();
|
|
494
519
|
if (terminal) this._drainPendingSubscriptions(`STOMP error: ${error.message}`);
|
|
495
520
|
|
|
496
521
|
this.emit('error', error);
|
|
@@ -512,7 +537,7 @@ class ConnectionManager extends EventEmitter {
|
|
|
512
537
|
|
|
513
538
|
// forceReconnect 의 의도적 teardown 이면 재연결 시도로 집계하지 않는다 —
|
|
514
539
|
// 카운터가 오염되면 복귀 직후 다시 max 를 소진해버린다.
|
|
515
|
-
if (this._tearingDown || this._intentionalDisconnect || this._terminalAuthError) {
|
|
540
|
+
if (this._tearingDown || this._intentionalDisconnect || this._terminalAuthError || this._subscriptionRequired) {
|
|
516
541
|
this.logger.debug('WebSocket closed during intentional teardown — not counting as reconnect');
|
|
517
542
|
return;
|
|
518
543
|
}
|
|
@@ -618,7 +643,43 @@ class ConnectionManager extends EventEmitter {
|
|
|
618
643
|
|
|
619
644
|
_canRecoverConnection() {
|
|
620
645
|
return this._recoveryEligible && !this._intentionalDisconnect && !this._terminalAuthError
|
|
621
|
-
&& this.reconnectRecoveryDelay > 0;
|
|
646
|
+
&& !this._subscriptionRequired && this.reconnectRecoveryDelay > 0;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** 회사 구독 대기 중인가 — 서버가 NOT_SUBSCRIBED 로 거부해 연결을 멈췄다. 앱이 connect() 를 다시 부르면 풀린다. */
|
|
650
|
+
isSubscriptionRequired() {
|
|
651
|
+
return this._subscriptionRequired;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* «구독 대기» 로 멈춘다 (서버 구독 관문 설계 v3 §7).
|
|
656
|
+
*
|
|
657
|
+
* <p>인증 정지({@link #_stopForAuthError})와 같은 방식으로 STOMP 재연결 루프·저빈도 복구·탭 복귀 재연결을 끄되, 토큰은
|
|
658
|
+
* 거부된 토큰으로 기록하지 않는다 — 토큰 갱신·로그인은 그대로 둔다. 대기 중인 connect()·구독은 reject 하고, 한 번 들어올 때
|
|
659
|
+
* {@code subscriptionRequired} 를 알린다(일반 {@code error} 는 내지 않는다 — 연결 오류 안내가 결제 안내를 가리지 않게).
|
|
660
|
+
* 연결 중에 해지되면 다음 SUBSCRIBE·SEND 의 ERROR 로 같은 경로에 온다.</p>
|
|
661
|
+
*
|
|
662
|
+
* @private
|
|
663
|
+
*/
|
|
664
|
+
_stopForSubscription(serverMessage) {
|
|
665
|
+
const entering = !this._subscriptionRequired;
|
|
666
|
+
this._subscriptionRequired = true;
|
|
667
|
+
this._cancelReconnectRecovery();
|
|
668
|
+
this._setState(ConnectionState.ERROR);
|
|
669
|
+
// 본문의 안내 문구 — message 헤더는 오류 JSON 전체가 이스케이프된 채 오므로 쓰지 않는다.
|
|
670
|
+
const message = serverMessage || 'Company subscription required';
|
|
671
|
+
const error = Object.assign(new Error('Company subscription required'),
|
|
672
|
+
{ code: SUBSCRIPTION_REQUIRED_CODE, type: ErrorTypes.SUBSCRIPTION_REQUIRED });
|
|
673
|
+
this._connectReject?.(error);
|
|
674
|
+
this._connectResolve = this._connectReject = null;
|
|
675
|
+
this._drainPendingSubscriptions('Company subscription required');
|
|
676
|
+
const client = this.stompClient;
|
|
677
|
+
Promise.resolve(client?.deactivate()).catch(failure => this.logger.warn('Subscription teardown failed:', failure));
|
|
678
|
+
this.logger.warn('Company subscription required — connection stopped until connect() is called again');
|
|
679
|
+
if (entering) {
|
|
680
|
+
// 정지 전이를 마친 뒤 알린다 — 처리기가 곧바로 connect() 를 불러도 정리 중인 연결과 섞이지 않는다.
|
|
681
|
+
Promise.resolve().then(() => this.emit('subscriptionRequired', { code: SUBSCRIPTION_REQUIRED_CODE, message }));
|
|
682
|
+
}
|
|
622
683
|
}
|
|
623
684
|
|
|
624
685
|
/** 기존에 연결됐던 클라이언트만 긴 장애 후 탭 이벤트 없이 다시 시도한다. */
|
|
@@ -795,6 +856,7 @@ class ConnectionManager extends EventEmitter {
|
|
|
795
856
|
// 명시적 해제 — 포그라운드 복귀가 이걸 되살리면 안 된다.
|
|
796
857
|
this._intentionalDisconnect = true;
|
|
797
858
|
this._recoveryEligible = false;
|
|
859
|
+
this._subscriptionRequired = false;
|
|
798
860
|
this._authRejected = false;
|
|
799
861
|
this._rejectedToken = null;
|
|
800
862
|
this._cancelReconnectRecovery();
|
|
@@ -7,6 +7,8 @@ const CONNECTION_EVENT_MAP = [
|
|
|
7
7
|
// 재연결 '성공' + 포그라운드 복귀 감지 (RECONNECT_SYNC_DESIGN §6.1/§6.4)
|
|
8
8
|
['reconnected', 'reconnected'],
|
|
9
9
|
['resumed', 'resumed'],
|
|
10
|
+
// 회사 구독 대기(서버 구독 관문) — 연결 오류와 따로 알린다. 결제 뒤 connect() 를 다시 부르면 풀린다.
|
|
11
|
+
['subscriptionRequired', 'subscriptionRequired'],
|
|
10
12
|
['error', 'connectionError']
|
|
11
13
|
];
|
|
12
14
|
|
|
@@ -51,6 +53,8 @@ const CHAT_EVENT_MAP = [
|
|
|
51
53
|
['roomListDeleted', 'roomListDeleted'],
|
|
52
54
|
['roomListRoomUpdated', 'roomListRoomUpdated'],
|
|
53
55
|
['roomListUnreadUpdated', 'roomListUnreadUpdated'],
|
|
56
|
+
// @PM 을 불렀는데 회사 AI 가 막힘 — 부른 사람에게만 오는 방 목록 채널 알림(AI_UNAVAILABLE).
|
|
57
|
+
['aiUnavailable', 'aiUnavailable'],
|
|
54
58
|
// Gmail 메일함 — mailChanged 는 받은 그대로. 새 메일 알림(newMail)은 아래에서 푸시와 합쳐 따로 낸다.
|
|
55
59
|
['mailSubscribed', 'mailSubscribed'],
|
|
56
60
|
['mailUnsubscribed', 'mailUnsubscribed'],
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* «AI 사용 불가» 알림·회사 AI 막힘 띠(8.10.0) — FE 가 타입 단언 없이 쓰는지 tsc 로 고정한다.
|
|
3
|
+
*
|
|
4
|
+
* <p>jest 는 `.d.ts` 를 보지 않는다. 최상위 client 와 chat 양쪽에서 `aiUnavailable` 를 구독할 수 있는지, 사유가 유니온인지,
|
|
5
|
+
* `getMyProfile()` 의 `aiBlocked` 에 구독 없음(`NOT_SUBSCRIBED`)이 들어갈 수 없는지(서버가 `/me` 에는 싣지 않는다)를 본다.</p>
|
|
6
|
+
*
|
|
7
|
+
* <p>실행: {@code npm run typecheck}</p>
|
|
8
|
+
*/
|
|
9
|
+
import TalkFlowClient, { RoomListEventType } from '../index';
|
|
10
|
+
import type { AiBlockedReason, AiUnavailableEvent, ChatClient, MyProfile, RoomListUpdateEvent } from '../index';
|
|
11
|
+
|
|
12
|
+
declare const talkflow: TalkFlowClient;
|
|
13
|
+
/** {@code talkflow.chat} 은 연결 전 null 이라 선언이 nullable 이다 — 연결된 chat 을 직접 받는 모양으로 본다. */
|
|
14
|
+
declare const chat: ChatClient;
|
|
15
|
+
|
|
16
|
+
function toast(_reason: AiBlockedReason, _roomId: string): void {}
|
|
17
|
+
|
|
18
|
+
// 전용 이벤트 — 최상위·chat 양쪽
|
|
19
|
+
talkflow.on('aiUnavailable', (e: AiUnavailableEvent) => toast(e.aiBlocked, e.roomId));
|
|
20
|
+
chat.on('aiUnavailable', (e) => {
|
|
21
|
+
const reason: AiBlockedReason = e.aiBlocked;
|
|
22
|
+
const messageId: string | undefined = e.messageId;
|
|
23
|
+
toast(reason, e.roomId);
|
|
24
|
+
return messageId;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// 통합 이벤트로 받던 코드도 단언 없이 좁혀진다
|
|
28
|
+
talkflow.on('roomListUpdate', (e: RoomListUpdateEvent) => {
|
|
29
|
+
if (e.eventType === RoomListEventType.AI_UNAVAILABLE && e.aiBlocked) {
|
|
30
|
+
toast(e.aiBlocked, e.roomId);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 사유 유니온 — 서버 REST 402 코드와 같다
|
|
35
|
+
const reasons: AiBlockedReason[] = ['CREDIT_EXHAUSTED', 'CREDIT_INSUFFICIENT', 'BILLING_SUSPENDED', 'NOT_SUBSCRIBED'];
|
|
36
|
+
// @ts-expect-error — 모르는 사유는 막힌다
|
|
37
|
+
const unknownReason: AiBlockedReason = 'SUSPENDED';
|
|
38
|
+
|
|
39
|
+
// 채팅방 상단 띠 — /me 의 aiBlocked
|
|
40
|
+
async function banner(): Promise<string | null> {
|
|
41
|
+
const me: MyProfile = await talkflow.getMyProfile();
|
|
42
|
+
return me.aiBlocked ?? null;
|
|
43
|
+
}
|
|
44
|
+
const profileReason: NonNullable<MyProfile['aiBlocked']> = 'CREDIT_EXHAUSTED';
|
|
45
|
+
// @ts-expect-error — /me 에는 구독 없음이 오지 않는다(그 회사는 구독 필요 화면이 맡는다)
|
|
46
|
+
const notOnProfile: NonNullable<MyProfile['aiBlocked']> = 'NOT_SUBSCRIBED';
|
|
47
|
+
|
|
48
|
+
export { reasons, unknownReason, banner, profileReason, notOnProfile };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 웹페이지 읽기 진행 단계(8.9.0) — `WEB_READING` 이 phase 유니온에 있다는 것을 tsc 로 고정한다.
|
|
3
|
+
*
|
|
4
|
+
* <p>phase 를 직접 분기하는 화면이 이 값을 쓸 수 있어야 한다. 기본 라벨은 «페이지 읽는 중…» 이다.</p>
|
|
5
|
+
*/
|
|
6
|
+
import type { AssistantProgressEvent, AssistantRoomProgressEvent, AssistantProgressStreamSnapshot } from '../index';
|
|
7
|
+
|
|
8
|
+
export function isWebReading(event: AssistantProgressEvent): boolean {
|
|
9
|
+
return event.active && event.phase === 'WEB_READING';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function roomIsWebReading(event: AssistantRoomProgressEvent): boolean {
|
|
13
|
+
return event.phase === 'WEB_READING';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function snapshotIsWebReading(snapshot: AssistantProgressStreamSnapshot): boolean {
|
|
17
|
+
return snapshot.phase === 'WEB_READING';
|
|
18
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export const ErrorTypes: {
|
|
|
26
26
|
readonly CONNECTION_FAILED: 'CONNECTION_FAILED';
|
|
27
27
|
readonly CONNECTION_LOST: 'CONNECTION_LOST';
|
|
28
28
|
readonly CONNECTION_TIMEOUT: 'CONNECTION_TIMEOUT';
|
|
29
|
+
/** 회사 구독 없음(결제 전·해지) — 웹소켓이 멈췄다. 결제 확인 뒤 `connect()` 를 다시 부른다. */
|
|
30
|
+
readonly SUBSCRIPTION_REQUIRED: 'SUBSCRIPTION_REQUIRED';
|
|
29
31
|
readonly JWT_INVALID: 'JWT_INVALID';
|
|
30
32
|
readonly JWT_EXPIRED: 'JWT_EXPIRED';
|
|
31
33
|
readonly JWT_PARSE_FAILED: 'JWT_PARSE_FAILED';
|
|
@@ -532,6 +534,7 @@ export const RoomListEventType: {
|
|
|
532
534
|
readonly ROOM_DELETED: 'ROOM_DELETED';
|
|
533
535
|
readonly MESSAGE_RETENTION_CLEANUP: 'MESSAGE_RETENTION_CLEANUP';
|
|
534
536
|
readonly ROOM_UNREAD_UPDATED: 'ROOM_UNREAD_UPDATED';
|
|
537
|
+
readonly AI_UNAVAILABLE: 'AI_UNAVAILABLE';
|
|
535
538
|
};
|
|
536
539
|
export type RoomListEventTypeValue = typeof RoomListEventType[keyof typeof RoomListEventType];
|
|
537
540
|
|
|
@@ -1171,6 +1174,14 @@ export interface MyProfile {
|
|
|
1171
1174
|
mustChangePassword: boolean;
|
|
1172
1175
|
accessToken?: string;
|
|
1173
1176
|
expiresIn?: number;
|
|
1177
|
+
/**
|
|
1178
|
+
* 회사 AI 가 지금 막혔는가 — 채팅방 상단 띠용. 막혔으면 사유, 열려 있으면 키가 없다.
|
|
1179
|
+
*
|
|
1180
|
+
* PM 입구와 같은 판정이라 값이 있는 동안 PM 은 불러도 답하지 않는다. 최대 30초 늦게 바뀔 수 있고, 서버가 상태 변화를 따로
|
|
1181
|
+
* 알려 주지 않으므로 앱 시작·방 입장·탭 복귀·{@link AiUnavailableEvent} 수신 때 다시 읽는다. 게스트에게는 없다.
|
|
1182
|
+
* 구독 없음(`NOT_SUBSCRIBED`)은 여기 오지 않는다 — 그 회사는 구독 필요 화면이 맡는다.
|
|
1183
|
+
*/
|
|
1184
|
+
aiBlocked?: Exclude<AiBlockedReason, 'NOT_SUBSCRIBED'>;
|
|
1174
1185
|
}
|
|
1175
1186
|
|
|
1176
1187
|
/** 채팅방 참가자. */
|
|
@@ -1354,9 +1365,36 @@ export interface RoomListUpdateEvent {
|
|
|
1354
1365
|
* {@code ROOM_UNREAD_UPDATED} 에 실려, 순서 역전을 {@link RoomUnreadReconciler} 가 판별한다.
|
|
1355
1366
|
*/
|
|
1356
1367
|
unreadVersion?: number;
|
|
1368
|
+
/** {@code AI_UNAVAILABLE} 전용 — 회사 AI 가 막힌 사유. 다른 eventType 에서는 undefined. 전용 이벤트 {@link AiUnavailableEvent} 로도 받을 수 있다. */
|
|
1369
|
+
aiBlocked?: AiBlockedReason;
|
|
1357
1370
|
[extra: string]: unknown;
|
|
1358
1371
|
}
|
|
1359
1372
|
|
|
1373
|
+
/**
|
|
1374
|
+
* 회사 AI 가 막힌 사유 — AI 버튼(요약·번역 등)이 받는 REST 402 오류 코드와 같다.
|
|
1375
|
+
* <ul>
|
|
1376
|
+
* <li>{@code CREDIT_EXHAUSTED} — 이번 결제 기간 AI 크레딧을 다 썼다(다음 갱신·충전 때 풀림).</li>
|
|
1377
|
+
* <li>{@code CREDIT_INSUFFICIENT} — 남은 크레딧을 진행 중인 AI 작업이 잡고 있다(작업이 끝나면 풀림, 드묾).</li>
|
|
1378
|
+
* <li>{@code BILLING_SUSPENDED} — 결제가 확인되지 않았다.</li>
|
|
1379
|
+
* <li>{@code NOT_SUBSCRIBED} — 회사 구독이 없다. {@link AiUnavailableEvent} 에만 드물게 오고 {@link MyProfile.aiBlocked} 에는 오지 않는다.</li>
|
|
1380
|
+
* </ul>
|
|
1381
|
+
*/
|
|
1382
|
+
export type AiBlockedReason = 'CREDIT_EXHAUSTED' | 'CREDIT_INSUFFICIENT' | 'BILLING_SUSPENDED' | 'NOT_SUBSCRIBED';
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* `aiUnavailable` 이벤트 payload — PM 을 직접(@PM) 불렀는데 회사 AI 가 막혀 답하지 않았다. <b>부른 사람에게만</b> 오고, 방에는
|
|
1386
|
+
* 아무것도 남지 않는다. 잠깐 알리고 사라지게(토스트) 쓴다 — 리스트·미독은 바꾸지 않는다. 막힌 상태를 계속 보여 줄 띠는
|
|
1387
|
+
* {@link MyProfile.aiBlocked} 로 그린다.
|
|
1388
|
+
*/
|
|
1389
|
+
export interface AiUnavailableEvent {
|
|
1390
|
+
roomId: string;
|
|
1391
|
+
/** 부른 메시지. */
|
|
1392
|
+
messageId?: string;
|
|
1393
|
+
aiBlocked: AiBlockedReason;
|
|
1394
|
+
/** 서버 발행 시각(epoch millis). */
|
|
1395
|
+
timestamp?: number;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1360
1398
|
/**
|
|
1361
1399
|
* `roomListUnreadUpdated` 이벤트 payload — 내가 어느 기기에서 읽든 같은 사용자의 다른 세션에 전달된다.
|
|
1362
1400
|
*
|
|
@@ -1507,7 +1545,7 @@ export interface AssistantProgressEvent {
|
|
|
1507
1545
|
/** 진행 주체 페르소나 ID. */
|
|
1508
1546
|
personaId: string | null;
|
|
1509
1547
|
/** 진행 단계 — `active:true` 일 때. 종료 시 null. */
|
|
1510
|
-
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WRITING' | null;
|
|
1548
|
+
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WEB_READING' | 'WRITING' | null;
|
|
1511
1549
|
/** DOCUMENT_ANALYSIS의 실측 진행률. 같은 phase heartbeat 사이와 재입장 후에도 보존된다. */
|
|
1512
1550
|
progress?: DocumentAnalysisProgress | null;
|
|
1513
1551
|
/** phase 의 기본 표시 라벨(예: "생각 중…"). SDK 매핑 — 커스텀하려면 `phase` 사용. 종료 시 null. */
|
|
@@ -1578,7 +1616,7 @@ export interface AssistantRoomProgressEvent {
|
|
|
1578
1616
|
/** 표시 기준이 된 stream (가장 최근 갱신). 없으면 null. */
|
|
1579
1617
|
streamId: string | null;
|
|
1580
1618
|
personaId: string | null;
|
|
1581
|
-
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WRITING' | null;
|
|
1619
|
+
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WEB_READING' | 'WRITING' | null;
|
|
1582
1620
|
progress?: DocumentAnalysisProgress | null;
|
|
1583
1621
|
label: string | null;
|
|
1584
1622
|
/** 표시 기준 stream 의 누적 본문. `textAvailable:false` 거나 진행 중이 아니면 null. */
|
|
@@ -1591,7 +1629,7 @@ export interface AssistantRoomProgressEvent {
|
|
|
1591
1629
|
export interface AssistantProgressStreamSnapshot {
|
|
1592
1630
|
streamId: string;
|
|
1593
1631
|
personaId: string | null;
|
|
1594
|
-
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WRITING' | null;
|
|
1632
|
+
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WEB_READING' | 'WRITING' | null;
|
|
1595
1633
|
progress?: DocumentAnalysisProgress | null;
|
|
1596
1634
|
label: string | null;
|
|
1597
1635
|
text: string | null;
|
|
@@ -1997,8 +2035,8 @@ export interface RetentionCleanupPayload {
|
|
|
1997
2035
|
/**
|
|
1998
2036
|
* TalkFlowClient 이벤트 맵 — 런타임 {@code TalkFlowClient.js} 의 {@code this.emit(...)} 호출 기반.
|
|
1999
2037
|
*
|
|
2000
|
-
* <p><b>Connection 관련</b>: stateChange, connected, disconnected, reconnecting, reconnected, resumed, connectionError, tokenSet, loggedOut</p>
|
|
2001
|
-
* <p><b>Chat 관련 (ChatClient 이벤트를 이 이름으로 re-emit)</b>: chatMessage, newChatMessage, messageUpdated, messageDeleted, reactionChanged, linkPreviewAttached, messageRead, typing, assistantProgress, memberJoined, memberLeft, roomSubscribed, roomUnsubscribed, roomListSubscribed, roomListUnsubscribed, roomListUpdate, roomListMessage, roomListCreated, roomListJoined, roomListLeft, roomListSelfLeft, roomListRoomUpdated, retentionCleanup</p>
|
|
2038
|
+
* <p><b>Connection 관련</b>: stateChange, connected, disconnected, reconnecting, reconnected, resumed, subscriptionRequired, connectionError, tokenSet, loggedOut</p>
|
|
2039
|
+
* <p><b>Chat 관련 (ChatClient 이벤트를 이 이름으로 re-emit)</b>: chatMessage, newChatMessage, messageUpdated, messageDeleted, reactionChanged, linkPreviewAttached, messageRead, typing, assistantProgress, memberJoined, memberLeft, roomSubscribed, roomUnsubscribed, roomListSubscribed, roomListUnsubscribed, roomListUpdate, roomListMessage, roomListCreated, roomListJoined, roomListLeft, roomListSelfLeft, roomListRoomUpdated, aiUnavailable, retentionCleanup</p>
|
|
2002
2040
|
* <p><b>Push 관련</b>: pushEnabled, pushFailed, pushNotification</p>
|
|
2003
2041
|
* <p><b>WebRTC 관련</b>: localStreamStarted, localStreamStopped, remoteTrack, screenShareStarted, screenShareEnded, deviceChange, mediaStateChanged, callStarted, callEnded, callRequested, callAccepted, callRejected, callCancelled, callInvitation, callBusy, incomingCall, incomingCallWhileBusy, userJoined, userLeft, participantLeft, participantMediaState, peerConnected, peerDisconnected, peerClosed, webrtcError</p>
|
|
2004
2042
|
*/
|
|
@@ -2022,6 +2060,15 @@ export interface TalkFlowClientEvents {
|
|
|
2022
2060
|
* 발행된다(두 이벤트가 겹치지 않아 재동기화가 중복 실행되지 않는다).</p>
|
|
2023
2061
|
*/
|
|
2024
2062
|
resumed: { reason: 'visibilitychange' | 'pageshow' | 'online' };
|
|
2063
|
+
/**
|
|
2064
|
+
* 회사 구독 없음 — 서버가 웹소켓 연결·구독·전송을 `NOT_SUBSCRIBED` 로 거부했다(결제 전·해지 회사). SDK 는 재연결하지 않고
|
|
2065
|
+
* 토큰도 거부된 것으로 기록하지 않는다(로그인·토큰 갱신은 그대로). 대기에 들어갈 때 한 번 발행되며 `connectionError` 는 내지 않는다.
|
|
2066
|
+
*
|
|
2067
|
+
* <p>앱: 파트너면 결제 화면, 그 밖은 «회사 구독이 필요합니다. 관리자에게 문의하세요.» 안내. 결제가 확인되면
|
|
2068
|
+
* (`subscription === 'ACTIVE'`) 같은 토큰으로 `connect()` 를 다시 부른다. 구독 확인 일시 실패(`SUBSCRIPTION_CHECK_UNAVAILABLE`)는
|
|
2069
|
+
* 이 이벤트가 아니다 — 평소처럼 재연결한다.</p>
|
|
2070
|
+
*/
|
|
2071
|
+
subscriptionRequired: { code: 'NOT_SUBSCRIBED'; message: string };
|
|
2025
2072
|
connectionError: unknown;
|
|
2026
2073
|
tokenSet: { userId: string };
|
|
2027
2074
|
loggedOut: void;
|
|
@@ -2077,6 +2124,8 @@ export interface TalkFlowClientEvents {
|
|
|
2077
2124
|
roomListRoomUpdated: RoomListUpdateEvent;
|
|
2078
2125
|
/** 내가 어느 기기에서 읽든 방별 미독 절대값(+version)을 통지 — 크로스-디바이스 badge 동기화. */
|
|
2079
2126
|
roomListUnreadUpdated: RoomUnreadUpdatedEvent;
|
|
2127
|
+
/** @PM 을 불렀는데 회사 AI 가 막혀 PM 이 답하지 않음 (AI_UNAVAILABLE) — 부른 사람에게만 온다. 잠깐 알리고 사라지게. */
|
|
2128
|
+
aiUnavailable: AiUnavailableEvent;
|
|
2080
2129
|
mailSubscribed: Record<string, unknown>;
|
|
2081
2130
|
mailUnsubscribed: Record<string, unknown>;
|
|
2082
2131
|
mailChanged: MailChangedEvent;
|
|
@@ -2193,6 +2242,8 @@ export interface ChatClientEvents {
|
|
|
2193
2242
|
roomListRoomUpdated: RoomListUpdateEvent;
|
|
2194
2243
|
/** 내가 어느 기기에서 읽든 방별 미독 절대값(+version)을 통지 — 크로스-디바이스 badge 동기화. */
|
|
2195
2244
|
roomListUnreadUpdated: RoomUnreadUpdatedEvent;
|
|
2245
|
+
/** @PM 을 불렀는데 회사 AI 가 막혀 PM 이 답하지 않음 (AI_UNAVAILABLE) — 부른 사람에게만 온다. 잠깐 알리고 사라지게. */
|
|
2246
|
+
aiUnavailable: AiUnavailableEvent;
|
|
2196
2247
|
mailSubscribed: Record<string, unknown>;
|
|
2197
2248
|
mailUnsubscribed: Record<string, unknown>;
|
|
2198
2249
|
mailChanged: MailChangedEvent;
|
|
@@ -2679,6 +2730,8 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
|
|
|
2679
2730
|
/**
|
|
2680
2731
|
* 연결 완료까지 기다립니다. 이전 연결의 자동 복구가 이어지는 동안 재시도 한도 소진으로 reject하지 않습니다.
|
|
2681
2732
|
* 복구 연결 시 resolve하며, 명시적 disconnect·인증 종료·최초 연결 실패 시 reject합니다.
|
|
2733
|
+
* 회사 구독이 없으면(서버 `NOT_SUBSCRIBED`) `code: 'NOT_SUBSCRIBED'`·`type: 'SUBSCRIPTION_REQUIRED'` 로 reject 하고
|
|
2734
|
+
* 재연결하지 않습니다 — {@link TalkFlowClientEvents.subscriptionRequired} 참고.
|
|
2682
2735
|
*/
|
|
2683
2736
|
connect(jwt?: string, options?: ConnectOptions): Promise<void>;
|
|
2684
2737
|
disconnect(): Promise<void>;
|
|
@@ -2686,6 +2739,11 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
|
|
|
2686
2739
|
setToken(token: string): Promise<void>;
|
|
2687
2740
|
updateToken(newToken: string): Promise<void>;
|
|
2688
2741
|
isConnected(): boolean;
|
|
2742
|
+
/**
|
|
2743
|
+
* 회사 구독 대기 중인가 — 서버가 웹소켓을 `NOT_SUBSCRIBED` 로 거부해 연결을 멈췄다(결제 전·해지 회사).
|
|
2744
|
+
* 결제가 확인되면(`GET /api/v1/company/billing` 의 `subscription === 'ACTIVE'`) 같은 토큰으로 `connect()` 를 다시 부른다.
|
|
2745
|
+
*/
|
|
2746
|
+
isSubscriptionRequired(): boolean;
|
|
2689
2747
|
getState(): ConnectionStateValue;
|
|
2690
2748
|
hasToken(): boolean;
|
|
2691
2749
|
|