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.
- package/cli.mjs +55 -72
- 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
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1589
|
-
//
|
|
1590
|
-
//
|
|
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
|
|
1620
|
+
console.log(` ${DIM}SSE passthrough: ${path}${RESET}`);
|
|
1595
1621
|
|
|
1596
|
-
|
|
1597
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1606
|
-
|
|
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) => {
|