solver-sdk 10.4.7 → 10.4.9
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/cjs/api/chat-api/index.js +431 -77
- package/dist/cjs/api/chat-api/index.js.map +1 -1
- package/dist/esm/api/chat-api/index.js +409 -77
- package/dist/esm/api/chat-api/index.js.map +1 -1
- package/dist/types/api/chat-api/index.d.ts +38 -0
- package/dist/types/api/chat-api/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -10,9 +10,31 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
|
|
|
10
10
|
if (k2 === undefined) k2 = k;
|
|
11
11
|
o[k2] = m[k];
|
|
12
12
|
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
13
18
|
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
19
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
20
|
};
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
16
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
39
|
exports.ChatApi = void 0;
|
|
18
40
|
const stream_utils_1 = require("./stream-utils");
|
|
@@ -25,6 +47,103 @@ function generateId(length = 10) {
|
|
|
25
47
|
// Экспортируем все типы и интерфейсы для внешнего использования
|
|
26
48
|
__exportStar(require("./models"), exports);
|
|
27
49
|
__exportStar(require("./interfaces"), exports);
|
|
50
|
+
/**
|
|
51
|
+
* 🔧 WorkAI: Internal Persistent Connection Implementation
|
|
52
|
+
*/
|
|
53
|
+
class PersistentSSEConnectionImpl {
|
|
54
|
+
constructor(id, sessionId, closeCallback) {
|
|
55
|
+
this.isConnected = false;
|
|
56
|
+
this.lastPing = 0;
|
|
57
|
+
this.createdAt = Date.now();
|
|
58
|
+
this.eventHandlers = new Map();
|
|
59
|
+
this.reconnectAttempts = 0;
|
|
60
|
+
this.maxReconnectAttempts = 3;
|
|
61
|
+
this.id = id;
|
|
62
|
+
this.sessionId = sessionId;
|
|
63
|
+
this.closeCallback = closeCallback;
|
|
64
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
65
|
+
this.resolveReady = resolve;
|
|
66
|
+
this.rejectReady = reject;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async waitForReady() {
|
|
70
|
+
return this.readyPromise;
|
|
71
|
+
}
|
|
72
|
+
async close(reason = 'client_request') {
|
|
73
|
+
if (!this.isConnected) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.isConnected = false;
|
|
77
|
+
this.emit('close', { reason });
|
|
78
|
+
if (this.closeCallback) {
|
|
79
|
+
await this.closeCallback(reason);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
on(event, callback) {
|
|
83
|
+
if (!this.eventHandlers.has(event)) {
|
|
84
|
+
this.eventHandlers.set(event, new Set());
|
|
85
|
+
}
|
|
86
|
+
this.eventHandlers.get(event).add(callback);
|
|
87
|
+
}
|
|
88
|
+
off(event, callback) {
|
|
89
|
+
const handlers = this.eventHandlers.get(event);
|
|
90
|
+
if (handlers) {
|
|
91
|
+
handlers.delete(callback);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Internal methods
|
|
95
|
+
markReady() {
|
|
96
|
+
this.isConnected = true;
|
|
97
|
+
this.reconnectAttempts = 0; // Reset on successful connection
|
|
98
|
+
this.resolveReady();
|
|
99
|
+
}
|
|
100
|
+
markFailed(error) {
|
|
101
|
+
this.isConnected = false;
|
|
102
|
+
this.rejectReady(error);
|
|
103
|
+
// Auto-reconnect with exponential backoff
|
|
104
|
+
this.attemptReconnect();
|
|
105
|
+
}
|
|
106
|
+
updatePing(timestamp) {
|
|
107
|
+
this.lastPing = timestamp;
|
|
108
|
+
this.emit('ping', { timestamp });
|
|
109
|
+
}
|
|
110
|
+
setReconnectCallback(callback) {
|
|
111
|
+
this.reconnectCallback = callback;
|
|
112
|
+
}
|
|
113
|
+
async attemptReconnect() {
|
|
114
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
115
|
+
console.error(`❌ Max reconnect attempts (${this.maxReconnectAttempts}) reached`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.reconnectAttempts++;
|
|
119
|
+
const backoffDelay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 4000);
|
|
120
|
+
console.log(`🔄 Reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} after ${backoffDelay}ms`);
|
|
121
|
+
this.emit('reconnecting', { attempt: this.reconnectAttempts });
|
|
122
|
+
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
|
123
|
+
if (this.reconnectCallback) {
|
|
124
|
+
try {
|
|
125
|
+
await this.reconnectCallback();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error(`❌ Reconnect failed:`, error);
|
|
129
|
+
// Will retry again on next error
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
emit(event, data) {
|
|
134
|
+
const handlers = this.eventHandlers.get(event);
|
|
135
|
+
if (handlers) {
|
|
136
|
+
handlers.forEach(handler => {
|
|
137
|
+
try {
|
|
138
|
+
handler(data);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.error(`Error in event handler for ${event}:`, error);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
28
147
|
/**
|
|
29
148
|
* API для работы с чатом с поддержкой Anthropic Extended Thinking
|
|
30
149
|
*/
|
|
@@ -82,10 +201,62 @@ class ChatApi extends cancel_methods_1.ChatCancelMethods {
|
|
|
82
201
|
if (debugLevel !== 'silent') {
|
|
83
202
|
console.error(`❌ ${message}`);
|
|
84
203
|
}
|
|
204
|
+
},
|
|
205
|
+
debug: (message) => {
|
|
206
|
+
const debugLevel = this.options.debug;
|
|
207
|
+
if (debugLevel === 'verbose' || debugLevel === 'debug') {
|
|
208
|
+
console.log(`🔍 ${message}`);
|
|
209
|
+
}
|
|
85
210
|
}
|
|
86
211
|
};
|
|
87
212
|
this.options = options;
|
|
88
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* 🔧 WorkAI: Выполняет streaming GET запрос используя нативный fetch
|
|
216
|
+
* Используется для persistent SSE connections, где axios не работает
|
|
217
|
+
*
|
|
218
|
+
* @param url - Полный URL для запроса
|
|
219
|
+
* @param headers - HTTP заголовки
|
|
220
|
+
* @returns Promise<Response> с ReadableStream body
|
|
221
|
+
*
|
|
222
|
+
* Best practice from undici docs: Node 18+ has global fetch
|
|
223
|
+
*/
|
|
224
|
+
async fetchStream(url, headers) {
|
|
225
|
+
console.log(`[SDK] 🔌 fetchStream: Attempting fetch to ${url}`);
|
|
226
|
+
console.log(`[SDK] 🔌 fetchStream: globalThis.fetch available: ${typeof globalThis.fetch !== 'undefined'}`);
|
|
227
|
+
// Node 18+ has native fetch support
|
|
228
|
+
if (typeof globalThis.fetch !== 'undefined') {
|
|
229
|
+
console.log(`[SDK] 🔌 fetchStream: Using globalThis.fetch`);
|
|
230
|
+
try {
|
|
231
|
+
const response = await globalThis.fetch(url, {
|
|
232
|
+
method: 'GET',
|
|
233
|
+
headers
|
|
234
|
+
});
|
|
235
|
+
console.log(`[SDK] ✅ fetchStream: Response received, status: ${response.status}, hasBody: ${!!response.body}`);
|
|
236
|
+
return response;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
console.error(`[SDK] ❌ fetchStream: globalThis.fetch failed:`, error.message);
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Fallback для Node 16 через dynamic import
|
|
244
|
+
console.log(`[SDK] 🔌 fetchStream: globalThis.fetch not available, trying node-fetch`);
|
|
245
|
+
try {
|
|
246
|
+
const { default: nodeFetch } = await Promise.resolve().then(() => __importStar(require('node-fetch')));
|
|
247
|
+
console.log(`[SDK] ✅ fetchStream: node-fetch imported`);
|
|
248
|
+
const response = await nodeFetch(url, {
|
|
249
|
+
method: 'GET',
|
|
250
|
+
headers
|
|
251
|
+
});
|
|
252
|
+
console.log(`[SDK] ✅ fetchStream: node-fetch response received, status: ${response.status}`);
|
|
253
|
+
return response;
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
console.error(`[SDK] ❌ fetchStream: node-fetch failed:`, error.message);
|
|
257
|
+
throw new Error(`Fetch API unavailable: ${error.message}. Please use Node.js 18+ or install node-fetch`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
89
260
|
/**
|
|
90
261
|
* Отправляет сообщение в чат и получает ответ от модели
|
|
91
262
|
* @param {ChatMessage[]} messages Массив сообщений для отправки
|
|
@@ -367,6 +538,35 @@ class ChatApi extends cancel_methods_1.ChatCancelMethods {
|
|
|
367
538
|
const messages = [{ role: 'user', content: prompt }];
|
|
368
539
|
yield* this.streamChat(messages, options);
|
|
369
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* 🔧 WorkAI: Send continuation via persistent SSE connection
|
|
543
|
+
* Uses existing connection instead of creating new one
|
|
544
|
+
*/
|
|
545
|
+
async *sendContinuationViaPersistent(connection, messages, options) {
|
|
546
|
+
if (!connection.isConnected) {
|
|
547
|
+
throw new Error('Persistent SSE connection is not active');
|
|
548
|
+
}
|
|
549
|
+
this.logger.info(`🔌 [CONTINUATION_VIA_PERSISTENT] Using connection: ${connection.id}`);
|
|
550
|
+
try {
|
|
551
|
+
const params = this.buildRequestParams(messages, {
|
|
552
|
+
...options,
|
|
553
|
+
stream: true,
|
|
554
|
+
clientRequestId: connection.id,
|
|
555
|
+
});
|
|
556
|
+
params.beta = 'interleaved-thinking-2025-05-14';
|
|
557
|
+
const response = await this.httpClient.post('/api/v1/chat/continue', params);
|
|
558
|
+
this.logger.info(`✅ [CONTINUATION_VIA_PERSISTENT] Request sent, waiting for events via persistent SSE`);
|
|
559
|
+
// Events will arrive via the persistent SSE connection
|
|
560
|
+
// We need to yield them from connection, but connection doesn't expose events yet
|
|
561
|
+
// For now, return empty - будет реализовано когда connection будет слушать continuation события
|
|
562
|
+
// TODO: Implement event listening on PersistentSSEConnection
|
|
563
|
+
this.logger.warn('⚠️ [CONTINUATION_VIA_PERSISTENT] Event listening not yet implemented');
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
this.logger.error(`❌ [CONTINUATION_VIA_PERSISTENT] Error: ${error.message}`);
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
370
570
|
/**
|
|
371
571
|
* Отправляет continuation запрос для interleaved thinking
|
|
372
572
|
*/
|
|
@@ -794,97 +994,251 @@ class ChatApi extends cancel_methods_1.ChatCancelMethods {
|
|
|
794
994
|
if (!clientRequestId) {
|
|
795
995
|
throw new Error('clientRequestId is required for subscribeToResponse');
|
|
796
996
|
}
|
|
997
|
+
// 🔧 WorkAI: If persistent connection provided, use it instead
|
|
998
|
+
if (options.persistentConnection && options.persistentConnection.isConnected) {
|
|
999
|
+
this.logger.info(`🔌 [PRE_SUBSCRIBE] Using persistent connection: ${clientRequestId}`);
|
|
1000
|
+
// TODO: Yield events from persistent connection
|
|
1001
|
+
// For now, fallback to direct subscription
|
|
1002
|
+
this.logger.warn(`⚠️ [PRE_SUBSCRIBE] Persistent connection event streaming not yet implemented`);
|
|
1003
|
+
}
|
|
797
1004
|
const endpoint = `/api/v1/chat/subscribe?clientRequestId=${encodeURIComponent(clientRequestId)}`;
|
|
798
1005
|
this.logger.info(`🔌 [PRE_SUBSCRIBE] Connecting to SSE: ${clientRequestId}`);
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
'X-Client-Ready': 'true',
|
|
807
|
-
},
|
|
808
|
-
responseType: 'stream',
|
|
809
|
-
});
|
|
810
|
-
if (!response.body) {
|
|
811
|
-
throw new Error('No response body from subscribe endpoint');
|
|
1006
|
+
// 🔧 WorkAI: Retry logic for "No response body" errors
|
|
1007
|
+
let lastError = null;
|
|
1008
|
+
const maxRetries = 1;
|
|
1009
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1010
|
+
if (attempt > 0) {
|
|
1011
|
+
this.logger.warn(`🔄 [RETRY_SUBSCRIBE] Attempt ${attempt + 1}/${maxRetries + 1} after 500ms`);
|
|
1012
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
812
1013
|
}
|
|
813
|
-
const reader = response.body.getReader();
|
|
814
|
-
const decoder = new TextDecoder();
|
|
815
|
-
let subscribed = false;
|
|
816
|
-
// 🔧 FIX: Буфер для накопления неполных SSE строк
|
|
817
|
-
let lineBuffer = '';
|
|
818
1014
|
try {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1015
|
+
// Используем request с responseType='stream' для GET запроса с SSE
|
|
1016
|
+
const response = await this.httpClient.request({
|
|
1017
|
+
method: 'GET',
|
|
1018
|
+
url: endpoint,
|
|
1019
|
+
headers: {
|
|
1020
|
+
'X-Project-ID': options.projectId,
|
|
1021
|
+
'X-Client-Ready': 'true',
|
|
1022
|
+
},
|
|
1023
|
+
responseType: 'stream',
|
|
1024
|
+
});
|
|
1025
|
+
// 🔍 DEBUG: Log response details
|
|
1026
|
+
this.logger.debug(`🔌 [PRE_SUBSCRIBE] Response status: ${response.status}`);
|
|
1027
|
+
this.logger.debug(`🔌 [PRE_SUBSCRIBE] Response headers: ${JSON.stringify(response.headers)}`);
|
|
1028
|
+
this.logger.debug(`🔌 [PRE_SUBSCRIBE] Response body present: ${!!response.body}`);
|
|
1029
|
+
if (!response.body) {
|
|
1030
|
+
this.logger.error(`❌ [PRE_SUBSCRIBE] No response body! Full response: ${JSON.stringify({
|
|
1031
|
+
status: response.status,
|
|
1032
|
+
statusText: response.statusText,
|
|
1033
|
+
headers: response.headers,
|
|
1034
|
+
bodyType: typeof response.body,
|
|
1035
|
+
})}`);
|
|
1036
|
+
throw new Error('No response body from subscribe endpoint');
|
|
1037
|
+
}
|
|
1038
|
+
const reader = response.body.getReader();
|
|
1039
|
+
const decoder = new TextDecoder();
|
|
1040
|
+
let subscribed = false;
|
|
1041
|
+
// 🔧 FIX: Буфер для накопления неполных SSE строк
|
|
1042
|
+
let lineBuffer = '';
|
|
1043
|
+
try {
|
|
1044
|
+
while (true) {
|
|
1045
|
+
const { done, value } = await reader.read();
|
|
1046
|
+
if (done) {
|
|
1047
|
+
this.logger.info(`🔌 [PRE_SUBSCRIBE] SSE stream ended: ${clientRequestId}`);
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1051
|
+
// 🔧 FIX: Добавляем к буферу, а не парсим сразу
|
|
1052
|
+
lineBuffer += chunk;
|
|
1053
|
+
// Ищем полные строки (заканчиваются на \n)
|
|
1054
|
+
const lines = lineBuffer.split('\n');
|
|
1055
|
+
// 🔧 FIX: Последняя "строка" может быть неполной - сохраняем в буфер
|
|
1056
|
+
lineBuffer = lines.pop() || '';
|
|
1057
|
+
for (const line of lines) {
|
|
1058
|
+
if (!line.trim() || !line.startsWith('data: '))
|
|
850
1059
|
continue;
|
|
1060
|
+
const data = line.slice(6).trim();
|
|
1061
|
+
if (!data)
|
|
1062
|
+
continue;
|
|
1063
|
+
try {
|
|
1064
|
+
const event = JSON.parse(data);
|
|
1065
|
+
// Callback when subscribed
|
|
1066
|
+
if (event.type === 'subscribed' && !subscribed) {
|
|
1067
|
+
subscribed = true;
|
|
1068
|
+
this.logger.info(`🔌 [PRE_SUBSCRIBE] Subscribed successfully: ${clientRequestId}`);
|
|
1069
|
+
if (options.onSubscribed) {
|
|
1070
|
+
options.onSubscribed();
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
// Skip heartbeat events
|
|
1074
|
+
if (event.type === 'heartbeat') {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
// Timeout event
|
|
1078
|
+
if (event.type === 'timeout') {
|
|
1079
|
+
this.logger.warn(`🔌 [PRE_SUBSCRIBE] Timeout: ${clientRequestId}`);
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
// Convert to ChatStreamChunk and yield
|
|
1083
|
+
const streamChunk = {
|
|
1084
|
+
type: event.type,
|
|
1085
|
+
...event,
|
|
1086
|
+
};
|
|
1087
|
+
yield streamChunk;
|
|
1088
|
+
// End on message_stop
|
|
1089
|
+
if (event.type === 'message_stop') {
|
|
1090
|
+
this.logger.info(`🔌 [PRE_SUBSCRIBE] Message stop received: ${clientRequestId}`);
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
851
1093
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
this.logger.warn(`🔌 [PRE_SUBSCRIBE] Timeout: ${clientRequestId}`);
|
|
855
|
-
break;
|
|
856
|
-
}
|
|
857
|
-
// Convert to ChatStreamChunk and yield
|
|
858
|
-
const streamChunk = {
|
|
859
|
-
type: event.type,
|
|
860
|
-
...event,
|
|
861
|
-
};
|
|
862
|
-
yield streamChunk;
|
|
863
|
-
// End on message_stop
|
|
864
|
-
if (event.type === 'message_stop') {
|
|
865
|
-
this.logger.info(`🔌 [PRE_SUBSCRIBE] Message stop received: ${clientRequestId}`);
|
|
866
|
-
break;
|
|
1094
|
+
catch (parseError) {
|
|
1095
|
+
this.logger.warn(`🔌 [PRE_SUBSCRIBE] Parse error: ${parseError}`);
|
|
867
1096
|
}
|
|
868
1097
|
}
|
|
869
|
-
catch (parseError) {
|
|
870
|
-
this.logger.warn(`🔌 [PRE_SUBSCRIBE] Parse error: ${parseError}`);
|
|
871
|
-
}
|
|
872
1098
|
}
|
|
873
1099
|
}
|
|
1100
|
+
finally {
|
|
1101
|
+
reader.releaseLock();
|
|
1102
|
+
}
|
|
1103
|
+
// Success - break retry loop
|
|
1104
|
+
return;
|
|
874
1105
|
}
|
|
875
|
-
|
|
876
|
-
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
lastError = error;
|
|
1108
|
+
// 🔌 WorkAI: Graceful handling для stream abort
|
|
1109
|
+
if (error.name === 'StreamAbortedError' ||
|
|
1110
|
+
(error.message && error.message.includes('aborted'))) {
|
|
1111
|
+
this.logger.info(`🔄 [PRE_SUBSCRIBE] SSE stream aborted (likely replaced): ${clientRequestId}`);
|
|
1112
|
+
// НЕ выбрасываем ошибку - это нормальное завершение
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
// 🔄 WorkAI: Retry for "No response body" errors
|
|
1116
|
+
if (error.message && error.message.includes('No response body')) {
|
|
1117
|
+
if (attempt < maxRetries) {
|
|
1118
|
+
this.logger.warn(`⚠️ [PRE_SUBSCRIBE] No response body, will retry: ${clientRequestId}`);
|
|
1119
|
+
continue; // Try again
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
this.logger.error(`🔌 [PRE_SUBSCRIBE] Error: ${error.message}`);
|
|
1123
|
+
throw error;
|
|
877
1124
|
}
|
|
878
1125
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1126
|
+
// If we exhausted all retries
|
|
1127
|
+
if (lastError) {
|
|
1128
|
+
this.logger.error(`❌ [PRE_SUBSCRIBE] All ${maxRetries + 1} attempts failed: ${lastError.message}`);
|
|
1129
|
+
throw lastError;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* 🔧 WorkAI: Open persistent SSE connection for chat session
|
|
1134
|
+
*/
|
|
1135
|
+
async openPersistentSSE(sessionId, clientRequestId, options) {
|
|
1136
|
+
const endpoint = `/api/v1/chat/subscribe/persistent?sessionId=${encodeURIComponent(sessionId)}&clientRequestId=${encodeURIComponent(clientRequestId)}`;
|
|
1137
|
+
this.logger.info(`🔌 [PERSISTENT_SSE] Opening: ${clientRequestId}`);
|
|
1138
|
+
const connection = new PersistentSSEConnectionImpl(clientRequestId, sessionId, async (reason) => {
|
|
1139
|
+
try {
|
|
1140
|
+
await this.httpClient.post('/api/v1/chat/subscribe/close', { sessionId, reason });
|
|
1141
|
+
}
|
|
1142
|
+
catch (error) {
|
|
1143
|
+
this.logger.error(`❌ [PERSISTENT_SSE] Close error: ${error.message}`);
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
try {
|
|
1147
|
+
// 🔧 WorkAI: Получаем auth token динамически для 401 retry support
|
|
1148
|
+
console.log(`[SDK] 🔌 openPersistentSSE: Getting auth token...`);
|
|
1149
|
+
let authToken = null;
|
|
1150
|
+
if (typeof this.httpClient.options?.getAuthToken === 'function') {
|
|
1151
|
+
try {
|
|
1152
|
+
authToken = await Promise.resolve(this.httpClient.options.getAuthToken());
|
|
1153
|
+
console.log(`[SDK] ✅ openPersistentSSE: Auth token obtained`);
|
|
1154
|
+
}
|
|
1155
|
+
catch (error) {
|
|
1156
|
+
console.log(`[SDK] ⚠️ openPersistentSSE: Failed to get auth token: ${error.message}`);
|
|
1157
|
+
this.logger.warn(`⚠️ [PERSISTENT_SSE] Failed to get auth token: ${error.message}`);
|
|
1158
|
+
}
|
|
886
1159
|
}
|
|
887
|
-
|
|
1160
|
+
// Формируем headers с auth token
|
|
1161
|
+
const headers = {
|
|
1162
|
+
'X-Project-ID': options.projectId,
|
|
1163
|
+
};
|
|
1164
|
+
if (authToken) {
|
|
1165
|
+
headers['Authorization'] = `Bearer ${authToken}`;
|
|
1166
|
+
}
|
|
1167
|
+
// Формируем полный URL (baseURL + endpoint)
|
|
1168
|
+
const baseURL = this.httpClient.getBaseURL?.() || 'http://localhost:3000';
|
|
1169
|
+
const fullUrl = `${baseURL}${endpoint}`;
|
|
1170
|
+
console.log(`[SDK] 🔌 openPersistentSSE: Calling fetchStream to ${fullUrl}`);
|
|
1171
|
+
// 🔧 WorkAI: Используем нативный fetch вместо axios для streaming
|
|
1172
|
+
// Best practice: fetch нативно возвращает Response с ReadableStream body
|
|
1173
|
+
const response = await this.fetchStream(fullUrl, headers);
|
|
1174
|
+
// Проверка статуса (401 retry будет добавлен отдельно если потребуется)
|
|
1175
|
+
console.log(`[SDK] 📡 openPersistentSSE: Response status: ${response.status}`);
|
|
1176
|
+
if (!response.ok) {
|
|
1177
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1178
|
+
}
|
|
1179
|
+
if (!response.body) {
|
|
1180
|
+
console.error(`[SDK] ❌ openPersistentSSE: No response body!`);
|
|
1181
|
+
throw new Error('No response body from persistent SSE');
|
|
1182
|
+
}
|
|
1183
|
+
console.log(`[SDK] ✅ openPersistentSSE: Response body available, starting SSE parsing...`);
|
|
1184
|
+
// 🔧 SSE parsing согласно best practices (ofetch + undici)
|
|
1185
|
+
const reader = response.body.getReader();
|
|
1186
|
+
const decoder = new TextDecoder();
|
|
1187
|
+
let buffer = '';
|
|
1188
|
+
// Асинхронно читаем stream
|
|
1189
|
+
(async () => {
|
|
1190
|
+
try {
|
|
1191
|
+
while (true) {
|
|
1192
|
+
const { done, value } = await reader.read();
|
|
1193
|
+
if (done)
|
|
1194
|
+
break;
|
|
1195
|
+
// Декодируем с stream: true для поддержки multi-byte символов
|
|
1196
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1197
|
+
const lines = buffer.split('\n');
|
|
1198
|
+
// Оставляем неполную строку в buffer (best practice)
|
|
1199
|
+
buffer = lines.pop() || '';
|
|
1200
|
+
for (const line of lines) {
|
|
1201
|
+
if (line.startsWith('data: ')) {
|
|
1202
|
+
try {
|
|
1203
|
+
const event = JSON.parse(line.slice(6));
|
|
1204
|
+
if (event.type === 'persistent_ready') {
|
|
1205
|
+
connection.markReady();
|
|
1206
|
+
}
|
|
1207
|
+
else if (event.type === 'ping') {
|
|
1208
|
+
connection.updatePing(event.timestamp);
|
|
1209
|
+
}
|
|
1210
|
+
else if (event.type === 'connection_closed') {
|
|
1211
|
+
connection.isConnected = false;
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
catch (e) {
|
|
1216
|
+
// Ignore JSON parse errors for non-JSON SSE events
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
catch (error) {
|
|
1223
|
+
// Ignore abort errors (normal when connection closes)
|
|
1224
|
+
if (!error.message?.includes('aborted')) {
|
|
1225
|
+
connection.emit('error', error);
|
|
1226
|
+
connection.markFailed(error);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
finally {
|
|
1230
|
+
reader.releaseLock();
|
|
1231
|
+
}
|
|
1232
|
+
})();
|
|
1233
|
+
// Ждем persistent_ready event с timeout
|
|
1234
|
+
await Promise.race([
|
|
1235
|
+
connection.waitForReady(),
|
|
1236
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Persistent SSE timeout after 10s')), 10000)),
|
|
1237
|
+
]);
|
|
1238
|
+
return connection;
|
|
1239
|
+
}
|
|
1240
|
+
catch (error) {
|
|
1241
|
+
connection.markFailed(error);
|
|
888
1242
|
throw error;
|
|
889
1243
|
}
|
|
890
1244
|
}
|