solver-sdk 10.4.8 → 10.5.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/cjs/api/chat-api/index.js +299 -128
- package/dist/cjs/api/chat-api/index.js.map +1 -1
- package/dist/esm/api/chat-api/index.js +277 -128
- package/dist/esm/api/chat-api/index.js.map +1 -1
- package/dist/types/api/chat-api/index.d.ts +13 -1
- 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");
|
|
@@ -36,6 +58,10 @@ class PersistentSSEConnectionImpl {
|
|
|
36
58
|
this.eventHandlers = new Map();
|
|
37
59
|
this.reconnectAttempts = 0;
|
|
38
60
|
this.maxReconnectAttempts = 3;
|
|
61
|
+
// 🔧 WorkAI: Chat events queue for persistent connection
|
|
62
|
+
this.chatEventsQueue = [];
|
|
63
|
+
this.chatEventsResolvers = [];
|
|
64
|
+
this.chatEventsCompleted = false;
|
|
39
65
|
this.id = id;
|
|
40
66
|
this.sessionId = sessionId;
|
|
41
67
|
this.closeCallback = closeCallback;
|
|
@@ -121,6 +147,52 @@ class PersistentSSEConnectionImpl {
|
|
|
121
147
|
});
|
|
122
148
|
}
|
|
123
149
|
}
|
|
150
|
+
// 🔧 WorkAI: Queue chat event for async generator
|
|
151
|
+
queueChatEvent(event) {
|
|
152
|
+
if (this.chatEventsResolvers.length > 0) {
|
|
153
|
+
// Someone is waiting - resolve immediately
|
|
154
|
+
const resolver = this.chatEventsResolvers.shift();
|
|
155
|
+
resolver(event);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Queue for later
|
|
159
|
+
this.chatEventsQueue.push(event);
|
|
160
|
+
}
|
|
161
|
+
// Also emit for listeners
|
|
162
|
+
this.emit('chat_event', event);
|
|
163
|
+
}
|
|
164
|
+
// 🔧 WorkAI: Stream chat events as async generator
|
|
165
|
+
async *streamChatEvents() {
|
|
166
|
+
while (!this.chatEventsCompleted || this.chatEventsQueue.length > 0) {
|
|
167
|
+
// Check queue first
|
|
168
|
+
if (this.chatEventsQueue.length > 0) {
|
|
169
|
+
yield this.chatEventsQueue.shift();
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
// Wait for next event
|
|
173
|
+
const event = await new Promise((resolve) => {
|
|
174
|
+
if (this.chatEventsCompleted) {
|
|
175
|
+
resolve(null);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.chatEventsResolvers.push(resolve);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
if (event === null) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
yield event;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// 🔧 WorkAI: Mark chat events stream as complete
|
|
188
|
+
completeChatEvents() {
|
|
189
|
+
this.chatEventsCompleted = true;
|
|
190
|
+
// Resolve all pending resolvers with null
|
|
191
|
+
while (this.chatEventsResolvers.length > 0) {
|
|
192
|
+
const resolver = this.chatEventsResolvers.shift();
|
|
193
|
+
resolver(null);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
124
196
|
}
|
|
125
197
|
/**
|
|
126
198
|
* API для работы с чатом с поддержкой Anthropic Extended Thinking
|
|
@@ -189,6 +261,52 @@ class ChatApi extends cancel_methods_1.ChatCancelMethods {
|
|
|
189
261
|
};
|
|
190
262
|
this.options = options;
|
|
191
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* 🔧 WorkAI: Выполняет streaming GET запрос используя нативный fetch
|
|
266
|
+
* Используется для persistent SSE connections, где axios не работает
|
|
267
|
+
*
|
|
268
|
+
* @param url - Полный URL для запроса
|
|
269
|
+
* @param headers - HTTP заголовки
|
|
270
|
+
* @returns Promise<Response> с ReadableStream body
|
|
271
|
+
*
|
|
272
|
+
* Best practice from undici docs: Node 18+ has global fetch
|
|
273
|
+
*/
|
|
274
|
+
async fetchStream(url, headers) {
|
|
275
|
+
console.log(`[SDK] 🔌 fetchStream: Attempting fetch to ${url}`);
|
|
276
|
+
console.log(`[SDK] 🔌 fetchStream: globalThis.fetch available: ${typeof globalThis.fetch !== 'undefined'}`);
|
|
277
|
+
// Node 18+ has native fetch support
|
|
278
|
+
if (typeof globalThis.fetch !== 'undefined') {
|
|
279
|
+
console.log(`[SDK] 🔌 fetchStream: Using globalThis.fetch`);
|
|
280
|
+
try {
|
|
281
|
+
const response = await globalThis.fetch(url, {
|
|
282
|
+
method: 'GET',
|
|
283
|
+
headers
|
|
284
|
+
});
|
|
285
|
+
console.log(`[SDK] ✅ fetchStream: Response received, status: ${response.status}, hasBody: ${!!response.body}`);
|
|
286
|
+
return response;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
console.error(`[SDK] ❌ fetchStream: globalThis.fetch failed:`, error.message);
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Fallback для Node 16 через dynamic import
|
|
294
|
+
console.log(`[SDK] 🔌 fetchStream: globalThis.fetch not available, trying node-fetch`);
|
|
295
|
+
try {
|
|
296
|
+
const { default: nodeFetch } = await Promise.resolve().then(() => __importStar(require('node-fetch')));
|
|
297
|
+
console.log(`[SDK] ✅ fetchStream: node-fetch imported`);
|
|
298
|
+
const response = await nodeFetch(url, {
|
|
299
|
+
method: 'GET',
|
|
300
|
+
headers
|
|
301
|
+
});
|
|
302
|
+
console.log(`[SDK] ✅ fetchStream: node-fetch response received, status: ${response.status}`);
|
|
303
|
+
return response;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
console.error(`[SDK] ❌ fetchStream: node-fetch failed:`, error.message);
|
|
307
|
+
throw new Error(`Fetch API unavailable: ${error.message}. Please use Node.js 18+ or install node-fetch`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
192
310
|
/**
|
|
193
311
|
* Отправляет сообщение в чат и получает ответ от модели
|
|
194
312
|
* @param {ChatMessage[]} messages Массив сообщений для отправки
|
|
@@ -926,139 +1044,143 @@ class ChatApi extends cancel_methods_1.ChatCancelMethods {
|
|
|
926
1044
|
if (!clientRequestId) {
|
|
927
1045
|
throw new Error('clientRequestId is required for subscribeToResponse');
|
|
928
1046
|
}
|
|
929
|
-
// 🔧 WorkAI: If persistent connection provided,
|
|
1047
|
+
// 🔧 WorkAI: If persistent connection provided and connected, yield events from it
|
|
930
1048
|
if (options.persistentConnection && options.persistentConnection.isConnected) {
|
|
931
1049
|
this.logger.info(`🔌 [PRE_SUBSCRIBE] Using persistent connection: ${clientRequestId}`);
|
|
932
|
-
//
|
|
933
|
-
|
|
934
|
-
|
|
1050
|
+
// Call onSubscribed callback immediately since connection already exists
|
|
1051
|
+
if (options.onSubscribed) {
|
|
1052
|
+
options.onSubscribed();
|
|
1053
|
+
}
|
|
1054
|
+
// Stream events from persistent connection
|
|
1055
|
+
try {
|
|
1056
|
+
for await (const event of options.persistentConnection.streamChatEvents()) {
|
|
1057
|
+
if (event === null) {
|
|
1058
|
+
break; // Stream completed
|
|
1059
|
+
}
|
|
1060
|
+
// Events from persistent connection are already in ChatStreamChunk format
|
|
1061
|
+
yield event;
|
|
1062
|
+
}
|
|
1063
|
+
this.logger.info(`🔌 [PERSISTENT_STREAM] Completed streaming from persistent connection`);
|
|
1064
|
+
return; // Successfully streamed from persistent connection
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
// 🚨 КРИТИЧНО: ошибка streaming из persistent connection
|
|
1068
|
+
this.logger.error(`❌ [PERSISTENT_STREAM] CRITICAL ERROR streaming from persistent connection: ${error.message} | ` +
|
|
1069
|
+
`stack=${error.stack} | clientRequestId=${clientRequestId} | connectionId=${options.persistentConnection.id}`);
|
|
1070
|
+
// 🚨 НЕ ДЕЛАЕМ FALLBACK! Если persistent connection сломан - это должно быть видно!
|
|
1071
|
+
throw new Error(`Persistent SSE stream failed: ${error.message}. ` +
|
|
1072
|
+
`This indicates a problem with persistent connection lifecycle. ` +
|
|
1073
|
+
`Check backend routing and connection management.`);
|
|
1074
|
+
}
|
|
935
1075
|
}
|
|
936
1076
|
const endpoint = `/api/v1/chat/subscribe?clientRequestId=${encodeURIComponent(clientRequestId)}`;
|
|
1077
|
+
// 🚨 КРИТИЧНО: Если дошли сюда БЕЗ persistent connection - это проблема!
|
|
1078
|
+
this.logger.error(`❌ [PRE_SUBSCRIBE] FALLBACK to regular /subscribe endpoint! ` +
|
|
1079
|
+
`This means persistent connection is NOT working. ` +
|
|
1080
|
+
`Multiple continuations WILL fail with "No response body" errors.`);
|
|
937
1081
|
this.logger.info(`🔌 [PRE_SUBSCRIBE] Connecting to SSE: ${clientRequestId}`);
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1082
|
+
try {
|
|
1083
|
+
// Используем request с responseType='stream' для GET запроса с SSE
|
|
1084
|
+
const response = await this.httpClient.request({
|
|
1085
|
+
method: 'GET',
|
|
1086
|
+
url: endpoint,
|
|
1087
|
+
headers: {
|
|
1088
|
+
'X-Project-ID': options.projectId,
|
|
1089
|
+
'X-Client-Ready': 'true',
|
|
1090
|
+
},
|
|
1091
|
+
responseType: 'stream',
|
|
1092
|
+
});
|
|
1093
|
+
// 🔍 DEBUG: Log response details
|
|
1094
|
+
this.logger.debug(`🔌 [PRE_SUBSCRIBE] Response status: ${response.status}`);
|
|
1095
|
+
this.logger.debug(`🔌 [PRE_SUBSCRIBE] Response headers: ${JSON.stringify(response.headers)}`);
|
|
1096
|
+
this.logger.debug(`🔌 [PRE_SUBSCRIBE] Response body present: ${!!response.body}`);
|
|
1097
|
+
if (!response.body) {
|
|
1098
|
+
this.logger.error(`❌ [PRE_SUBSCRIBE] No response body! Full response: ${JSON.stringify({
|
|
1099
|
+
status: response.status,
|
|
1100
|
+
statusText: response.statusText,
|
|
1101
|
+
headers: response.headers,
|
|
1102
|
+
bodyType: typeof response.body,
|
|
1103
|
+
})}`);
|
|
1104
|
+
throw new Error('No response body from subscribe endpoint');
|
|
945
1105
|
}
|
|
1106
|
+
const reader = response.body.getReader();
|
|
1107
|
+
const decoder = new TextDecoder();
|
|
1108
|
+
let subscribed = false;
|
|
1109
|
+
// 🔧 FIX: Буфер для накопления неполных SSE строк
|
|
1110
|
+
let lineBuffer = '';
|
|
946
1111
|
try {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
let lineBuffer = '';
|
|
975
|
-
try {
|
|
976
|
-
while (true) {
|
|
977
|
-
const { done, value } = await reader.read();
|
|
978
|
-
if (done) {
|
|
979
|
-
this.logger.info(`🔌 [PRE_SUBSCRIBE] SSE stream ended: ${clientRequestId}`);
|
|
980
|
-
break;
|
|
981
|
-
}
|
|
982
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
983
|
-
// 🔧 FIX: Добавляем к буферу, а не парсим сразу
|
|
984
|
-
lineBuffer += chunk;
|
|
985
|
-
// Ищем полные строки (заканчиваются на \n)
|
|
986
|
-
const lines = lineBuffer.split('\n');
|
|
987
|
-
// 🔧 FIX: Последняя "строка" может быть неполной - сохраняем в буфер
|
|
988
|
-
lineBuffer = lines.pop() || '';
|
|
989
|
-
for (const line of lines) {
|
|
990
|
-
if (!line.trim() || !line.startsWith('data: '))
|
|
991
|
-
continue;
|
|
992
|
-
const data = line.slice(6).trim();
|
|
993
|
-
if (!data)
|
|
994
|
-
continue;
|
|
995
|
-
try {
|
|
996
|
-
const event = JSON.parse(data);
|
|
997
|
-
// Callback when subscribed
|
|
998
|
-
if (event.type === 'subscribed' && !subscribed) {
|
|
999
|
-
subscribed = true;
|
|
1000
|
-
this.logger.info(`🔌 [PRE_SUBSCRIBE] Subscribed successfully: ${clientRequestId}`);
|
|
1001
|
-
if (options.onSubscribed) {
|
|
1002
|
-
options.onSubscribed();
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
// Skip heartbeat events
|
|
1006
|
-
if (event.type === 'heartbeat') {
|
|
1007
|
-
continue;
|
|
1008
|
-
}
|
|
1009
|
-
// Timeout event
|
|
1010
|
-
if (event.type === 'timeout') {
|
|
1011
|
-
this.logger.warn(`🔌 [PRE_SUBSCRIBE] Timeout: ${clientRequestId}`);
|
|
1012
|
-
break;
|
|
1013
|
-
}
|
|
1014
|
-
// Convert to ChatStreamChunk and yield
|
|
1015
|
-
const streamChunk = {
|
|
1016
|
-
type: event.type,
|
|
1017
|
-
...event,
|
|
1018
|
-
};
|
|
1019
|
-
yield streamChunk;
|
|
1020
|
-
// End on message_stop
|
|
1021
|
-
if (event.type === 'message_stop') {
|
|
1022
|
-
this.logger.info(`🔌 [PRE_SUBSCRIBE] Message stop received: ${clientRequestId}`);
|
|
1023
|
-
break;
|
|
1112
|
+
while (true) {
|
|
1113
|
+
const { done, value } = await reader.read();
|
|
1114
|
+
if (done) {
|
|
1115
|
+
this.logger.info(`🔌 [PRE_SUBSCRIBE] SSE stream ended: ${clientRequestId}`);
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1119
|
+
// 🔧 FIX: Добавляем к буферу, а не парсим сразу
|
|
1120
|
+
lineBuffer += chunk;
|
|
1121
|
+
// Ищем полные строки (заканчиваются на \n)
|
|
1122
|
+
const lines = lineBuffer.split('\n');
|
|
1123
|
+
// 🔧 FIX: Последняя "строка" может быть неполной - сохраняем в буфер
|
|
1124
|
+
lineBuffer = lines.pop() || '';
|
|
1125
|
+
for (const line of lines) {
|
|
1126
|
+
if (!line.trim() || !line.startsWith('data: '))
|
|
1127
|
+
continue;
|
|
1128
|
+
const data = line.slice(6).trim();
|
|
1129
|
+
if (!data)
|
|
1130
|
+
continue;
|
|
1131
|
+
try {
|
|
1132
|
+
const event = JSON.parse(data);
|
|
1133
|
+
// Callback when subscribed
|
|
1134
|
+
if (event.type === 'subscribed' && !subscribed) {
|
|
1135
|
+
subscribed = true;
|
|
1136
|
+
this.logger.info(`🔌 [PRE_SUBSCRIBE] Subscribed successfully: ${clientRequestId}`);
|
|
1137
|
+
if (options.onSubscribed) {
|
|
1138
|
+
options.onSubscribed();
|
|
1024
1139
|
}
|
|
1025
1140
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1141
|
+
// Skip heartbeat events
|
|
1142
|
+
if (event.type === 'heartbeat') {
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
// Timeout event
|
|
1146
|
+
if (event.type === 'timeout') {
|
|
1147
|
+
this.logger.warn(`🔌 [PRE_SUBSCRIBE] Timeout: ${clientRequestId}`);
|
|
1148
|
+
break;
|
|
1028
1149
|
}
|
|
1150
|
+
// Convert to ChatStreamChunk and yield
|
|
1151
|
+
const streamChunk = {
|
|
1152
|
+
type: event.type,
|
|
1153
|
+
...event,
|
|
1154
|
+
};
|
|
1155
|
+
yield streamChunk;
|
|
1156
|
+
// End on message_stop
|
|
1157
|
+
if (event.type === 'message_stop') {
|
|
1158
|
+
this.logger.info(`🔌 [PRE_SUBSCRIBE] Message stop received: ${clientRequestId}`);
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
catch (parseError) {
|
|
1163
|
+
this.logger.warn(`🔌 [PRE_SUBSCRIBE] Parse error: ${parseError}`);
|
|
1029
1164
|
}
|
|
1030
1165
|
}
|
|
1031
1166
|
}
|
|
1032
|
-
finally {
|
|
1033
|
-
reader.releaseLock();
|
|
1034
|
-
}
|
|
1035
|
-
// Success - break retry loop
|
|
1036
|
-
return;
|
|
1037
1167
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
// 🔌 WorkAI: Graceful handling для stream abort
|
|
1041
|
-
if (error.name === 'StreamAbortedError' ||
|
|
1042
|
-
(error.message && error.message.includes('aborted'))) {
|
|
1043
|
-
this.logger.info(`🔄 [PRE_SUBSCRIBE] SSE stream aborted (likely replaced): ${clientRequestId}`);
|
|
1044
|
-
// НЕ выбрасываем ошибку - это нормальное завершение
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
// 🔄 WorkAI: Retry for "No response body" errors
|
|
1048
|
-
if (error.message && error.message.includes('No response body')) {
|
|
1049
|
-
if (attempt < maxRetries) {
|
|
1050
|
-
this.logger.warn(`⚠️ [PRE_SUBSCRIBE] No response body, will retry: ${clientRequestId}`);
|
|
1051
|
-
continue; // Try again
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
this.logger.error(`🔌 [PRE_SUBSCRIBE] Error: ${error.message}`);
|
|
1055
|
-
throw error;
|
|
1168
|
+
finally {
|
|
1169
|
+
reader.releaseLock();
|
|
1056
1170
|
}
|
|
1057
1171
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
this.logger.error(`❌ [PRE_SUBSCRIBE]
|
|
1061
|
-
|
|
1172
|
+
catch (error) {
|
|
1173
|
+
// 🚨 КРИТИЧНО: Ошибка при fallback на /subscribe
|
|
1174
|
+
this.logger.error(`❌ [PRE_SUBSCRIBE] Failed to subscribe to /subscribe endpoint: ${error.message} | ` +
|
|
1175
|
+
`stack=${error.stack} | clientRequestId=${clientRequestId}`);
|
|
1176
|
+
// 🔌 WorkAI: Graceful handling только для stream abort
|
|
1177
|
+
if (error.name === 'StreamAbortedError' ||
|
|
1178
|
+
(error.message && error.message.includes('aborted'))) {
|
|
1179
|
+
this.logger.info(`🔄 [PRE_SUBSCRIBE] SSE stream aborted (likely replaced): ${clientRequestId}`);
|
|
1180
|
+
return; // Это нормально
|
|
1181
|
+
}
|
|
1182
|
+
// Для всех остальных ошибок - прокидываем наверх
|
|
1183
|
+
throw error;
|
|
1062
1184
|
}
|
|
1063
1185
|
}
|
|
1064
1186
|
/**
|
|
@@ -1076,45 +1198,93 @@ class ChatApi extends cancel_methods_1.ChatCancelMethods {
|
|
|
1076
1198
|
}
|
|
1077
1199
|
});
|
|
1078
1200
|
try {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1201
|
+
// 🔧 WorkAI: Получаем auth token динамически для 401 retry support
|
|
1202
|
+
console.log(`[SDK] 🔌 openPersistentSSE: Getting auth token...`);
|
|
1203
|
+
let authToken = null;
|
|
1204
|
+
if (typeof this.httpClient.options?.getAuthToken === 'function') {
|
|
1205
|
+
try {
|
|
1206
|
+
authToken = await Promise.resolve(this.httpClient.options.getAuthToken());
|
|
1207
|
+
console.log(`[SDK] ✅ openPersistentSSE: Auth token obtained`);
|
|
1208
|
+
}
|
|
1209
|
+
catch (error) {
|
|
1210
|
+
console.log(`[SDK] ⚠️ openPersistentSSE: Failed to get auth token: ${error.message}`);
|
|
1211
|
+
this.logger.warn(`⚠️ [PERSISTENT_SSE] Failed to get auth token: ${error.message}`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
// Формируем headers с auth token
|
|
1215
|
+
const headers = {
|
|
1216
|
+
'X-Project-ID': options.projectId,
|
|
1217
|
+
};
|
|
1218
|
+
if (authToken) {
|
|
1219
|
+
headers['Authorization'] = `Bearer ${authToken}`;
|
|
1220
|
+
}
|
|
1221
|
+
// Формируем полный URL (baseURL + endpoint)
|
|
1222
|
+
const baseURL = this.httpClient.getBaseURL?.() || 'http://localhost:3000';
|
|
1223
|
+
const fullUrl = `${baseURL}${endpoint}`;
|
|
1224
|
+
console.log(`[SDK] 🔌 openPersistentSSE: Calling fetchStream to ${fullUrl}`);
|
|
1225
|
+
// 🔧 WorkAI: Используем нативный fetch вместо axios для streaming
|
|
1226
|
+
// Best practice: fetch нативно возвращает Response с ReadableStream body
|
|
1227
|
+
const response = await this.fetchStream(fullUrl, headers);
|
|
1228
|
+
// Проверка статуса (401 retry будет добавлен отдельно если потребуется)
|
|
1229
|
+
console.log(`[SDK] 📡 openPersistentSSE: Response status: ${response.status}`);
|
|
1230
|
+
if (!response.ok) {
|
|
1231
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1232
|
+
}
|
|
1233
|
+
if (!response.body) {
|
|
1234
|
+
console.error(`[SDK] ❌ openPersistentSSE: No response body!`);
|
|
1086
1235
|
throw new Error('No response body from persistent SSE');
|
|
1236
|
+
}
|
|
1237
|
+
console.log(`[SDK] ✅ openPersistentSSE: Response body available, starting SSE parsing...`);
|
|
1238
|
+
// 🔧 SSE parsing согласно best practices (ofetch + undici)
|
|
1087
1239
|
const reader = response.body.getReader();
|
|
1088
1240
|
const decoder = new TextDecoder();
|
|
1089
1241
|
let buffer = '';
|
|
1242
|
+
// Асинхронно читаем stream
|
|
1090
1243
|
(async () => {
|
|
1091
1244
|
try {
|
|
1092
1245
|
while (true) {
|
|
1093
1246
|
const { done, value } = await reader.read();
|
|
1094
1247
|
if (done)
|
|
1095
1248
|
break;
|
|
1249
|
+
// Декодируем с stream: true для поддержки multi-byte символов
|
|
1096
1250
|
buffer += decoder.decode(value, { stream: true });
|
|
1097
1251
|
const lines = buffer.split('\n');
|
|
1252
|
+
// Оставляем неполную строку в buffer (best practice)
|
|
1098
1253
|
buffer = lines.pop() || '';
|
|
1099
1254
|
for (const line of lines) {
|
|
1100
1255
|
if (line.startsWith('data: ')) {
|
|
1101
1256
|
try {
|
|
1102
1257
|
const event = JSON.parse(line.slice(6));
|
|
1103
|
-
|
|
1258
|
+
// Handle connection lifecycle events
|
|
1259
|
+
if (event.type === 'persistent_ready') {
|
|
1104
1260
|
connection.markReady();
|
|
1105
|
-
|
|
1261
|
+
}
|
|
1262
|
+
else if (event.type === 'ping') {
|
|
1106
1263
|
connection.updatePing(event.timestamp);
|
|
1264
|
+
}
|
|
1107
1265
|
else if (event.type === 'connection_closed') {
|
|
1108
1266
|
connection.isConnected = false;
|
|
1267
|
+
connection.completeChatEvents();
|
|
1109
1268
|
break;
|
|
1110
1269
|
}
|
|
1270
|
+
// 🔧 WorkAI: Queue chat events (message_start, content_block_delta, etc.)
|
|
1271
|
+
else if (event.type && event.type.startsWith('message_') ||
|
|
1272
|
+
event.type === 'content_block_start' ||
|
|
1273
|
+
event.type === 'content_block_delta' ||
|
|
1274
|
+
event.type === 'content_block_stop' ||
|
|
1275
|
+
event.type === 'error') {
|
|
1276
|
+
connection.queueChatEvent(event);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
catch (e) {
|
|
1280
|
+
// Ignore JSON parse errors for non-JSON SSE events
|
|
1111
1281
|
}
|
|
1112
|
-
catch (e) { }
|
|
1113
1282
|
}
|
|
1114
1283
|
}
|
|
1115
1284
|
}
|
|
1116
1285
|
}
|
|
1117
1286
|
catch (error) {
|
|
1287
|
+
// Ignore abort errors (normal when connection closes)
|
|
1118
1288
|
if (!error.message?.includes('aborted')) {
|
|
1119
1289
|
connection.emit('error', error);
|
|
1120
1290
|
connection.markFailed(error);
|
|
@@ -1124,9 +1294,10 @@ class ChatApi extends cancel_methods_1.ChatCancelMethods {
|
|
|
1124
1294
|
reader.releaseLock();
|
|
1125
1295
|
}
|
|
1126
1296
|
})();
|
|
1297
|
+
// Ждем persistent_ready event с timeout
|
|
1127
1298
|
await Promise.race([
|
|
1128
1299
|
connection.waitForReady(),
|
|
1129
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('
|
|
1300
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Persistent SSE timeout after 10s')), 10000)),
|
|
1130
1301
|
]);
|
|
1131
1302
|
return connection;
|
|
1132
1303
|
}
|