openclaw-navigator 5.7.0 → 5.7.1

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.
Files changed (2) hide show
  1. package/cli.mjs +46 -63
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -1501,78 +1501,62 @@ function handleRequest(req, res) {
1501
1501
  return;
1502
1502
  }
1503
1503
 
1504
- // ── SSE passthrough for streaming endpoints (/api/chat) ──────────
1505
- // Pipe SSE through transparently. Use setNoDelay to prevent TCP buffering
1506
- // that would cause the browser to receive multiple SSE events in one chunk.
1507
- // Also tap the stream to broadcast via WebSocket (for Navigator sidepane)
1508
- // and store the response in the bridge chat session.
1504
+ // ── SSE→JSON for streaming endpoints (/api/chat) ──────────────────
1505
+ // The BFF returns SSE but the web UI's parser can't handle SSE through
1506
+ // a reverse proxy (it does JSON.parse on raw chunks without stripping
1507
+ // the "data: " prefix). So we collect the full SSE stream, extract the
1508
+ // text, and return a single JSON response. Also store + broadcast via WS.
1509
1509
  if (isSSE && isStreamingEndpoint) {
1510
- console.log(` ${DIM}SSE passthrough: ${path}${RESET}`);
1511
-
1512
- // Disable TCP Nagle for immediate per-event delivery
1513
- if (res.socket) res.socket.setNoDelay(true);
1514
-
1515
- // Clean transfer headers — let Node.js manage chunked encoding
1516
- delete headers["content-length"];
1517
- delete headers["transfer-encoding"];
1518
- res.writeHead(proxyRes.statusCode ?? 200, headers);
1510
+ console.log(` ${DIM}SSE→JSON collect: ${path}${RESET}`);
1519
1511
 
1520
1512
  let fullText = "";
1521
- let sseBuffer = "";
1513
+ let sseData = "";
1522
1514
 
1523
1515
  proxyRes.setEncoding("utf-8");
1524
1516
  proxyRes.on("data", (chunk) => {
1525
- sseBuffer += chunk;
1526
-
1527
- // Split by double-newline (SSE event boundary)
1528
- const events = sseBuffer.split("\n\n");
1529
- sseBuffer = events.pop() || ""; // Keep incomplete event in buffer
1530
-
1531
- for (const event of events) {
1532
- const trimmed = event.trim();
1533
- if (!trimmed) continue;
1534
-
1535
- // Forward the COMPLETE SSE event to browser (preserving data: prefix)
1536
- res.write(trimmed + "\n\n");
1537
-
1538
- // Tap for WebSocket broadcast
1539
- for (const line of trimmed.split("\n")) {
1540
- if (line.startsWith("data: ")) {
1541
- const raw = line.slice(6).trim();
1542
- if (raw === "[DONE]" || !raw) continue;
1543
- try {
1544
- const evt = JSON.parse(raw);
1545
- const delta =
1546
- evt.choices?.[0]?.delta?.content ||
1547
- evt.delta?.text ||
1548
- evt.text ||
1549
- evt.content ||
1550
- "";
1551
- if (delta) {
1552
- fullText += delta;
1553
- broadcastToWS({
1554
- type: "chat.delta",
1555
- text: fullText,
1556
- delta,
1557
- sessionKey: "main",
1558
- timestamp: Date.now(),
1559
- });
1560
- }
1561
- } catch {
1562
- /* non-JSON SSE event */
1563
- }
1564
- }
1565
- }
1566
- }
1517
+ sseData += chunk;
1567
1518
  });
1568
1519
 
1569
1520
  proxyRes.on("end", () => {
1570
- // Flush remaining buffer
1571
- if (sseBuffer.trim()) {
1572
- res.write(sseBuffer + "\n\n");
1521
+ // Extract text from all SSE events
1522
+ for (const line of sseData.split("\n")) {
1523
+ if (line.startsWith("data: ")) {
1524
+ const raw = line.slice(6).trim();
1525
+ if (raw === "[DONE]" || !raw) continue;
1526
+ try {
1527
+ const evt = JSON.parse(raw);
1528
+ const delta =
1529
+ evt.choices?.[0]?.delta?.content ||
1530
+ evt.delta?.text ||
1531
+ evt.text ||
1532
+ evt.content ||
1533
+ "";
1534
+ if (delta) fullText += delta;
1535
+ } catch {
1536
+ if (raw) fullText += raw;
1537
+ }
1538
+ }
1573
1539
  }
1574
1540
 
1575
- // Store and broadcast final response
1541
+ // Return as a single OpenAI-compatible JSON response
1542
+ const result = {
1543
+ id: "chatcmpl_bridge_" + Date.now(),
1544
+ object: "chat.completion",
1545
+ created: Math.floor(Date.now() / 1000),
1546
+ choices: [{
1547
+ index: 0,
1548
+ message: { role: "assistant", content: fullText },
1549
+ finish_reason: "stop",
1550
+ }],
1551
+ };
1552
+
1553
+ headers["content-type"] = "application/json";
1554
+ delete headers["content-length"];
1555
+ delete headers["transfer-encoding"];
1556
+ res.writeHead(200, headers);
1557
+ res.end(JSON.stringify(result));
1558
+
1559
+ // Store in bridge session + broadcast via WebSocket
1576
1560
  if (fullText) {
1577
1561
  const session = getChatSession("main");
1578
1562
  session.messages.push({ role: "assistant", content: fullText, timestamp: Date.now() });
@@ -1589,12 +1573,11 @@ function handleRequest(req, res) {
1589
1573
  } else {
1590
1574
  console.log(` ${DIM}SSE stream ended with no content${RESET}`);
1591
1575
  }
1592
- res.end();
1593
1576
  });
1594
1577
 
1595
1578
  proxyRes.on("error", (err) => {
1596
1579
  console.log(` ${DIM}SSE stream error: ${err.message}${RESET}`);
1597
- res.end();
1580
+ sendJSON(res, 502, { ok: false, error: "Stream error" });
1598
1581
  });
1599
1582
  return;
1600
1583
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.7.0",
3
+ "version": "5.7.1",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",