openclaw-navigator 5.7.4 → 5.7.6

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 +55 -72
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -1193,12 +1193,31 @@ function handleRequest(req, res) {
1193
1193
 
1194
1194
  // 3. Background: relay to BFF (port 4000) /api/chat for AI processing
1195
1195
  // Format: OpenAI-compatible chat completions with message history
1196
- const chatHistory = session.messages.map((m) => ({
1197
- role: m.role,
1198
- content: m.content,
1199
- }));
1196
+ // Sanitize: BFF expects alternating user/assistant roles, starting with user.
1197
+ // Drop orphaned assistant messages at the start (from web UI SSE tap).
1198
+ let chatHistory = session.messages
1199
+ .map((m) => ({ role: m.role, content: m.content }))
1200
+ .filter((m) => m.role === "user" || m.role === "assistant" || m.role === "system");
1201
+ // Ensure first non-system message is 'user' — drop leading assistant messages
1202
+ while (chatHistory.length > 0 && chatHistory[0].role === "assistant") {
1203
+ chatHistory.shift();
1204
+ }
1205
+ // Collapse consecutive same-role messages (merge them)
1206
+ chatHistory = chatHistory.reduce((acc, msg) => {
1207
+ if (acc.length > 0 && acc[acc.length - 1].role === msg.role) {
1208
+ acc[acc.length - 1].content += "\n" + msg.content;
1209
+ } else {
1210
+ acc.push({ ...msg });
1211
+ }
1212
+ return acc;
1213
+ }, []);
1214
+ // Must have at least one user message
1215
+ if (chatHistory.length === 0 || chatHistory[0].role !== "user") {
1216
+ chatHistory.unshift({ role: "user", content: message });
1217
+ }
1200
1218
  const proxyBody = JSON.stringify({
1201
- messages: chatHistory,
1219
+ message: message, // BFF expects "message" (singular) — the current user text
1220
+ messages: chatHistory, // Also send full history for OpenAI-compatible endpoints
1202
1221
  stream: true,
1203
1222
  });
1204
1223
  // Build cookie header: prefer captured BFF cookies, fall back to browser's cookies
@@ -1217,6 +1236,7 @@ function handleRequest(req, res) {
1217
1236
  };
1218
1237
 
1219
1238
  console.log(` ${DIM}→ Relaying to BFF /api/chat (port ${ocUIPort}) with ${chatHistory.length} messages${relayCookie ? " +cookie" : " NO-COOKIE"}${RESET}`);
1239
+ console.log(` ${DIM} Body: ${proxyBody.substring(0, 300)}${proxyBody.length > 300 ? "..." : ""}${RESET}`);
1220
1240
 
1221
1241
  // Helper: send the relay request (with optional retry after login)
1222
1242
  function sendBFFRelay(opts, body, retryCount = 0) {
@@ -1341,7 +1361,15 @@ function handleRequest(req, res) {
1341
1361
  if (!isSSE && sseBuffer) {
1342
1362
  try {
1343
1363
  const jsonBody = JSON.parse(sseBuffer);
1344
- fullText = jsonBody.choices?.[0]?.message?.content || jsonBody.response || jsonBody.message || jsonBody.text || "";
1364
+ // Check for error response (BFF returns 400/500 with error details)
1365
+ if (proxyRes.statusCode >= 400) {
1366
+ const errMsg = jsonBody.error?.message || jsonBody.error || jsonBody.message || JSON.stringify(jsonBody);
1367
+ console.log(` ${RED}✗${RESET} BFF error ${proxyRes.statusCode}: ${String(errMsg).substring(0, 300)}`);
1368
+ // Send error to Navigator so user sees it
1369
+ fullText = `⚠️ Error from AI: ${String(errMsg).substring(0, 200)}`;
1370
+ } else {
1371
+ fullText = jsonBody.choices?.[0]?.message?.content || jsonBody.response || jsonBody.message || jsonBody.text || "";
1372
+ }
1345
1373
  } catch {
1346
1374
  console.log(` ${DIM}BFF returned non-JSON (${sseBuffer.length} bytes): ${sseBuffer.substring(0, 200)}${RESET}`);
1347
1375
  }
@@ -1359,7 +1387,7 @@ function handleRequest(req, res) {
1359
1387
  });
1360
1388
  console.log(` ${GREEN}✓${RESET} AI response (${fullText.length} chars): ${fullText.substring(0, 80)}...`);
1361
1389
  } else {
1362
- console.log(` ${DIM}BFF returned no content${RESET}`);
1390
+ console.log(` ${DIM}BFF returned no content (status ${proxyRes.statusCode})${RESET}`);
1363
1391
  }
1364
1392
  });
