openclaw-navigator 5.6.3 → 5.7.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/cli.mjs +155 -113
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -1137,11 +1137,12 @@ function handleRequest(req, res) {
|
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
1139
1139
|
// ── Chat send: store locally + respond immediately ─────────────────────
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1143
|
-
//
|
|
1144
|
-
//
|
|
1140
|
+
// ── Chat send: Navigator sidepane sends a message ─────────────────────
|
|
1141
|
+
// 1. Store user message in bridge memory
|
|
1142
|
+
// 2. Respond to Navigator immediately
|
|
1143
|
+
// 3. In background: relay to the BFF on port 4000 (/api/chat) for AI processing
|
|
1144
|
+
// The AI lives behind the BFF, NOT on the gateway (port 18789 returns 405).
|
|
1145
|
+
// The BFF returns SSE — collect it, store the response, broadcast via WS.
|
|
1145
1146
|
if (path === "/api/sessions/send" && req.method === "POST") {
|
|
1146
1147
|
readBody(req)
|
|
1147
1148
|
.then((bodyStr) => {
|
|
@@ -1172,47 +1173,63 @@ function handleRequest(req, res) {
|
|
|
1172
1173
|
// 2. Respond immediately — don't block Navigator
|
|
1173
1174
|
sendJSON(res, 200, { ok: true, stored: true, messageCount: session.messages.length });
|
|
1174
1175
|
|
|
1175
|
-
// 3. Background: relay to
|
|
1176
|
-
|
|
1176
|
+
// 3. Background: relay to BFF (port 4000) /api/chat for AI processing
|
|
1177
|
+
// Format: OpenAI-compatible chat completions with message history
|
|
1178
|
+
const chatHistory = session.messages.map((m) => ({
|
|
1179
|
+
role: m.role,
|
|
1180
|
+
content: m.content,
|
|
1181
|
+
}));
|
|
1182
|
+
const proxyBody = JSON.stringify({
|
|
1183
|
+
messages: chatHistory,
|
|
1184
|
+
stream: true,
|
|
1185
|
+
});
|
|
1177
1186
|
const proxyOpts = {
|
|
1178
1187
|
hostname: "127.0.0.1",
|
|
1179
|
-
port:
|
|
1180
|
-
path: `/api/
|
|
1188
|
+
port: ocUIPort,
|
|
1189
|
+
path: `/api/chat`,
|
|
1181
1190
|
method: "POST",
|
|
1182
|
-
timeout:
|
|
1191
|
+
timeout: 120000, // 2 min — agent may take a while
|
|
1183
1192
|
headers: {
|
|
1184
1193
|
"content-type": "application/json",
|
|
1185
1194
|
"content-length": Buffer.byteLength(proxyBody),
|
|
1186
1195
|
},
|
|
1187
1196
|
};
|
|
1188
1197
|
|
|
1198
|
+
console.log(` ${DIM}→ Relaying to BFF /api/chat (port ${ocUIPort}) with ${chatHistory.length} messages${RESET}`);
|
|
1199
|
+
|
|
1189
1200
|
const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
|
|
1190
1201
|
const contentType = (proxyRes.headers["content-type"] || "").toLowerCase();
|
|
1191
1202
|
const isSSE = contentType.includes("text/event-stream");
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1203
|
+
console.log(` ${DIM}← BFF response: ${proxyRes.statusCode} ${contentType || "no-content-type"}${RESET}`);
|
|
1204
|
+
|
|
1205
|
+
// Collect the response (SSE or JSON) and extract the assistant's text
|
|
1206
|
+
let fullText = "";
|
|
1207
|
+
let sseBuffer = "";
|
|
1208
|
+
|
|
1209
|
+
proxyRes.setEncoding("utf-8");
|
|
1210
|
+
proxyRes.on("data", (chunk) => {
|
|
1211
|
+
if (isSSE) {
|
|
1212
|
+
sseBuffer += chunk;
|
|
1213
|
+
const lines = sseBuffer.split("\n");
|
|
1214
|
+
sseBuffer = lines.pop() || "";
|
|
1202
1215
|
for (const line of lines) {
|
|
1203
1216
|
if (line.startsWith("data: ")) {
|
|
1204
1217
|
const raw = line.slice(6).trim();
|
|
1205
|
-
if (raw === "[DONE]") continue;
|
|
1218
|
+
if (raw === "[DONE]" || !raw) continue;
|
|
1206
1219
|
try {
|
|
1207
1220
|
const evt = JSON.parse(raw);
|
|
1208
|
-
const
|
|
1209
|
-
evt.
|
|
1210
|
-
|
|
1211
|
-
|
|
1221
|
+
const delta =
|
|
1222
|
+
evt.choices?.[0]?.delta?.content ||
|
|
1223
|
+
evt.delta?.text ||
|
|
1224
|
+
evt.text ||
|
|
1225
|
+
evt.content ||
|
|
1226
|
+
"";
|
|
1227
|
+
if (delta) {
|
|
1228
|
+
fullText += delta;
|
|
1212
1229
|
broadcastToWS({
|
|
1213
1230
|
type: "chat.delta",
|
|
1214
1231
|
text: fullText,
|
|
1215
|
-
delta
|
|
1232
|
+
delta,
|
|
1216
1233
|
sessionKey,
|
|
1217
1234
|
timestamp: Date.now(),
|
|
1218
1235
|
});
|
|
@@ -1222,71 +1239,64 @@ function handleRequest(req, res) {
|
|
|
1222
1239
|
}
|
|
1223
1240
|
}
|
|
1224
1241
|
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1242
|
+
} else {
|
|
1243
|
+
sseBuffer += chunk; // Collect JSON body
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
proxyRes.on("end", () => {
|
|
1248
|
+
// Handle remaining buffer for SSE
|
|
1249
|
+
if (isSSE && sseBuffer) {
|
|
1250
|
+
for (const line of sseBuffer.split("\n")) {
|
|
1251
|
+
if (line.startsWith("data: ")) {
|
|
1252
|
+
const raw = line.slice(6).trim();
|
|
1253
|
+
if (raw && raw !== "[DONE]") {
|
|
1254
|
+
try {
|
|
1255
|
+
const evt = JSON.parse(raw);
|
|
1256
|
+
const delta = evt.choices?.[0]?.delta?.content || evt.delta?.text || evt.text || evt.content || "";
|
|
1257
|
+
if (delta) fullText += delta;
|
|
1258
|
+
} catch { fullText += raw; }
|
|
1235
1259
|
}
|
|
1236
1260
|
}
|
|
1237
1261
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
text: fullText,
|
|
1243
|
-
content: fullText,
|
|
1244
|
-
sessionKey,
|
|
1245
|
-
role: "assistant",
|
|
1246
|
-
timestamp: Date.now(),
|
|
1247
|
-
});
|
|
1248
|
-
console.log(` ${GREEN}✓${RESET} Gateway SSE response: ${fullText.substring(0, 80)}...`);
|
|
1249
|
-
}
|
|
1250
|
-
});
|
|
1251
|
-
proxyRes.on("error", () => {});
|
|
1252
|
-
} else {
|
|
1253
|
-
// JSON response from gateway
|
|
1254
|
-
const chunks = [];
|
|
1255
|
-
proxyRes.on("data", (c) => chunks.push(c));
|
|
1256
|
-
proxyRes.on("end", () => {
|
|
1257
|
-
const body = Buffer.concat(chunks).toString("utf-8");
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Handle JSON response
|
|
1265
|
+
if (!isSSE && sseBuffer) {
|
|
1258
1266
|
try {
|
|
1259
|
-
const jsonBody = JSON.parse(
|
|
1260
|
-
|
|
1261
|
-
jsonBody.response || jsonBody.message || jsonBody.answer || jsonBody.text || "";
|
|
1262
|
-
if (inlineResponse) {
|
|
1263
|
-
session.messages.push({ role: "assistant", content: inlineResponse, timestamp: Date.now() });
|
|
1264
|
-
broadcastToWS({
|
|
1265
|
-
type: "chat.final",
|
|
1266
|
-
text: inlineResponse,
|
|
1267
|
-
content: inlineResponse,
|
|
1268
|
-
sessionKey,
|
|
1269
|
-
role: "assistant",
|
|
1270
|
-
timestamp: Date.now(),
|
|
1271
|
-
});
|
|
1272
|
-
console.log(` ${GREEN}✓${RESET} Gateway JSON response: ${inlineResponse.substring(0, 80)}...`);
|
|
1273
|
-
} else {
|
|
1274
|
-
console.log(` ${DIM}Gateway returned JSON with no response field — waiting for MCP${RESET}`);
|
|
1275
|
-
}
|
|
1267
|
+
const jsonBody = JSON.parse(sseBuffer);
|
|
1268
|
+
fullText = jsonBody.choices?.[0]?.message?.content || jsonBody.response || jsonBody.message || jsonBody.text || "";
|
|
1276
1269
|
} catch {
|
|
1277
|
-
console.log(` ${DIM}
|
|
1270
|
+
console.log(` ${DIM}BFF returned non-JSON (${sseBuffer.length} bytes): ${sseBuffer.substring(0, 200)}${RESET}`);
|
|
1278
1271
|
}
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (fullText) {
|
|
1275
|
+
session.messages.push({ role: "assistant", content: fullText, timestamp: Date.now() });
|
|
1276
|
+
broadcastToWS({
|
|
1277
|
+
type: "chat.final",
|
|
1278
|
+
text: fullText,
|
|
1279
|
+
content: fullText,
|
|
1280
|
+
sessionKey,
|
|
1281
|
+
role: "assistant",
|
|
1282
|
+
timestamp: Date.now(),
|
|
1283
|
+
});
|
|
1284
|
+
console.log(` ${GREEN}✓${RESET} AI response (${fullText.length} chars): ${fullText.substring(0, 80)}...`);
|
|
1285
|
+
} else {
|
|
1286
|
+
console.log(` ${DIM}BFF returned no content${RESET}`);
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
proxyRes.on("error", () => {});
|
|
1281
1291
|
});
|
|
1282
1292
|
|
|
1283
1293
|
proxyReq.on("timeout", () => {
|
|
1284
1294
|
proxyReq.destroy();
|
|
1285
|
-
console.log(` ${DIM}
|
|
1295
|
+
console.log(` ${DIM}BFF relay timed out${RESET}`);
|
|
1286
1296
|
});
|
|
1287
1297
|
|
|
1288
1298
|
proxyReq.on("error", (err) => {
|
|
1289
|
-
console.log(` ${DIM}
|
|
1299
|
+
console.log(` ${DIM}BFF relay failed: ${err.message}${RESET}`);
|
|
1290
1300
|
});
|
|
1291
1301
|
|
|
1292
1302
|
proxyReq.write(proxyBody);
|
|
@@ -1492,53 +1502,81 @@ function handleRequest(req, res) {
|
|
|
1492
1502
|
}
|
|
1493
1503
|
|
|
1494
1504
|
// ── SSE passthrough for streaming endpoints (/api/chat) ──────────
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
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.
|
|
1497
1509
|
if (isSSE && isStreamingEndpoint) {
|
|
1498
|
-
console.log(` ${DIM}SSE passthrough
|
|
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"];
|
|
1499
1518
|
res.writeHead(proxyRes.statusCode ?? 200, headers);
|
|
1500
1519
|
|
|
1501
1520
|
let fullText = "";
|
|
1502
|
-
|
|
1503
|
-
// Forward the chunk to the browser immediately
|
|
1504
|
-
res.write(chunk);
|
|
1521
|
+
let sseBuffer = "";
|
|
1505
1522
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1523
|
+
proxyRes.setEncoding("utf-8");
|
|
1524
|
+
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 */
|
|
1531
1563
|
}
|
|
1532
|
-
} catch {
|
|
1533
|
-
/* non-JSON SSE event — skip */
|
|
1534
1564
|
}
|
|
1535
1565
|
}
|
|
1536
1566
|
}
|
|
1537
1567
|
});
|
|
1538
1568
|
|
|
1539
1569
|
proxyRes.on("end", () => {
|
|
1540
|
-
//
|
|
1570
|
+
// Flush remaining buffer
|
|
1571
|
+
if (sseBuffer.trim()) {
|
|
1572
|
+
res.write(sseBuffer + "\n\n");
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Store and broadcast final response
|
|
1541
1576
|
if (fullText) {
|
|
1577
|
+
const session = getChatSession("main");
|
|
1578
|
+
session.messages.push({ role: "assistant", content: fullText, timestamp: Date.now() });
|
|
1579
|
+
|
|
1542
1580
|
broadcastToWS({
|
|
1543
1581
|
type: "chat.final",
|
|
1544
1582
|
text: fullText,
|
|
@@ -1547,11 +1585,15 @@ function handleRequest(req, res) {
|
|
|
1547
1585
|
role: "assistant",
|
|
1548
1586
|
timestamp: Date.now(),
|
|
1549
1587
|
});
|
|
1588
|
+
console.log(` ${GREEN}✓${RESET} Chat response (${fullText.length} chars): ${fullText.substring(0, 80)}...`);
|
|
1589
|
+
} else {
|
|
1590
|
+
console.log(` ${DIM}SSE stream ended with no content${RESET}`);
|
|
1550
1591
|
}
|
|
1551
1592
|
res.end();
|
|
1552
1593
|
});
|
|
1553
1594
|
|
|
1554
|
-
proxyRes.on("error", () => {
|
|
1595
|
+
proxyRes.on("error", (err) => {
|
|
1596
|
+
console.log(` ${DIM}SSE stream error: ${err.message}${RESET}`);
|
|
1555
1597
|
res.end();
|
|
1556
1598
|
});
|
|
1557
1599
|
return;
|