openclaw-navigator 5.7.5 → 5.7.7

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 +43 -62
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -1216,7 +1216,8 @@ function handleRequest(req, res) {
1216
1216
  chatHistory.unshift({ role: "user", content: message });
1217
1217
  }
1218
1218
  const proxyBody = JSON.stringify({
1219
- messages: chatHistory,
1219
+ message: message, // BFF expects "message" (singular) — the current user text
1220
+ messages: chatHistory, // Also send full history for OpenAI-compatible endpoints
1220
1221
  stream: true,
1221
1222
  });
1222
1223
  // Build cookie header: prefer captured BFF cookies, fall back to browser's cookies
@@ -1612,82 +1613,62 @@ function handleRequest(req, res) {
1612
1613
  return;
1613
1614
  }
1614
1615
 
1615
- // ── SSE→JSON for streaming endpoints (/api/chat) ──────────────────
1616
- // The BFF returns SSE but the web UI's parser can't handle SSE through
1617
- // a reverse proxy (it does JSON.parse on raw chunks without stripping
1618
- // the "data: " prefix). So we collect the full SSE stream, extract the
1619
- // text, and return a single JSON response. Also store + broadcast via WS.
1616
+ // ── SSE NDJSON for streaming endpoints (/api/chat) ─────────────
1617
+ // The OC web UI frontend does fetch() + JSON.parse() on each line,
1618
+ // NOT proper EventSource parsing. So it chokes on "data: {...}"
1619
+ // (the "data: " prefix isn't valid JSON). We strip the prefix and
1620
+ // forward clean JSON lines (NDJSON), one per write, flushed immediately.
1620
1621
  if (isSSE && isStreamingEndpoint) {
1621
- console.log(` ${DIM}SSE→JSON collect: ${path}${RESET}`);
1622
+ console.log(` ${DIM}SSE→NDJSON: ${path}${RESET}`);
1622
1623
 
1623
- let fullText = "";
1624
- let sseData = "";
1624
+ // Send as NDJSON (application/x-ndjson) so the frontend gets
1625
+ // one clean JSON object per line, no SSE framing.
1626
+ headers["content-type"] = "application/x-ndjson";
1627
+ headers["cache-control"] = "no-cache";
1628
+ headers["connection"] = "keep-alive";
1629
+ delete headers["content-length"];
1630
+ delete headers["transfer-encoding"];
1625
1631
 
1632
+ res.writeHead(200, headers);
1633
+ if (res.socket) res.socket.setNoDelay(true);
1634
+
1635
+ let sseLineBuf = "";
1626
1636
  proxyRes.setEncoding("utf-8");
1627
1637
  proxyRes.on("data", (chunk) => {
1628
- sseData += chunk;
1629
- });
1630
-
1631
- proxyRes.on("end", () => {
1632
- // Extract text from all SSE events
1633
- for (const line of sseData.split("\n")) {
1638
+ sseLineBuf += chunk;
1639
+ // Process complete lines
1640
+ const lines = sseLineBuf.split("\n");
1641
+ sseLineBuf = lines.pop() || ""; // keep incomplete line in buffer
1642
+ for (const line of lines) {
1634
1643
  if (line.startsWith("data: ")) {
1635
- const raw = line.slice(6).trim();
1636
- if (raw === "[DONE]" || !raw) continue;
1637
- try {
1638
- const evt = JSON.parse(raw);
1639
- const delta =
1640
- evt.choices?.[0]?.delta?.content ||
1641
- evt.delta?.text ||
1642
- evt.text ||
1643
- evt.content ||
1644
- "";
1645
- if (delta) fullText += delta;
1646
- } catch {
1647
- if (raw) fullText += raw;
1644
+ const payload = line.slice(6).trim();
1645
+ if (payload === "[DONE]") {
1646
+ res.write("data: [DONE]\n\n");
1647
+ } else if (payload) {
1648
+ // Write clean JSON + newline — one object per write
1649
+ res.write(payload + "\n");
1648
1650
  }
1649
1651
  }
1652
+ // Skip empty lines, "event:", "id:", "retry:" etc.
1650
1653
  }
1654
+ if (typeof res.flush === "function") res.flush();
1655
+ });
1651
1656
 
1652
- // Return as a single OpenAI-compatible JSON response
1653
- const result = {
1654
- id: "chatcmpl_bridge_" + Date.now(),
1655
- object: "chat.completion",
1656
- created: Math.floor(Date.now() / 1000),
1657
- choices: [{
1658
- index: 0,
1659
- message: { role: "assistant", content: fullText },
1660
- finish_reason: "stop",
1661
- }],
1662
- };
1663
-
1664
- headers["content-type"] = "application/json";
1665
- delete headers["content-length"];
1666
- delete headers["transfer-encoding"];
1667
- res.writeHead(200, headers);
1668
- res.end(JSON.stringify(result));
1669
-
1670
- // Broadcast via WebSocket for live updates (but DON'T store in
1671
- // bridge chat session — web UI manages its own conversation state
1672
- // separately from sidepane chat. Mixing them causes 400 errors from
1673
- // the BFF due to malformed message history.)
1674
- if (fullText) {
1675
- broadcastToWS({
1676
- type: "chat.webui",
1677
- text: fullText,
1678
- content: fullText,
1679
- role: "assistant",
1680
- timestamp: Date.now(),
1681
- });
1682
- console.log(` ${GREEN}✓${RESET} Chat response (${fullText.length} chars): ${fullText.substring(0, 80)}...`);
1683
- } else {
1684
- console.log(` ${DIM}SSE stream ended with no content${RESET}`);
1657
+ proxyRes.on("end", () => {
1658
+ // Flush remaining buffer
1659
+ if (sseLineBuf.startsWith("data: ")) {
1660
+ const payload = sseLineBuf.slice(6).trim();
1661
+ if (payload && payload !== "[DONE]") {
1662
+ res.write(payload + "\n");
1663
+ }
1685
1664
  }
1665
+ res.end();
1666
+ console.log(` ${GREEN}✓${RESET} SSE→NDJSON stream completed for ${path}`);
1686
1667
  });
1687
1668
 
1688
1669
  proxyRes.on("error", (err) => {
1689
1670
  console.log(` ${DIM}SSE stream error: ${err.message}${RESET}`);
1690
- sendJSON(res, 502, { ok: false, error: "Stream error" });
1671
+ res.end();
1691
1672
  });
1692
1673
  return;
1693
1674
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.7.5",
3
+ "version": "5.7.7",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",