1365
1393
 
@@ -1585,78 +1613,33 @@ function handleRequest(req, res) {
1585
1613
  return;
1586
1614
  }
1587
1615
 
1588
- // ── SSE→JSON for streaming endpoints (/api/chat) ──────────────────
1589
- // The BFF returns SSE but the web UI's parser can't handle SSE through
1590
- // a reverse proxy (it does JSON.parse on raw chunks without stripping
1591
- // the "data: " prefix). So we collect the full SSE stream, extract the
1592
- // text, and return a single JSON response. Also store + broadcast via WS.
1616
+ // ── SSE passthrough for streaming endpoints (/api/chat) ────────────
1617
+ // Pass SSE through as-is the web UI frontend handles SSE natively.
1618
+ // Disable response buffering so each SSE event reaches the client immediately.
1593
1619
  if (isSSE && isStreamingEndpoint) {
1594
- console.log(` ${DIM}SSE→JSON collect: ${path}${RESET}`);
1620
+ console.log(` ${DIM}SSE passthrough: ${path}${RESET}`);
1595
1621
 
1596
- let fullText = "";
1597
- let sseData = "";
1622
+ // Ensure SSE headers are clean for the browser
1623
+ headers["cache-control"] = "no-cache";
1624
+ headers["connection"] = "keep-alive";
1625
+ // Keep content-type as text/event-stream
1626
+ delete headers["content-length"]; // SSE is chunked
1598
1627
 
1599
- proxyRes.setEncoding("utf-8");
1628
+ res.writeHead(proxyRes.statusCode ?? 200, headers);
1629
+
1630
+ // Disable buffering at every layer
1631
+ if (res.socket) res.socket.setNoDelay(true);
1632
+
1633
+ // Pipe SSE events directly — no transformation
1600
1634
  proxyRes.on("data", (chunk) => {
1601
- sseData += chunk;
1635
+ res.write(chunk);
1636
+ // Flush after each chunk to prevent TCP buffering
1637
+ if (typeof res.flush === "function") res.flush();
1602
1638
  });
1603
1639
 
1604
1640
  proxyRes.on("end", () => {
1605
- // Extract text from all SSE events
1606
- for (const line of sseData.split("\n")) {
1607
- if (line.startsWith("data: ")) {
1608
- const raw = line.slice(6).trim();
1609
- if (raw === "[DONE]" || !raw) continue;
1610
- try {
1611
- const evt = JSON.parse(raw);
1612
- const delta =
1613
- evt.choices?.[0]?.delta?.content ||
1614
- evt.delta?.text ||
1615
- evt.text ||
1616
- evt.content ||
1617
- "";
1618
- if (delta) fullText += delta;
1619
- } catch {
1620
- if (raw) fullText += raw;
1621
- }
1622
- }
1623
- }
1624
-
1625
- // Return as a single OpenAI-compatible JSON response
1626
- const result = {
1627
- id: "chatcmpl_bridge_" + Date.now(),
1628
- object: "chat.completion",
1629
- created: Math.floor(Date.now() / 1000),
1630
- choices: [{
1631
- index: 0,
1632
- message: { role: "assistant", content: fullText },
1633
- finish_reason: "stop",
1634
- }],
1635
- };
1636
-
1637
- headers["content-type"] = "application/json";
1638
- delete headers["content-length"];
1639
- delete headers["transfer-encoding"];
1640
- res.writeHead(200, headers);
1641
- res.end(JSON.stringify(result));
1642
-
1643
- // Store in bridge session + broadcast via WebSocket
1644
- if (fullText) {
1645
- const session = getChatSession("main");
1646
- session.messages.push({ role: "assistant", content: fullText, timestamp: Date.now() });
1647
-
1648
- broadcastToWS({
1649
- type: "chat.final",
1650
- text: fullText,
1651
- content: fullText,
1652
- sessionKey: "main",
1653
- role: "assistant",
1654
- timestamp: Date.now(),
1655
- });
1656
- console.log(` ${GREEN}✓${RESET} Chat response (${fullText.length} chars): ${fullText.substring(0, 80)}...`);
1657
- } else {
1658
- console.log(` ${DIM}SSE stream ended with no content${RESET}`);
1659
- }
1641
+ res.end();
1642
+ console.log(` ${GREEN}✓${RESET} SSE stream completed for ${path}`);
1660
1643
  });
1661
1644
 
1662
1645
  proxyRes.on("error", (err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.7.4",
3
+ "version": "5.7.6",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",