bloby-bot 0.48.2 → 0.49.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/package.json +1 -1
- package/supervisor/app-ws.js +53 -0
- package/supervisor/index.ts +93 -7
package/package.json
CHANGED
package/supervisor/app-ws.js
CHANGED
|
@@ -16,6 +16,25 @@
|
|
|
16
16
|
var intentionalClose = false;
|
|
17
17
|
var originalFetch = window.fetch;
|
|
18
18
|
|
|
19
|
+
// Chat pub/sub state — bot:* / chat:* events forwarded from the supervisor's chat
|
|
20
|
+
// broadcaster. Listeners are independent of WS lifecycle: when the WS reconnects we
|
|
21
|
+
// re-send the subscribe message automatically.
|
|
22
|
+
var chatListeners = [];
|
|
23
|
+
var chatSubscribed = false;
|
|
24
|
+
var chatClientId = null;
|
|
25
|
+
|
|
26
|
+
function emitChatEvent(eventType, data) {
|
|
27
|
+
for (var i = 0; i < chatListeners.length; i++) {
|
|
28
|
+
try { chatListeners[i](eventType, data); } catch (err) { console.error('[app-ws] chat listener error', err); }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sendChatSubscribe() {
|
|
33
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
34
|
+
ws.send(JSON.stringify({ type: 'chat:subscribe', data: { clientId: chatClientId } }));
|
|
35
|
+
console.log('[app-ws] chat:subscribe sent clientId=' + chatClientId);
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
function buildWsUrl() {
|
|
20
39
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
21
40
|
return proto + '//' + location.host + '/app/ws';
|
|
@@ -56,6 +75,7 @@
|
|
|
56
75
|
reconnectDelay = RECONNECT_BASE;
|
|
57
76
|
startHeartbeat();
|
|
58
77
|
console.log('[app-ws] Connected to /app/ws');
|
|
78
|
+
if (chatSubscribed) sendChatSubscribe();
|
|
59
79
|
};
|
|
60
80
|
|
|
61
81
|
ws.onmessage = function (e) {
|
|
@@ -68,6 +88,16 @@
|
|
|
68
88
|
return;
|
|
69
89
|
}
|
|
70
90
|
|
|
91
|
+
if (msg.type === 'chat:event' && msg.data) {
|
|
92
|
+
emitChatEvent(msg.data.eventType, msg.data.eventData);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (msg.type === 'chat:subscribed') {
|
|
97
|
+
console.log('[app-ws] chat:subscribed ack clientId=' + (msg.data && msg.data.clientId));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
71
101
|
if (msg.type === 'app:api:response' && msg.data && msg.data.id) {
|
|
72
102
|
var pending = pendingRequests[msg.data.id];
|
|
73
103
|
if (pending) {
|
|
@@ -194,6 +224,29 @@
|
|
|
194
224
|
connected: function () {
|
|
195
225
|
return connected;
|
|
196
226
|
},
|
|
227
|
+
// Subscribe to chat events (bot:typing, bot:token, bot:response, chat:sync, …).
|
|
228
|
+
// Returns an unsubscribe function. Multiple listeners are supported. The underlying
|
|
229
|
+
// subscribe message is sent on first listener registration and re-sent on reconnect.
|
|
230
|
+
onChatEvent: function (handler) {
|
|
231
|
+
chatListeners.push(handler);
|
|
232
|
+
if (!chatSubscribed) {
|
|
233
|
+
chatSubscribed = true;
|
|
234
|
+
chatClientId =
|
|
235
|
+
(typeof crypto !== 'undefined' && crypto.randomUUID)
|
|
236
|
+
? crypto.randomUUID()
|
|
237
|
+
: 'ws-' + Math.random().toString(36).slice(2);
|
|
238
|
+
if (ws && ws.readyState === WebSocket.OPEN) sendChatSubscribe();
|
|
239
|
+
}
|
|
240
|
+
return function unsubscribe() {
|
|
241
|
+
var i = chatListeners.indexOf(handler);
|
|
242
|
+
if (i >= 0) chatListeners.splice(i, 1);
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
// Stable per-tab clientId used to identify this subscriber to the server. Useful
|
|
246
|
+
// when calling /api/agent/chat/message so the server can skip echoing chat:sync back.
|
|
247
|
+
clientId: function () {
|
|
248
|
+
return chatClientId;
|
|
249
|
+
},
|
|
197
250
|
};
|
|
198
251
|
|
|
199
252
|
connect();
|
package/supervisor/index.ts
CHANGED
|
@@ -441,7 +441,21 @@ export async function startSupervisor() {
|
|
|
441
441
|
const proxy = http.request(
|
|
442
442
|
{ host: '127.0.0.1', port: backendPort, path: backendPath, method: req.method, headers: req.headers },
|
|
443
443
|
(proxyRes) => {
|
|
444
|
+
const ct = String(proxyRes.headers['content-type'] || '');
|
|
445
|
+
const isSse = ct.includes('text/event-stream');
|
|
446
|
+
if (isSse) {
|
|
447
|
+
console.log(`[app-proxy] SSE upstream status=${proxyRes.statusCode} ct="${ct}" url=${backendPath}`);
|
|
448
|
+
}
|
|
444
449
|
res.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
|
450
|
+
if (isSse) {
|
|
451
|
+
try { res.socket?.setNoDelay(true); } catch {}
|
|
452
|
+
proxyRes.on('data', (chunk: Buffer) => {
|
|
453
|
+
console.log(`[app-proxy] SSE chunk bytes=${chunk.length} preview=${JSON.stringify(chunk.toString('utf-8').slice(0, 80))}`);
|
|
454
|
+
});
|
|
455
|
+
proxyRes.on('end', () => console.log(`[app-proxy] SSE upstream END`));
|
|
456
|
+
proxyRes.on('error', (e: any) => console.log(`[app-proxy] SSE upstream ERROR ${e.message}`));
|
|
457
|
+
res.on('close', () => console.log(`[app-proxy] SSE res CLOSE`));
|
|
458
|
+
}
|
|
445
459
|
proxyRes.pipe(res);
|
|
446
460
|
},
|
|
447
461
|
);
|
|
@@ -1243,25 +1257,33 @@ mint();
|
|
|
1243
1257
|
// GET /api/agent/chat/stream — Server-Sent Events, every chat event (bot:*, chat:*)
|
|
1244
1258
|
if (req.method === 'GET' && agentPath === '/api/agent/chat/stream') {
|
|
1245
1259
|
const clientId = urlObj.searchParams.get('clientId') || undefined;
|
|
1260
|
+
const subId = crypto.randomBytes(8).toString('hex');
|
|
1261
|
+
console.log(`[sse-handler] OPEN sub=${subId} clientId=${clientId} remote=${req.socket.remoteAddress}`);
|
|
1262
|
+
|
|
1246
1263
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
1247
1264
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
1248
1265
|
res.setHeader('Connection', 'keep-alive');
|
|
1249
1266
|
res.setHeader('X-Accel-Buffering', 'no');
|
|
1250
1267
|
res.writeHead(200);
|
|
1251
|
-
res.
|
|
1268
|
+
try { res.socket?.setNoDelay(true); } catch {}
|
|
1269
|
+
const wrote = res.write(': connected\n\n');
|
|
1270
|
+
console.log(`[sse-handler] wrote initial comment sub=${subId} writeOk=${wrote}`);
|
|
1252
1271
|
|
|
1253
1272
|
const sub: ChatSubscriber = {
|
|
1254
|
-
id:
|
|
1273
|
+
id: subId,
|
|
1255
1274
|
clientId,
|
|
1256
1275
|
send: (type, data) => {
|
|
1257
|
-
if (res.writableEnded)
|
|
1258
|
-
|
|
1276
|
+
if (res.writableEnded) {
|
|
1277
|
+
console.log(`[sse-handler] skip write (ended) sub=${subId} type=${type}`);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const ok = res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
1281
|
+
console.log(`[sse-handler] write sub=${subId} type=${type} ok=${ok} bytes=${(type.length + JSON.stringify(data).length + 10)}`);
|
|
1259
1282
|
},
|
|
1260
1283
|
close: () => { try { res.end(); } catch {} },
|
|
1261
1284
|
};
|
|
1262
1285
|
chatSubscribers.add(sub);
|
|
1263
1286
|
|
|
1264
|
-
// Replay current streaming state so a late-joining workspace catches up mid-turn
|
|
1265
1287
|
if (agentQueryActive && currentStreamConvId) {
|
|
1266
1288
|
sub.send('chat:state', {
|
|
1267
1289
|
streaming: true,
|
|
@@ -1273,12 +1295,20 @@ mint();
|
|
|
1273
1295
|
const keepAlive = setInterval(() => {
|
|
1274
1296
|
if (res.writableEnded) return;
|
|
1275
1297
|
res.write(': ping\n\n');
|
|
1298
|
+
console.log(`[sse-handler] ping sub=${subId}`);
|
|
1276
1299
|
}, 25_000);
|
|
1277
1300
|
|
|
1278
1301
|
req.on('close', () => {
|
|
1302
|
+
console.log(`[sse-handler] CLOSE sub=${subId} reason=req-close`);
|
|
1279
1303
|
clearInterval(keepAlive);
|
|
1280
1304
|
chatSubscribers.delete(sub);
|
|
1281
1305
|
});
|
|
1306
|
+
res.on('close', () => {
|
|
1307
|
+
console.log(`[sse-handler] CLOSE sub=${subId} reason=res-close`);
|
|
1308
|
+
});
|
|
1309
|
+
res.on('error', (err: any) => {
|
|
1310
|
+
console.log(`[sse-handler] ERROR sub=${subId} ${err?.message}`);
|
|
1311
|
+
});
|
|
1282
1312
|
return;
|
|
1283
1313
|
}
|
|
1284
1314
|
|
|
@@ -1630,6 +1660,12 @@ mint();
|
|
|
1630
1660
|
appWss.on('connection', (ws) => {
|
|
1631
1661
|
console.log('[supervisor] App API WS client connected');
|
|
1632
1662
|
|
|
1663
|
+
// Per-WS chat subscription: when the client opts in, this WS joins chatSubscribers
|
|
1664
|
+
// and receives every bot:* / chat:* event the dashboard widget does. SSE through the
|
|
1665
|
+
// Cloudflare tunnel buffers chunks; WebSocket frames flow through reliably (same
|
|
1666
|
+
// reason /app/api fetch is routed through this WS to begin with).
|
|
1667
|
+
let chatSub: ChatSubscriber | null = null;
|
|
1668
|
+
|
|
1633
1669
|
ws.on('message', (raw) => {
|
|
1634
1670
|
const rawStr = raw.toString();
|
|
1635
1671
|
|
|
@@ -1645,6 +1681,44 @@ mint();
|
|
|
1645
1681
|
return;
|
|
1646
1682
|
}
|
|
1647
1683
|
|
|
1684
|
+
if (msg.type === 'chat:subscribe') {
|
|
1685
|
+
if (chatSub) chatSubscribers.delete(chatSub);
|
|
1686
|
+
const clientId = msg.data?.clientId;
|
|
1687
|
+
const subId = crypto.randomBytes(8).toString('hex');
|
|
1688
|
+
chatSub = {
|
|
1689
|
+
id: subId,
|
|
1690
|
+
clientId,
|
|
1691
|
+
send: (type, data) => {
|
|
1692
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
1693
|
+
ws.send(JSON.stringify({ type: 'chat:event', data: { eventType: type, eventData: data } }));
|
|
1694
|
+
},
|
|
1695
|
+
close: () => {},
|
|
1696
|
+
};
|
|
1697
|
+
chatSubscribers.add(chatSub);
|
|
1698
|
+
console.log(`[app-ws-chat] subscribe sub=${subId} clientId=${clientId} total=${chatSubscribers.size}`);
|
|
1699
|
+
|
|
1700
|
+
if (agentQueryActive && currentStreamConvId) {
|
|
1701
|
+
chatSub.send('chat:state', {
|
|
1702
|
+
streaming: true,
|
|
1703
|
+
conversationId: currentStreamConvId,
|
|
1704
|
+
buffer: currentStreamBuffer,
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1708
|
+
ws.send(JSON.stringify({ type: 'chat:subscribed', data: { clientId, subId } }));
|
|
1709
|
+
}
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (msg.type === 'chat:unsubscribe') {
|
|
1714
|
+
if (chatSub) {
|
|
1715
|
+
chatSubscribers.delete(chatSub);
|
|
1716
|
+
console.log(`[app-ws-chat] unsubscribe sub=${chatSub.id} total=${chatSubscribers.size}`);
|
|
1717
|
+
chatSub = null;
|
|
1718
|
+
}
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1648
1722
|
if (msg.type !== 'app:api' || !msg.data) return;
|
|
1649
1723
|
|
|
1650
1724
|
const { id, method, path: reqPath, headers: reqHeaders, body } = msg.data;
|
|
@@ -1708,6 +1782,11 @@ mint();
|
|
|
1708
1782
|
});
|
|
1709
1783
|
|
|
1710
1784
|
ws.on('close', () => {
|
|
1785
|
+
if (chatSub) {
|
|
1786
|
+
chatSubscribers.delete(chatSub);
|
|
1787
|
+
console.log(`[app-ws-chat] auto-unsubscribe on close sub=${chatSub.id} total=${chatSubscribers.size}`);
|
|
1788
|
+
chatSub = null;
|
|
1789
|
+
}
|
|
1711
1790
|
console.log('[supervisor] App API WS client disconnected');
|
|
1712
1791
|
});
|
|
1713
1792
|
});
|
|
@@ -1716,11 +1795,18 @@ mint();
|
|
|
1716
1795
|
* subscribers. The workspace mirror sees the exact same event stream the widget does. */
|
|
1717
1796
|
function broadcastBloby(type: string, data: any = {}) {
|
|
1718
1797
|
const msg = JSON.stringify({ type, data });
|
|
1798
|
+
let wsCount = 0;
|
|
1719
1799
|
for (const client of blobyWss.clients) {
|
|
1720
|
-
if (client.readyState === WebSocket.OPEN) client.send(msg);
|
|
1800
|
+
if (client.readyState === WebSocket.OPEN) { client.send(msg); wsCount++; }
|
|
1721
1801
|
}
|
|
1802
|
+
let sseCount = 0;
|
|
1722
1803
|
for (const sub of chatSubscribers) {
|
|
1723
|
-
try { sub.send(type, data); } catch {
|
|
1804
|
+
try { sub.send(type, data); sseCount++; } catch (e: any) {
|
|
1805
|
+
console.log(`[sse-broadcast] send failed sub=${sub.id}: ${e.message}`);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
if (type.startsWith('bot:') || type.startsWith('chat:')) {
|
|
1809
|
+
console.log(`[sse-broadcast] type=${type} ws=${wsCount} sse=${sseCount} subs=${chatSubscribers.size}`);
|
|
1724
1810
|
}
|
|
1725
1811
|
}
|
|
1726
1812
|
|