openclaw-navigator 5.6.4 → 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 +138 -135
- 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,48 +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
|
-
console.log(` ${DIM}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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() || "";
|
|
1203
1215
|
for (const line of lines) {
|
|
1204
1216
|
if (line.startsWith("data: ")) {
|
|
1205
1217
|
const raw = line.slice(6).trim();
|
|
1206
|
-
if (raw === "[DONE]") continue;
|
|
1218
|
+
if (raw === "[DONE]" || !raw) continue;
|
|
1207
1219
|
try {
|
|
1208
1220
|
const evt = JSON.parse(raw);
|
|
1209
|
-
const
|
|
1210
|
-
evt.
|
|
1211
|
-
|
|
1212
|
-
|
|
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;
|
|
1213
1229
|
broadcastToWS({
|
|
1214
1230
|
type: "chat.delta",
|
|
1215
1231
|
text: fullText,
|
|
1216
|
-
delta
|
|
1232
|
+
delta,
|
|
1217
1233
|
sessionKey,
|
|
1218
1234
|
timestamp: Date.now(),
|
|
1219
1235
|
});
|
|
@@ -1223,72 +1239,64 @@ function handleRequest(req, res) {
|
|
|
1223
1239
|
}
|
|
1224
1240
|
}
|
|
1225
1241
|
}
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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; }
|
|
1236
1259
|
}
|
|
1237
1260
|
}
|
|
1238
1261
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
text: fullText,
|
|
1244
|
-
content: fullText,
|
|
1245
|
-
sessionKey,
|
|
1246
|
-
role: "assistant",
|
|
1247
|
-
timestamp: Date.now(),
|
|
1248
|
-
});
|
|
1249
|
-
console.log(` ${GREEN}✓${RESET} Gateway SSE response: ${fullText.substring(0, 80)}...`);
|
|
1250
|
-
}
|
|
1251
|
-
});
|
|
1252
|
-
proxyRes.on("error", () => {});
|
|
1253
|
-
} else {
|
|
1254
|
-
// JSON response from gateway
|
|
1255
|
-
const chunks = [];
|
|
1256
|
-
proxyRes.on("data", (c) => chunks.push(c));
|
|
1257
|
-
proxyRes.on("end", () => {
|
|
1258
|
-
const body = Buffer.concat(chunks).toString("utf-8");
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Handle JSON response
|
|
1265
|
+
if (!isSSE && sseBuffer) {
|
|
1259
1266
|
try {
|
|
1260
|
-
const jsonBody = JSON.parse(
|
|
1261
|
-
|
|
1262
|
-
jsonBody.response || jsonBody.message || jsonBody.answer || jsonBody.text || "";
|
|
1263
|
-
if (inlineResponse) {
|
|
1264
|
-
session.messages.push({ role: "assistant", content: inlineResponse, timestamp: Date.now() });
|
|
1265
|
-
broadcastToWS({
|
|
1266
|
-
type: "chat.final",
|
|
1267
|
-
text: inlineResponse,
|
|
1268
|
-
content: inlineResponse,
|
|
1269
|
-
sessionKey,
|
|
1270
|
-
role: "assistant",
|
|
1271
|
-
timestamp: Date.now(),
|
|
1272
|
-
});
|
|
1273
|
-
console.log(` ${GREEN}✓${RESET} Gateway JSON response: ${inlineResponse.substring(0, 80)}...`);
|
|
1274
|
-
} else {
|
|
1275
|
-
console.log(` ${DIM}Gateway returned JSON with no response field — waiting for MCP${RESET}`);
|
|
1276
|
-
}
|
|
1267
|
+
const jsonBody = JSON.parse(sseBuffer);
|
|
1268
|
+
fullText = jsonBody.choices?.[0]?.message?.content || jsonBody.response || jsonBody.message || jsonBody.text || "";
|
|
1277
1269
|
} catch {
|
|
1278
|
-
console.log(` ${DIM}
|
|
1279
|
-
console.log(` ${DIM}Waiting for MCP agent to respond...${RESET}`);
|
|
1270
|
+
console.log(` ${DIM}BFF returned non-JSON (${sseBuffer.length} bytes): ${sseBuffer.substring(0, 200)}${RESET}`);
|
|
1280
1271
|
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
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", () => {});
|
|
1283
1291
|
});
|
|
1284
1292
|
|
|
1285
1293
|
proxyReq.on("timeout", () => {
|
|
1286
1294
|
proxyReq.destroy();
|
|
1287
|
-
console.log(` ${DIM}
|
|
1295
|
+
console.log(` ${DIM}BFF relay timed out${RESET}`);
|
|
1288
1296
|
});
|
|
1289
1297
|
|
|
1290
1298
|
proxyReq.on("error", (err) => {
|
|
1291
|
-
console.log(` ${DIM}
|
|
1299
|
+
console.log(` ${DIM}BFF relay failed: ${err.message}${RESET}`);
|
|
1292
1300
|
});
|
|
1293
1301
|
|
|
1294
1302
|
proxyReq.write(proxyBody);
|
|
@@ -1493,16 +1501,18 @@ function handleRequest(req, res) {
|
|
|
1493
1501
|
return;
|
|
1494
1502
|
}
|
|
1495
1503
|
|
|
1496
|
-
// ── SSE
|
|
1497
|
-
//
|
|
1498
|
-
//
|
|
1499
|
-
//
|
|
1500
|
-
//
|
|
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.
|
|
1501
1509
|
if (isSSE && isStreamingEndpoint) {
|
|
1502
|
-
console.log(` ${DIM}SSE
|
|
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);
|
|
1503
1514
|
|
|
1504
|
-
//
|
|
1505
|
-
headers["content-type"] = "text/plain; charset=utf-8";
|
|
1515
|
+
// Clean transfer headers — let Node.js manage chunked encoding
|
|
1506
1516
|
delete headers["content-length"];
|
|
1507
1517
|
delete headers["transfer-encoding"];
|
|
1508
1518
|
res.writeHead(proxyRes.statusCode ?? 200, headers);
|
|
@@ -1513,64 +1523,57 @@ function handleRequest(req, res) {
|
|
|
1513
1523
|
proxyRes.setEncoding("utf-8");
|
|
1514
1524
|
proxyRes.on("data", (chunk) => {
|
|
1515
1525
|
sseBuffer += chunk;
|
|
1516
|
-
const lines = sseBuffer.split("\n");
|
|
1517
|
-
sseBuffer = lines.pop() || ""; // keep incomplete last line
|
|
1518
|
-
|
|
1519
|
-
for (const line of lines) {
|
|
1520
|
-
if (line.startsWith("data: ")) {
|
|
1521
|
-
const raw = line.slice(6).trim();
|
|
1522
|
-
if (raw === "[DONE]" || !raw) {
|
|
1523
|
-
continue;
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
// Forward clean JSON line to browser (no "data: " prefix)
|
|
1527
|
-
res.write(raw + "\n");
|
|
1528
|
-
console.log(` ${DIM} SSE chunk: ${raw.substring(0, 100)}${raw.length > 100 ? "..." : ""}${RESET}`);
|
|
1529
1526
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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 */
|
|
1548
1563
|
}
|
|
1549
|
-
} catch {
|
|
1550
|
-
// Non-JSON SSE data — forward as text anyway
|
|
1551
|
-
fullText += raw;
|
|
1552
1564
|
}
|
|
1553
1565
|
}
|
|
1554
1566
|
}
|
|
1555
1567
|
});
|
|
1556
1568
|
|
|
1557
1569
|
proxyRes.on("end", () => {
|
|
1558
|
-
//
|
|
1559
|
-
if (sseBuffer.
|
|
1560
|
-
|
|
1561
|
-
if (raw && raw !== "[DONE]") {
|
|
1562
|
-
res.write(raw + "\n");
|
|
1563
|
-
try {
|
|
1564
|
-
const evt = JSON.parse(raw);
|
|
1565
|
-
const delta = evt.choices?.[0]?.delta?.content || evt.delta?.text || evt.text || evt.content || "";
|
|
1566
|
-
if (delta) fullText += delta;
|
|
1567
|
-
} catch { fullText += raw; }
|
|
1568
|
-
}
|
|
1570
|
+
// Flush remaining buffer
|
|
1571
|
+
if (sseBuffer.trim()) {
|
|
1572
|
+
res.write(sseBuffer + "\n\n");
|
|
1569
1573
|
}
|
|
1570
1574
|
|
|
1571
|
-
//
|
|
1575
|
+
// Store and broadcast final response
|
|
1572
1576
|
if (fullText) {
|
|
1573
|
-
// Also store as assistant message in bridge chat session
|
|
1574
1577
|
const session = getChatSession("main");
|
|
1575
1578
|
session.messages.push({ role: "assistant", content: fullText, timestamp: Date.now() });
|
|
1576
1579
|
|