connectbase-client 3.31.0 → 3.33.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/CHANGELOG.md +55 -0
- package/dist/connect-base.umd.js +5 -4
- package/dist/index.d.mts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +236 -9
- package/dist/index.mjs +236 -9
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -2951,6 +2951,21 @@ interface RealtimeConnectOptions {
|
|
|
2951
2951
|
timeout?: number;
|
|
2952
2952
|
/** 디버그 로깅 활성화 (기본: false) */
|
|
2953
2953
|
debug?: boolean;
|
|
2954
|
+
/**
|
|
2955
|
+
* WebSocket 핸드셰이크가 실패할 때의 fallback transport (기본: `'sse'`).
|
|
2956
|
+
*
|
|
2957
|
+
* 일부 네트워크(특정 가정용 ISP/기업 프록시/미들박스)는 HTTPS 는 통과시키면서
|
|
2958
|
+
* WebSocket upgrade 만 차단한다. 이 경우 `'sse'` 이면 `connect()` 가 reject 하지 않고
|
|
2959
|
+
* **degraded(SSE) 모드로 resolve** 하며, 이후 `stream()` 호출은 자동으로 HTTP SSE
|
|
2960
|
+
* (`/v1/public/ai/chat/stream`) 로 흘러 AI 스트리밍이 계속 동작한다 (앱 코드 변경 불필요).
|
|
2961
|
+
*
|
|
2962
|
+
* SSE 는 단방향(server→client)이므로 fallback 모드에서는 AI `stream()` 만 지원되고
|
|
2963
|
+
* presence/typing/subscribe/sendMessage 같은 양방향 기능은 명시적 에러를 던진다.
|
|
2964
|
+
* 현재 활성 transport 는 `cb.realtime.transport` 로 확인할 수 있다.
|
|
2965
|
+
*
|
|
2966
|
+
* `'none'` 이면 기존처럼 WS 실패 시 `connect()` 가 reject 한다.
|
|
2967
|
+
*/
|
|
2968
|
+
fallback?: 'sse' | 'none';
|
|
2954
2969
|
}
|
|
2955
2970
|
/** 이벤트 핸들러 타입 */
|
|
2956
2971
|
type MessageHandler<T = unknown> = (message: RealtimeMessage<T>) => void;
|
|
@@ -3171,10 +3186,26 @@ declare class RealtimeAPI {
|
|
|
3171
3186
|
private clientId;
|
|
3172
3187
|
private userId?;
|
|
3173
3188
|
private _appId;
|
|
3189
|
+
/**
|
|
3190
|
+
* 현재 활성 transport.
|
|
3191
|
+
* - `'ws'`: WebSocket 정상 연결
|
|
3192
|
+
* - `'sse'`: WS 핸드셰이크 실패로 HTTP SSE fallback 모드 (AI `stream()` 만 지원)
|
|
3193
|
+
* - `null`: 미연결
|
|
3194
|
+
*/
|
|
3195
|
+
private activeTransport;
|
|
3196
|
+
/** SSE fallback 모드 활성 여부. true 면 백그라운드 WS 재연결 머신을 정지한다. */
|
|
3197
|
+
private sseFallbackActive;
|
|
3198
|
+
/** SSE fallback 스트림의 AbortController (sessionId → controller) — stop()/disconnect() 에서 중단용 */
|
|
3199
|
+
private sseSessions;
|
|
3174
3200
|
/** 현재 연결 ID */
|
|
3175
3201
|
get connectionId(): string | null;
|
|
3176
3202
|
/** 현재 연결된 앱 ID */
|
|
3177
3203
|
get appId(): string | null;
|
|
3204
|
+
/**
|
|
3205
|
+
* 현재 활성 transport (`'ws'` | `'sse'` | `null`).
|
|
3206
|
+
* `'sse'` 이면 WS 가 차단되어 fallback 모드로 동작 중이며 AI `stream()` 만 가능하다.
|
|
3207
|
+
*/
|
|
3208
|
+
get transport(): 'ws' | 'sse' | null;
|
|
3178
3209
|
private options;
|
|
3179
3210
|
private retryCount;
|
|
3180
3211
|
private pendingRequests;
|
|
@@ -3195,6 +3226,11 @@ declare class RealtimeAPI {
|
|
|
3195
3226
|
* - userId: 사용자 식별자 (표시용)
|
|
3196
3227
|
*/
|
|
3197
3228
|
connect(options?: RealtimeConnectOptions): Promise<void>;
|
|
3229
|
+
/**
|
|
3230
|
+
* SSE fallback 모드로 전환한다. WS 는 사용 불가하지만 state 는 'connected' 로 두어
|
|
3231
|
+
* (degraded) AI stream() 이 SSE 로 동작하게 한다. 백그라운드 WS 재연결 머신은 정지한다.
|
|
3232
|
+
*/
|
|
3233
|
+
private activateSseFallback;
|
|
3198
3234
|
/**
|
|
3199
3235
|
* 연결 해제
|
|
3200
3236
|
*/
|
|
@@ -3251,6 +3287,17 @@ declare class RealtimeAPI {
|
|
|
3251
3287
|
* ```
|
|
3252
3288
|
*/
|
|
3253
3289
|
stream(messages: StreamMessage[], handlers: StreamHandlers, options?: StreamOptions): Promise<StreamSession>;
|
|
3290
|
+
/** WebSocket 으로 AI 스트리밍 (기본 경로) */
|
|
3291
|
+
private streamViaWS;
|
|
3292
|
+
/**
|
|
3293
|
+
* HTTP SSE (`/v1/public/ai/chat/stream`) 로 AI 스트리밍 — WS 가 차단된 네트워크용 fallback.
|
|
3294
|
+
*
|
|
3295
|
+
* SSE 는 단방향이라 WS 의 `StreamDoneData` 토큰 카운트(total/prompt)를 받지 못하므로
|
|
3296
|
+
* `totalTokens`/`promptTokens` 는 0 으로 채워진다(`fullText`/`duration` 은 정확). `mcpGroup`
|
|
3297
|
+
* (MCP 도구) 은 SSE fallback 에서 미지원 — 지정 시 무시되고 도구 없이 스트리밍한다.
|
|
3298
|
+
*/
|
|
3299
|
+
private streamViaSSE;
|
|
3300
|
+
private runSseStream;
|
|
3254
3301
|
/**
|
|
3255
3302
|
* 스트리밍 중지
|
|
3256
3303
|
*/
|
|
@@ -3438,6 +3485,11 @@ declare class RealtimeAPI {
|
|
|
3438
3485
|
* 재연결 성공 후 호출됩니다.
|
|
3439
3486
|
*/
|
|
3440
3487
|
private restoreSubscriptions;
|
|
3488
|
+
/**
|
|
3489
|
+
* 양방향(WS 전용) 기능을 SSE fallback 모드에서 호출하면 명확한 에러를 던진다.
|
|
3490
|
+
* SSE 는 단방향이라 presence/typing/subscribe/sendMessage 를 지원하지 못한다.
|
|
3491
|
+
*/
|
|
3492
|
+
private assertBidirectional;
|
|
3441
3493
|
private sendRequest;
|
|
3442
3494
|
private sendRaw;
|
|
3443
3495
|
private notifyStateChange;
|
|
@@ -7543,6 +7595,21 @@ interface AIChatRequest {
|
|
|
7543
7595
|
topK?: number;
|
|
7544
7596
|
agentic?: boolean;
|
|
7545
7597
|
toolGroupId?: string;
|
|
7598
|
+
/**
|
|
7599
|
+
* 0 보다 크면, 서버측 도구 그룹 실행(`toolGroupId` 사용 시)에서 각 도구 결과를 모델에
|
|
7600
|
+
* **재투입하기 전** N 자(rune 기준)로 잘라낸다(말미에 잘림 표시). SSE `onToolEvent` 의
|
|
7601
|
+
* `tool_end.result` 로 표시되는 값은 원문을 유지한다.
|
|
7602
|
+
*
|
|
7603
|
+
* 거대한 도구 결과(예: 복잡한 페이지의 `browser_snapshot`)가 누적 컨텍스트를 부풀려
|
|
7604
|
+
* 매 턴 prefill 을 무겁게 만들 때, 표시는 원문으로 두고 모델 재투입분만 압축해
|
|
7605
|
+
* 지연을 줄이는 용도. 미지정/0 이면 자르지 않는다.
|
|
7606
|
+
*/
|
|
7607
|
+
toolResultMaxChars?: number;
|
|
7608
|
+
/**
|
|
7609
|
+
* 요청에 포함한 MCP 도구(`tools`)를 호출할 외부 MCP 서버의 Public Key (cb_pk_* 형식).
|
|
7610
|
+
* MCP 도구를 직접 넘기지 않으면 불필요하다.
|
|
7611
|
+
*/
|
|
7612
|
+
mcpPublicKey?: string;
|
|
7546
7613
|
}
|
|
7547
7614
|
/**
|
|
7548
7615
|
* `chatStream` 콜백.
|
package/dist/index.d.ts
CHANGED
|
@@ -2951,6 +2951,21 @@ interface RealtimeConnectOptions {
|
|
|
2951
2951
|
timeout?: number;
|
|
2952
2952
|
/** 디버그 로깅 활성화 (기본: false) */
|
|
2953
2953
|
debug?: boolean;
|
|
2954
|
+
/**
|
|
2955
|
+
* WebSocket 핸드셰이크가 실패할 때의 fallback transport (기본: `'sse'`).
|
|
2956
|
+
*
|
|
2957
|
+
* 일부 네트워크(특정 가정용 ISP/기업 프록시/미들박스)는 HTTPS 는 통과시키면서
|
|
2958
|
+
* WebSocket upgrade 만 차단한다. 이 경우 `'sse'` 이면 `connect()` 가 reject 하지 않고
|
|
2959
|
+
* **degraded(SSE) 모드로 resolve** 하며, 이후 `stream()` 호출은 자동으로 HTTP SSE
|
|
2960
|
+
* (`/v1/public/ai/chat/stream`) 로 흘러 AI 스트리밍이 계속 동작한다 (앱 코드 변경 불필요).
|
|
2961
|
+
*
|
|
2962
|
+
* SSE 는 단방향(server→client)이므로 fallback 모드에서는 AI `stream()` 만 지원되고
|
|
2963
|
+
* presence/typing/subscribe/sendMessage 같은 양방향 기능은 명시적 에러를 던진다.
|
|
2964
|
+
* 현재 활성 transport 는 `cb.realtime.transport` 로 확인할 수 있다.
|
|
2965
|
+
*
|
|
2966
|
+
* `'none'` 이면 기존처럼 WS 실패 시 `connect()` 가 reject 한다.
|
|
2967
|
+
*/
|
|
2968
|
+
fallback?: 'sse' | 'none';
|
|
2954
2969
|
}
|
|
2955
2970
|
/** 이벤트 핸들러 타입 */
|
|
2956
2971
|
type MessageHandler<T = unknown> = (message: RealtimeMessage<T>) => void;
|
|
@@ -3171,10 +3186,26 @@ declare class RealtimeAPI {
|
|
|
3171
3186
|
private clientId;
|
|
3172
3187
|
private userId?;
|
|
3173
3188
|
private _appId;
|
|
3189
|
+
/**
|
|
3190
|
+
* 현재 활성 transport.
|
|
3191
|
+
* - `'ws'`: WebSocket 정상 연결
|
|
3192
|
+
* - `'sse'`: WS 핸드셰이크 실패로 HTTP SSE fallback 모드 (AI `stream()` 만 지원)
|
|
3193
|
+
* - `null`: 미연결
|
|
3194
|
+
*/
|
|
3195
|
+
private activeTransport;
|
|
3196
|
+
/** SSE fallback 모드 활성 여부. true 면 백그라운드 WS 재연결 머신을 정지한다. */
|
|
3197
|
+
private sseFallbackActive;
|
|
3198
|
+
/** SSE fallback 스트림의 AbortController (sessionId → controller) — stop()/disconnect() 에서 중단용 */
|
|
3199
|
+
private sseSessions;
|
|
3174
3200
|
/** 현재 연결 ID */
|
|
3175
3201
|
get connectionId(): string | null;
|
|
3176
3202
|
/** 현재 연결된 앱 ID */
|
|
3177
3203
|
get appId(): string | null;
|
|
3204
|
+
/**
|
|
3205
|
+
* 현재 활성 transport (`'ws'` | `'sse'` | `null`).
|
|
3206
|
+
* `'sse'` 이면 WS 가 차단되어 fallback 모드로 동작 중이며 AI `stream()` 만 가능하다.
|
|
3207
|
+
*/
|
|
3208
|
+
get transport(): 'ws' | 'sse' | null;
|
|
3178
3209
|
private options;
|
|
3179
3210
|
private retryCount;
|
|
3180
3211
|
private pendingRequests;
|
|
@@ -3195,6 +3226,11 @@ declare class RealtimeAPI {
|
|
|
3195
3226
|
* - userId: 사용자 식별자 (표시용)
|
|
3196
3227
|
*/
|
|
3197
3228
|
connect(options?: RealtimeConnectOptions): Promise<void>;
|
|
3229
|
+
/**
|
|
3230
|
+
* SSE fallback 모드로 전환한다. WS 는 사용 불가하지만 state 는 'connected' 로 두어
|
|
3231
|
+
* (degraded) AI stream() 이 SSE 로 동작하게 한다. 백그라운드 WS 재연결 머신은 정지한다.
|
|
3232
|
+
*/
|
|
3233
|
+
private activateSseFallback;
|
|
3198
3234
|
/**
|
|
3199
3235
|
* 연결 해제
|
|
3200
3236
|
*/
|
|
@@ -3251,6 +3287,17 @@ declare class RealtimeAPI {
|
|
|
3251
3287
|
* ```
|
|
3252
3288
|
*/
|
|
3253
3289
|
stream(messages: StreamMessage[], handlers: StreamHandlers, options?: StreamOptions): Promise<StreamSession>;
|
|
3290
|
+
/** WebSocket 으로 AI 스트리밍 (기본 경로) */
|
|
3291
|
+
private streamViaWS;
|
|
3292
|
+
/**
|
|
3293
|
+
* HTTP SSE (`/v1/public/ai/chat/stream`) 로 AI 스트리밍 — WS 가 차단된 네트워크용 fallback.
|
|
3294
|
+
*
|
|
3295
|
+
* SSE 는 단방향이라 WS 의 `StreamDoneData` 토큰 카운트(total/prompt)를 받지 못하므로
|
|
3296
|
+
* `totalTokens`/`promptTokens` 는 0 으로 채워진다(`fullText`/`duration` 은 정확). `mcpGroup`
|
|
3297
|
+
* (MCP 도구) 은 SSE fallback 에서 미지원 — 지정 시 무시되고 도구 없이 스트리밍한다.
|
|
3298
|
+
*/
|
|
3299
|
+
private streamViaSSE;
|
|
3300
|
+
private runSseStream;
|
|
3254
3301
|
/**
|
|
3255
3302
|
* 스트리밍 중지
|
|
3256
3303
|
*/
|
|
@@ -3438,6 +3485,11 @@ declare class RealtimeAPI {
|
|
|
3438
3485
|
* 재연결 성공 후 호출됩니다.
|
|
3439
3486
|
*/
|
|
3440
3487
|
private restoreSubscriptions;
|
|
3488
|
+
/**
|
|
3489
|
+
* 양방향(WS 전용) 기능을 SSE fallback 모드에서 호출하면 명확한 에러를 던진다.
|
|
3490
|
+
* SSE 는 단방향이라 presence/typing/subscribe/sendMessage 를 지원하지 못한다.
|
|
3491
|
+
*/
|
|
3492
|
+
private assertBidirectional;
|
|
3441
3493
|
private sendRequest;
|
|
3442
3494
|
private sendRaw;
|
|
3443
3495
|
private notifyStateChange;
|
|
@@ -7543,6 +7595,21 @@ interface AIChatRequest {
|
|
|
7543
7595
|
topK?: number;
|
|
7544
7596
|
agentic?: boolean;
|
|
7545
7597
|
toolGroupId?: string;
|
|
7598
|
+
/**
|
|
7599
|
+
* 0 보다 크면, 서버측 도구 그룹 실행(`toolGroupId` 사용 시)에서 각 도구 결과를 모델에
|
|
7600
|
+
* **재투입하기 전** N 자(rune 기준)로 잘라낸다(말미에 잘림 표시). SSE `onToolEvent` 의
|
|
7601
|
+
* `tool_end.result` 로 표시되는 값은 원문을 유지한다.
|
|
7602
|
+
*
|
|
7603
|
+
* 거대한 도구 결과(예: 복잡한 페이지의 `browser_snapshot`)가 누적 컨텍스트를 부풀려
|
|
7604
|
+
* 매 턴 prefill 을 무겁게 만들 때, 표시는 원문으로 두고 모델 재투입분만 압축해
|
|
7605
|
+
* 지연을 줄이는 용도. 미지정/0 이면 자르지 않는다.
|
|
7606
|
+
*/
|
|
7607
|
+
toolResultMaxChars?: number;
|
|
7608
|
+
/**
|
|
7609
|
+
* 요청에 포함한 MCP 도구(`tools`)를 호출할 외부 MCP 서버의 Public Key (cb_pk_* 형식).
|
|
7610
|
+
* MCP 도구를 직접 넘기지 않으면 불필요하다.
|
|
7611
|
+
*/
|
|
7612
|
+
mcpPublicKey?: string;
|
|
7546
7613
|
}
|
|
7547
7614
|
/**
|
|
7548
7615
|
* `chatStream` 콜백.
|
package/dist/index.js
CHANGED
|
@@ -153,6 +153,10 @@ function sanitizePathForBreadcrumb(rawUrl) {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// src/core/http.ts
|
|
156
|
+
function fetchCredentialsForPath(url) {
|
|
157
|
+
const path = url.split("?")[0];
|
|
158
|
+
return path.startsWith("/v1/public/") ? "omit" : "include";
|
|
159
|
+
}
|
|
156
160
|
var TOKEN_STORAGE_KEY = "cb_auth_tokens";
|
|
157
161
|
function decodeJwtPayload(token) {
|
|
158
162
|
try {
|
|
@@ -679,7 +683,7 @@ var HttpClient = class {
|
|
|
679
683
|
try {
|
|
680
684
|
const response = await fetch(`${this.config.baseUrl}${url}`, {
|
|
681
685
|
...init,
|
|
682
|
-
credentials:
|
|
686
|
+
credentials: fetchCredentialsForPath(url),
|
|
683
687
|
signal
|
|
684
688
|
});
|
|
685
689
|
return { response, ok: response.ok, status: response.status };
|
|
@@ -748,7 +752,7 @@ var HttpClient = class {
|
|
|
748
752
|
}
|
|
749
753
|
return fetch(`${this.config.baseUrl}${url}`, {
|
|
750
754
|
...init,
|
|
751
|
-
credentials:
|
|
755
|
+
credentials: fetchCredentialsForPath(url),
|
|
752
756
|
headers: mergedHeaders
|
|
753
757
|
});
|
|
754
758
|
}
|
|
@@ -2955,13 +2959,25 @@ var RealtimeAPI = class {
|
|
|
2955
2959
|
this.state = "disconnected";
|
|
2956
2960
|
this._connectionId = null;
|
|
2957
2961
|
this._appId = null;
|
|
2962
|
+
/**
|
|
2963
|
+
* 현재 활성 transport.
|
|
2964
|
+
* - `'ws'`: WebSocket 정상 연결
|
|
2965
|
+
* - `'sse'`: WS 핸드셰이크 실패로 HTTP SSE fallback 모드 (AI `stream()` 만 지원)
|
|
2966
|
+
* - `null`: 미연결
|
|
2967
|
+
*/
|
|
2968
|
+
this.activeTransport = null;
|
|
2969
|
+
/** SSE fallback 모드 활성 여부. true 면 백그라운드 WS 재연결 머신을 정지한다. */
|
|
2970
|
+
this.sseFallbackActive = false;
|
|
2971
|
+
/** SSE fallback 스트림의 AbortController (sessionId → controller) — stop()/disconnect() 에서 중단용 */
|
|
2972
|
+
this.sseSessions = /* @__PURE__ */ new Map();
|
|
2958
2973
|
this.options = {
|
|
2959
2974
|
maxRetries: 5,
|
|
2960
2975
|
retryInterval: 1e3,
|
|
2961
2976
|
userId: "",
|
|
2962
2977
|
accessToken: "",
|
|
2963
2978
|
timeout: 3e4,
|
|
2964
|
-
debug: false
|
|
2979
|
+
debug: false,
|
|
2980
|
+
fallback: "sse"
|
|
2965
2981
|
};
|
|
2966
2982
|
this.retryCount = 0;
|
|
2967
2983
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
@@ -2990,6 +3006,13 @@ var RealtimeAPI = class {
|
|
|
2990
3006
|
get appId() {
|
|
2991
3007
|
return this._appId;
|
|
2992
3008
|
}
|
|
3009
|
+
/**
|
|
3010
|
+
* 현재 활성 transport (`'ws'` | `'sse'` | `null`).
|
|
3011
|
+
* `'sse'` 이면 WS 가 차단되어 fallback 모드로 동작 중이며 AI `stream()` 만 가능하다.
|
|
3012
|
+
*/
|
|
3013
|
+
get transport() {
|
|
3014
|
+
return this.activeTransport;
|
|
3015
|
+
}
|
|
2993
3016
|
/**
|
|
2994
3017
|
* WebSocket 연결
|
|
2995
3018
|
* @param options 연결 옵션
|
|
@@ -3010,10 +3033,36 @@ var RealtimeAPI = class {
|
|
|
3010
3033
|
this.connectPromise = this.doConnect();
|
|
3011
3034
|
try {
|
|
3012
3035
|
await this.connectPromise;
|
|
3036
|
+
} catch (e) {
|
|
3037
|
+
const hasAuth = !!(this.options.accessToken || this.http.getPublicKey());
|
|
3038
|
+
if (this.options.fallback !== "none" && hasAuth) {
|
|
3039
|
+
this.activateSseFallback(e);
|
|
3040
|
+
} else {
|
|
3041
|
+
throw e;
|
|
3042
|
+
}
|
|
3013
3043
|
} finally {
|
|
3014
3044
|
this.connectPromise = null;
|
|
3015
3045
|
}
|
|
3016
3046
|
}
|
|
3047
|
+
/**
|
|
3048
|
+
* SSE fallback 모드로 전환한다. WS 는 사용 불가하지만 state 는 'connected' 로 두어
|
|
3049
|
+
* (degraded) AI stream() 이 SSE 로 동작하게 한다. 백그라운드 WS 재연결 머신은 정지한다.
|
|
3050
|
+
*/
|
|
3051
|
+
activateSseFallback(cause) {
|
|
3052
|
+
if (this.ws) {
|
|
3053
|
+
try {
|
|
3054
|
+
this.ws.close();
|
|
3055
|
+
} catch {
|
|
3056
|
+
}
|
|
3057
|
+
this.ws = null;
|
|
3058
|
+
}
|
|
3059
|
+
this.sseFallbackActive = true;
|
|
3060
|
+
this.activeTransport = "sse";
|
|
3061
|
+
this.state = "connected";
|
|
3062
|
+
this.retryCount = 0;
|
|
3063
|
+
this.log(`WS unavailable (${cause instanceof Error ? cause.message : String(cause)}); using SSE fallback transport for AI streaming`);
|
|
3064
|
+
this.notifyStateChange();
|
|
3065
|
+
}
|
|
3017
3066
|
/**
|
|
3018
3067
|
* 연결 해제
|
|
3019
3068
|
*/
|
|
@@ -3036,6 +3085,15 @@ var RealtimeAPI = class {
|
|
|
3036
3085
|
}
|
|
3037
3086
|
});
|
|
3038
3087
|
this.streamSessions.clear();
|
|
3088
|
+
this.sseSessions.forEach((controller) => {
|
|
3089
|
+
try {
|
|
3090
|
+
controller.abort();
|
|
3091
|
+
} catch {
|
|
3092
|
+
}
|
|
3093
|
+
});
|
|
3094
|
+
this.sseSessions.clear();
|
|
3095
|
+
this.sseFallbackActive = false;
|
|
3096
|
+
this.activeTransport = null;
|
|
3039
3097
|
this.presenceHandlers = [];
|
|
3040
3098
|
this.presenceSubscriptions.clear();
|
|
3041
3099
|
this.typingHandlers.clear();
|
|
@@ -3049,6 +3107,7 @@ var RealtimeAPI = class {
|
|
|
3049
3107
|
* 카테고리 구독
|
|
3050
3108
|
*/
|
|
3051
3109
|
async subscribe(category, options = {}) {
|
|
3110
|
+
this.assertBidirectional("subscribe");
|
|
3052
3111
|
if (this.state !== "connected") {
|
|
3053
3112
|
throw new Error("Not connected. Call connect() first.");
|
|
3054
3113
|
}
|
|
@@ -3109,6 +3168,7 @@ var RealtimeAPI = class {
|
|
|
3109
3168
|
* @param options 전송 옵션 (includeSelf: 발신자도 메시지 수신 여부, 기본값 true)
|
|
3110
3169
|
*/
|
|
3111
3170
|
async sendMessage(category, data, options = {}) {
|
|
3171
|
+
this.assertBidirectional("sendMessage");
|
|
3112
3172
|
if (this.state !== "connected") {
|
|
3113
3173
|
throw new Error("Not connected");
|
|
3114
3174
|
}
|
|
@@ -3126,6 +3186,7 @@ var RealtimeAPI = class {
|
|
|
3126
3186
|
* 히스토리 조회
|
|
3127
3187
|
*/
|
|
3128
3188
|
async getHistory(category, limit) {
|
|
3189
|
+
this.assertBidirectional("getHistory");
|
|
3129
3190
|
if (this.state !== "connected") {
|
|
3130
3191
|
throw new Error("Not connected");
|
|
3131
3192
|
}
|
|
@@ -3181,9 +3242,22 @@ var RealtimeAPI = class {
|
|
|
3181
3242
|
* ```
|
|
3182
3243
|
*/
|
|
3183
3244
|
async stream(messages, handlers, options = {}) {
|
|
3184
|
-
|
|
3185
|
-
|
|
3245
|
+
const canWs = this.state === "connected" && this.activeTransport === "ws" && this.ws?.readyState === WebSocket.OPEN;
|
|
3246
|
+
if (canWs) {
|
|
3247
|
+
try {
|
|
3248
|
+
return this.streamViaWS(messages, handlers, options);
|
|
3249
|
+
} catch (e) {
|
|
3250
|
+
if (this.options.fallback === "none") throw e;
|
|
3251
|
+
this.log(`WS stream send failed (${e instanceof Error ? e.message : String(e)}); falling back to SSE`);
|
|
3252
|
+
}
|
|
3186
3253
|
}
|
|
3254
|
+
if (this.options.fallback !== "none") {
|
|
3255
|
+
return this.streamViaSSE(messages, handlers, options);
|
|
3256
|
+
}
|
|
3257
|
+
throw new Error("Not connected. Call connect() first.");
|
|
3258
|
+
}
|
|
3259
|
+
/** WebSocket 으로 AI 스트리밍 (기본 경로) */
|
|
3260
|
+
streamViaWS(messages, handlers, options) {
|
|
3187
3261
|
const requestId = this.generateRequestId();
|
|
3188
3262
|
const sessionId = options.sessionId || this.generateRequestId();
|
|
3189
3263
|
this.streamSessions.set(sessionId, {
|
|
@@ -3218,11 +3292,130 @@ var RealtimeAPI = class {
|
|
|
3218
3292
|
}
|
|
3219
3293
|
};
|
|
3220
3294
|
}
|
|
3295
|
+
/**
|
|
3296
|
+
* HTTP SSE (`/v1/public/ai/chat/stream`) 로 AI 스트리밍 — WS 가 차단된 네트워크용 fallback.
|
|
3297
|
+
*
|
|
3298
|
+
* SSE 는 단방향이라 WS 의 `StreamDoneData` 토큰 카운트(total/prompt)를 받지 못하므로
|
|
3299
|
+
* `totalTokens`/`promptTokens` 는 0 으로 채워진다(`fullText`/`duration` 은 정확). `mcpGroup`
|
|
3300
|
+
* (MCP 도구) 은 SSE fallback 에서 미지원 — 지정 시 무시되고 도구 없이 스트리밍한다.
|
|
3301
|
+
*/
|
|
3302
|
+
streamViaSSE(messages, handlers, options) {
|
|
3303
|
+
const sessionId = options.sessionId || this.generateRequestId();
|
|
3304
|
+
const controller = new AbortController();
|
|
3305
|
+
this.sseSessions.set(sessionId, controller);
|
|
3306
|
+
if (options.mcpGroup) {
|
|
3307
|
+
this.log("mcpGroup is not supported in SSE fallback mode; streaming without MCP tools");
|
|
3308
|
+
}
|
|
3309
|
+
this.log(`Streaming via SSE fallback (session ${sessionId})`);
|
|
3310
|
+
void this.runSseStream(sessionId, messages, handlers, options, controller.signal);
|
|
3311
|
+
return {
|
|
3312
|
+
sessionId,
|
|
3313
|
+
stop: async () => {
|
|
3314
|
+
await this.stopStream(sessionId);
|
|
3315
|
+
}
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
async runSseStream(sessionId, messages, handlers, options, signal) {
|
|
3319
|
+
const startedAt = Date.now();
|
|
3320
|
+
let tokenIndex = 0;
|
|
3321
|
+
let fullText = "";
|
|
3322
|
+
let reader;
|
|
3323
|
+
const wireMessages = options.system ? [{ role: "system", content: options.system }, ...messages] : messages;
|
|
3324
|
+
const body = {
|
|
3325
|
+
messages: wireMessages,
|
|
3326
|
+
provider: options.provider,
|
|
3327
|
+
model: options.model,
|
|
3328
|
+
temperature: options.temperature,
|
|
3329
|
+
maxTokens: options.maxTokens
|
|
3330
|
+
};
|
|
3331
|
+
try {
|
|
3332
|
+
const response = await this.http.fetchRaw("/v1/public/ai/chat/stream", {
|
|
3333
|
+
method: "POST",
|
|
3334
|
+
headers: { "Content-Type": "application/json" },
|
|
3335
|
+
body: JSON.stringify(body),
|
|
3336
|
+
signal
|
|
3337
|
+
});
|
|
3338
|
+
if (!response.ok) {
|
|
3339
|
+
const errData = await response.json().catch(() => ({ error: "Stream request failed" }));
|
|
3340
|
+
handlers.onError?.(new Error(errData.error || errData.message || "Stream request failed"));
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
reader = response.body?.getReader();
|
|
3344
|
+
if (!reader) {
|
|
3345
|
+
handlers.onError?.(new Error("ReadableStream not supported"));
|
|
3346
|
+
return;
|
|
3347
|
+
}
|
|
3348
|
+
const decoder = new TextDecoder();
|
|
3349
|
+
let buffer = "";
|
|
3350
|
+
while (true) {
|
|
3351
|
+
const { done, value } = await reader.read();
|
|
3352
|
+
if (done) break;
|
|
3353
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3354
|
+
const lines = buffer.split("\n");
|
|
3355
|
+
buffer = lines.pop() || "";
|
|
3356
|
+
for (const line of lines) {
|
|
3357
|
+
if (!line.startsWith("data: ")) continue;
|
|
3358
|
+
const data = line.slice(6).trim();
|
|
3359
|
+
if (data === "[DONE]") {
|
|
3360
|
+
handlers.onDone?.({ sessionId, fullText, totalTokens: 0, promptTokens: 0, duration: Date.now() - startedAt });
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
try {
|
|
3364
|
+
const ev = JSON.parse(data);
|
|
3365
|
+
if (ev.error) {
|
|
3366
|
+
handlers.onError?.(new Error(ev.message || ev.error || "stream error"));
|
|
3367
|
+
return;
|
|
3368
|
+
}
|
|
3369
|
+
if (ev.type === "tool_start") {
|
|
3370
|
+
handlers.onToolCall?.(ev.name || "", ev.arguments || {}, tokenIndex);
|
|
3371
|
+
continue;
|
|
3372
|
+
}
|
|
3373
|
+
if (ev.type === "tool_end") {
|
|
3374
|
+
handlers.onToolResult?.(ev.name || "", ev.success !== false, ev.durationMs || 0, tokenIndex);
|
|
3375
|
+
continue;
|
|
3376
|
+
}
|
|
3377
|
+
if (ev.type === "sources" || ev.type === "searching" || ev.type === "heartbeat") {
|
|
3378
|
+
continue;
|
|
3379
|
+
}
|
|
3380
|
+
if (ev.content) {
|
|
3381
|
+
fullText += ev.content;
|
|
3382
|
+
handlers.onToken?.(ev.content, tokenIndex++);
|
|
3383
|
+
}
|
|
3384
|
+
if (ev.done) {
|
|
3385
|
+
handlers.onDone?.({ sessionId, fullText, totalTokens: 0, promptTokens: 0, duration: Date.now() - startedAt });
|
|
3386
|
+
return;
|
|
3387
|
+
}
|
|
3388
|
+
} catch {
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
} catch (err) {
|
|
3393
|
+
const aborted = signal.aborted || err instanceof DOMException && err.name === "AbortError" || typeof err === "object" && err !== null && err.name === "AbortError";
|
|
3394
|
+
if (!aborted) {
|
|
3395
|
+
handlers.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
3396
|
+
}
|
|
3397
|
+
} finally {
|
|
3398
|
+
this.sseSessions.delete(sessionId);
|
|
3399
|
+
if (reader) {
|
|
3400
|
+
try {
|
|
3401
|
+
await reader.cancel();
|
|
3402
|
+
} catch {
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3221
3407
|
/**
|
|
3222
3408
|
* 스트리밍 중지
|
|
3223
3409
|
*/
|
|
3224
3410
|
async stopStream(sessionId) {
|
|
3225
|
-
|
|
3411
|
+
const sse = this.sseSessions.get(sessionId);
|
|
3412
|
+
if (sse) {
|
|
3413
|
+
sse.abort();
|
|
3414
|
+
this.sseSessions.delete(sessionId);
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
if (this.state !== "connected" || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
3418
|
+
this.streamSessions.delete(sessionId);
|
|
3226
3419
|
return;
|
|
3227
3420
|
}
|
|
3228
3421
|
const requestId = this.generateRequestId();
|
|
@@ -3281,6 +3474,7 @@ var RealtimeAPI = class {
|
|
|
3281
3474
|
* ```
|
|
3282
3475
|
*/
|
|
3283
3476
|
async setPresence(status, options = {}) {
|
|
3477
|
+
this.assertBidirectional("setPresence");
|
|
3284
3478
|
if (this.state !== "connected") {
|
|
3285
3479
|
throw new Error("Not connected");
|
|
3286
3480
|
}
|
|
@@ -3308,6 +3502,7 @@ var RealtimeAPI = class {
|
|
|
3308
3502
|
* ```
|
|
3309
3503
|
*/
|
|
3310
3504
|
async setPresenceOnDisconnect(status, metadata) {
|
|
3505
|
+
this.assertBidirectional("setPresenceOnDisconnect");
|
|
3311
3506
|
if (this.state !== "connected") {
|
|
3312
3507
|
throw new Error("Not connected");
|
|
3313
3508
|
}
|
|
@@ -3331,6 +3526,7 @@ var RealtimeAPI = class {
|
|
|
3331
3526
|
* ```
|
|
3332
3527
|
*/
|
|
3333
3528
|
async getPresence(userId) {
|
|
3529
|
+
this.assertBidirectional("getPresence");
|
|
3334
3530
|
if (this.state !== "connected") {
|
|
3335
3531
|
throw new Error("Not connected");
|
|
3336
3532
|
}
|
|
@@ -3361,6 +3557,7 @@ var RealtimeAPI = class {
|
|
|
3361
3557
|
* ```
|
|
3362
3558
|
*/
|
|
3363
3559
|
async getPresenceMany(userIds) {
|
|
3560
|
+
this.assertBidirectional("getPresenceMany");
|
|
3364
3561
|
if (this.state !== "connected") {
|
|
3365
3562
|
throw new Error("Not connected");
|
|
3366
3563
|
}
|
|
@@ -3400,6 +3597,7 @@ var RealtimeAPI = class {
|
|
|
3400
3597
|
* ```
|
|
3401
3598
|
*/
|
|
3402
3599
|
async subscribePresence(userId, handler) {
|
|
3600
|
+
this.assertBidirectional("subscribePresence");
|
|
3403
3601
|
if (this.state !== "connected") {
|
|
3404
3602
|
throw new Error("Not connected");
|
|
3405
3603
|
}
|
|
@@ -3469,6 +3667,7 @@ var RealtimeAPI = class {
|
|
|
3469
3667
|
* ```
|
|
3470
3668
|
*/
|
|
3471
3669
|
async startTyping(roomId) {
|
|
3670
|
+
this.assertBidirectional("startTyping");
|
|
3472
3671
|
if (this.state !== "connected") {
|
|
3473
3672
|
throw new Error("Not connected");
|
|
3474
3673
|
}
|
|
@@ -3493,6 +3692,7 @@ var RealtimeAPI = class {
|
|
|
3493
3692
|
* ```
|
|
3494
3693
|
*/
|
|
3495
3694
|
async stopTyping(roomId) {
|
|
3695
|
+
this.assertBidirectional("stopTyping");
|
|
3496
3696
|
if (this.state !== "connected") {
|
|
3497
3697
|
throw new Error("Not connected");
|
|
3498
3698
|
}
|
|
@@ -3522,6 +3722,7 @@ var RealtimeAPI = class {
|
|
|
3522
3722
|
* ```
|
|
3523
3723
|
*/
|
|
3524
3724
|
async onTypingChange(roomId, handler) {
|
|
3725
|
+
this.assertBidirectional("onTypingChange");
|
|
3525
3726
|
if (this.state !== "connected") {
|
|
3526
3727
|
throw new Error("Not connected");
|
|
3527
3728
|
}
|
|
@@ -3569,6 +3770,7 @@ var RealtimeAPI = class {
|
|
|
3569
3770
|
* ```
|
|
3570
3771
|
*/
|
|
3571
3772
|
async markRead(category, messageIds) {
|
|
3773
|
+
this.assertBidirectional("markRead");
|
|
3572
3774
|
if (this.state !== "connected") {
|
|
3573
3775
|
throw new Error("Not connected");
|
|
3574
3776
|
}
|
|
@@ -3717,6 +3919,8 @@ var RealtimeAPI = class {
|
|
|
3717
3919
|
this._connectionId = data.connection_id;
|
|
3718
3920
|
this._appId = data.app_id;
|
|
3719
3921
|
this.state = "connected";
|
|
3922
|
+
this.activeTransport = "ws";
|
|
3923
|
+
this.sseFallbackActive = false;
|
|
3720
3924
|
this.retryCount = 0;
|
|
3721
3925
|
this.notifyStateChange();
|
|
3722
3926
|
if (connectResolve) connectResolve();
|
|
@@ -3879,6 +4083,10 @@ var RealtimeAPI = class {
|
|
|
3879
4083
|
}
|
|
3880
4084
|
}
|
|
3881
4085
|
handleDisconnect() {
|
|
4086
|
+
if (this.sseFallbackActive) {
|
|
4087
|
+
this.ws = null;
|
|
4088
|
+
return;
|
|
4089
|
+
}
|
|
3882
4090
|
this.ws = null;
|
|
3883
4091
|
this._connectionId = null;
|
|
3884
4092
|
this.streamSessions.forEach((session) => {
|
|
@@ -3911,6 +4119,7 @@ var RealtimeAPI = class {
|
|
|
3911
4119
|
3e4
|
|
3912
4120
|
);
|
|
3913
4121
|
setTimeout(async () => {
|
|
4122
|
+
if (this.sseFallbackActive) return;
|
|
3914
4123
|
try {
|
|
3915
4124
|
await this.doConnect();
|
|
3916
4125
|
this.log("Reconnected successfully, restoring subscriptions...");
|
|
@@ -3921,12 +4130,19 @@ var RealtimeAPI = class {
|
|
|
3921
4130
|
);
|
|
3922
4131
|
} catch (e) {
|
|
3923
4132
|
this.logError("Reconnect failed", e);
|
|
4133
|
+
if (this.retryCount >= this.options.maxRetries && this.options.fallback !== "none" && !this.sseFallbackActive) {
|
|
4134
|
+
this.activateSseFallback(e);
|
|
4135
|
+
}
|
|
3924
4136
|
}
|
|
3925
4137
|
}, delay);
|
|
3926
4138
|
} else {
|
|
3927
|
-
this.
|
|
3928
|
-
|
|
3929
|
-
|
|
4139
|
+
if (this.options.fallback !== "none") {
|
|
4140
|
+
this.activateSseFallback(new Error("Connection lost. Max WS retries exceeded."));
|
|
4141
|
+
} else {
|
|
4142
|
+
this.state = "disconnected";
|
|
4143
|
+
this.notifyStateChange();
|
|
4144
|
+
this.notifyError(new Error("Connection lost. Max retries exceeded."));
|
|
4145
|
+
}
|
|
3930
4146
|
}
|
|
3931
4147
|
}
|
|
3932
4148
|
/**
|
|
@@ -3989,6 +4205,17 @@ var RealtimeAPI = class {
|
|
|
3989
4205
|
}
|
|
3990
4206
|
}
|
|
3991
4207
|
}
|
|
4208
|
+
/**
|
|
4209
|
+
* 양방향(WS 전용) 기능을 SSE fallback 모드에서 호출하면 명확한 에러를 던진다.
|
|
4210
|
+
* SSE 는 단방향이라 presence/typing/subscribe/sendMessage 를 지원하지 못한다.
|
|
4211
|
+
*/
|
|
4212
|
+
assertBidirectional(feature) {
|
|
4213
|
+
if (this.activeTransport === "sse") {
|
|
4214
|
+
throw new Error(
|
|
4215
|
+
`realtime.${feature}() requires a WebSocket connection, which is blocked on this network. Only AI streaming (realtime.stream) works in SSE fallback mode (cb.realtime.transport === 'sse').`
|
|
4216
|
+
);
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
3992
4219
|
sendRequest(message) {
|
|
3993
4220
|
return new Promise((resolve, reject) => {
|
|
3994
4221
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|