openclaw-navigator 5.7.0 → 5.7.2
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 +133 -66
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -375,6 +375,22 @@ function getChatSession(sessionKey = "main") {
|
|
|
375
375
|
return chatSessions.get(sessionKey);
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
// ── Cookie jar for BFF auth ────────────────────────────────────────────
|
|
379
|
+
// Captures session cookies from web UI /api/auth responses so the bridge
|
|
380
|
+
// can make authenticated server-side requests to /api/chat (for sidepane relay).
|
|
381
|
+
let bffCookieJar = "";
|
|
382
|
+
|
|
383
|
+
function captureBFFCookies(setCookieHeaders) {
|
|
384
|
+
if (!setCookieHeaders) return;
|
|
385
|
+
const cookies = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders];
|
|
386
|
+
// Extract just the cookie name=value (strip attributes like path, domain, etc.)
|
|
387
|
+
const extracted = cookies.map((c) => c.split(";")[0].trim()).filter(Boolean);
|
|
388
|
+
if (extracted.length > 0) {
|
|
389
|
+
bffCookieJar = extracted.join("; ");
|
|
390
|
+
console.log(` ${DIM}Captured BFF cookies: ${bffCookieJar.substring(0, 60)}...${RESET}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
378
394
|
// ── WebSocket server for chat (minimal, no dependencies) ────────────────
|
|
379
395
|
// Tracks connected WebSocket clients. When the OC agent pushes messages
|
|
380
396
|
// via /api/sessions/respond or /api/sessions/stream, we broadcast to all
|
|
@@ -1048,6 +1064,8 @@ function handleRequest(req, res) {
|
|
|
1048
1064
|
headers["set-cookie"] = cookies.map((c) =>
|
|
1049
1065
|
c.replace(/;\s*domain=[^;]*/gi, "").replace(/;\s*secure/gi, ""),
|
|
1050
1066
|
);
|
|
1067
|
+
// Also capture these for server-side relay auth (sidepane chat)
|
|
1068
|
+
captureBFFCookies(proxyRes.headers["set-cookie"]);
|
|
1051
1069
|
}
|
|
1052
1070
|
|
|
1053
1071
|
res.writeHead(proxyRes.statusCode ?? 502, headers);
|
|
@@ -1183,6 +1201,8 @@ function handleRequest(req, res) {
|
|
|
1183
1201
|
messages: chatHistory,
|
|
1184
1202
|
stream: true,
|
|
1185
1203
|
});
|
|
1204
|
+
// Build cookie header: prefer captured BFF cookies, fall back to browser's cookies
|
|
1205
|
+
const relayCookie = bffCookieJar || req.headers.cookie || "";
|
|
1186
1206
|
const proxyOpts = {
|
|
1187
1207
|
hostname: "127.0.0.1",
|
|
1188
1208
|
port: ocUIPort,
|
|
@@ -1192,12 +1212,68 @@ function handleRequest(req, res) {
|
|
|
1192
1212
|
headers: {
|
|
1193
1213
|
"content-type": "application/json",
|
|
1194
1214
|
"content-length": Buffer.byteLength(proxyBody),
|
|
1215
|
+
...(relayCookie ? { cookie: relayCookie } : {}),
|
|
1195
1216
|
},
|
|
1196
1217
|
};
|
|
1197
1218
|
|
|
1198
|
-
console.log(` ${DIM}→ Relaying to BFF /api/chat (port ${ocUIPort}) with ${chatHistory.length} messages${RESET}`);
|
|
1219
|
+
console.log(` ${DIM}→ Relaying to BFF /api/chat (port ${ocUIPort}) with ${chatHistory.length} messages${relayCookie ? " +cookie" : " NO-COOKIE"}${RESET}`);
|
|
1220
|
+
|
|
1221
|
+
// Helper: send the relay request (with optional retry after login)
|
|
1222
|
+
function sendBFFRelay(opts, body, retryCount = 0) {
|
|
1223
|
+
const proxyReq = httpRequest(opts, (proxyRes) => {
|
|
1224
|
+
// Capture any cookies the BFF sends (login session, etc.)
|
|
1225
|
+
if (proxyRes.headers["set-cookie"]) {
|
|
1226
|
+
captureBFFCookies(proxyRes.headers["set-cookie"]);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// If redirect (307/302 to /login) — follow it to seed cookies, then retry
|
|
1230
|
+
if ((proxyRes.statusCode === 307 || proxyRes.statusCode === 302) && retryCount < 2) {
|
|
1231
|
+
proxyRes.resume(); // drain
|
|
1232
|
+
const loginPath = proxyRes.headers.location || "/login";
|
|
1233
|
+
console.log(` ${DIM}← BFF redirect → ${loginPath} — following to seed cookies...${RESET}`);
|
|
1234
|
+
|
|
1235
|
+
// Hit the login page to get session cookies
|
|
1236
|
+
const loginReq = httpRequest(
|
|
1237
|
+
{
|
|
1238
|
+
hostname: "127.0.0.1",
|
|
1239
|
+
port: ocUIPort,
|
|
1240
|
+
path: loginPath,
|
|
1241
|
+
method: "GET",
|
|
1242
|
+
timeout: 5000,
|
|
1243
|
+
headers: bffCookieJar ? { cookie: bffCookieJar } : {},
|
|
1244
|
+
},
|
|
1245
|
+
(loginRes) => {
|
|
1246
|
+
if (loginRes.headers["set-cookie"]) {
|
|
1247
|
+
captureBFFCookies(loginRes.headers["set-cookie"]);
|
|
1248
|
+
}
|
|
1249
|
+
loginRes.resume(); // drain
|
|
1250
|
+
console.log(` ${DIM}← Login page: ${loginRes.statusCode} — cookies: ${bffCookieJar ? "yes" : "no"}${RESET}`);
|
|
1251
|
+
|
|
1252
|
+
// Retry the original request with new cookies
|
|
1253
|
+
if (bffCookieJar) {
|
|
1254
|
+
opts.headers = { ...opts.headers, cookie: bffCookieJar };
|
|
1255
|
+
console.log(` ${DIM}→ Retrying /api/chat with cookies...${RESET}`);
|
|
1256
|
+
sendBFFRelay(opts, body, retryCount + 1);
|
|
1257
|
+
} else {
|
|
1258
|
+
console.log(` ${DIM}No cookies from login — BFF may require real auth${RESET}`);
|
|
1259
|
+
broadcastToWS({
|
|
1260
|
+
type: "chat.final",
|
|
1261
|
+
text: "⚠️ Could not authenticate with OC — try opening the web UI first to log in.",
|
|
1262
|
+
content: "⚠️ Could not authenticate with OC — try opening the web UI first to log in.",
|
|
1263
|
+
sessionKey,
|
|
1264
|
+
role: "assistant",
|
|
1265
|
+
timestamp: Date.now(),
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
},
|
|
1269
|
+
);
|
|
1270
|
+
loginReq.on("error", () => {
|
|
1271
|
+
console.log(` ${DIM}Login page unreachable${RESET}`);
|
|
1272
|
+
});
|
|
1273
|
+
loginReq.end();
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1199
1276
|
|
|
1200
|
-
const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
|
|
1201
1277
|
const contentType = (proxyRes.headers["content-type"] || "").toLowerCase();
|
|
1202
1278
|
const isSSE = contentType.includes("text/event-stream");
|
|
1203
1279
|
console.log(` ${DIM}← BFF response: ${proxyRes.statusCode} ${contentType || "no-content-type"}${RESET}`);
|
|
@@ -1299,8 +1375,11 @@ function handleRequest(req, res) {
|
|
|
1299
1375
|
console.log(` ${DIM}BFF relay failed: ${err.message}${RESET}`);
|
|
1300
1376
|
});
|
|
1301
1377
|
|
|
1302
|
-
proxyReq.write(
|
|
1378
|
+
proxyReq.write(body);
|
|
1303
1379
|
proxyReq.end();
|
|
1380
|
+
} // end sendBFFRelay
|
|
1381
|
+
|
|
1382
|
+
sendBFFRelay(proxyOpts, proxyBody);
|
|
1304
1383
|
})
|
|
1305
1384
|
.catch(() => sendJSON(res, 400, { ok: false, error: "Bad request body" }));
|
|
1306
1385
|
return;
|
|
@@ -1411,6 +1490,11 @@ function handleRequest(req, res) {
|
|
|
1411
1490
|
console.log(` ${DIM}← ${proxyRes.statusCode} ${ct} for ${path}${RESET}`);
|
|
1412
1491
|
}
|
|
1413
1492
|
|
|
1493
|
+
// Capture session cookies from BFF (for server-side relay auth)
|
|
1494
|
+
if (proxyRes.headers["set-cookie"]) {
|
|
1495
|
+
captureBFFCookies(proxyRes.headers["set-cookie"]);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1414
1498
|
// CORS
|
|
1415
1499
|
headers["access-control-allow-origin"] = "*";
|
|
1416
1500
|
headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, OPTIONS";
|
|
@@ -1501,78 +1585,62 @@ function handleRequest(req, res) {
|
|
|
1501
1585
|
return;
|
|
1502
1586
|
}
|
|
1503
1587
|
|
|
1504
|
-
// ── SSE
|
|
1505
|
-
//
|
|
1506
|
-
//
|
|
1507
|
-
//
|
|
1508
|
-
// and
|
|
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.
|
|
1509
1593
|
if (isSSE && isStreamingEndpoint) {
|
|
1510
|
-
console.log(` ${DIM}SSE
|
|
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"];
|
|
1518
|
-
res.writeHead(proxyRes.statusCode ?? 200, headers);
|
|
1594
|
+
console.log(` ${DIM}SSE→JSON collect: ${path}${RESET}`);
|
|
1519
1595
|
|
|
1520
1596
|
let fullText = "";
|
|
1521
|
-
let
|
|
1597
|
+
let sseData = "";
|
|
1522
1598
|
|
|
1523
1599
|
proxyRes.setEncoding("utf-8");
|
|
1524
1600
|
proxyRes.on("data", (chunk) => {
|
|
1525
|
-
|
|
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 */
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1601
|
+
sseData += chunk;
|
|
1567
1602
|
});
|
|
1568
1603
|
|
|
1569
1604
|
proxyRes.on("end", () => {
|
|
1570
|
-
//
|
|
1571
|
-
|
|
1572
|
-
|
|
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
|
+
}
|
|
1573
1623
|
}
|
|
1574
1624
|
|
|
1575
|
-
//
|
|
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
|
|
1576
1644
|
if (fullText) {
|
|
1577
1645
|
const session = getChatSession("main");
|
|
1578
1646
|
session.messages.push({ role: "assistant", content: fullText, timestamp: Date.now() });
|
|
@@ -1589,12 +1657,11 @@ function handleRequest(req, res) {
|
|
|
1589
1657
|
} else {
|
|
1590
1658
|
console.log(` ${DIM}SSE stream ended with no content${RESET}`);
|
|
1591
1659
|
}
|
|
1592
|
-
res.end();
|
|
1593
1660
|
});
|
|
1594
1661
|
|
|
1595
1662
|
proxyRes.on("error", (err) => {
|
|
1596
1663
|
console.log(` ${DIM}SSE stream error: ${err.message}${RESET}`);
|
|
1597
|
-
res
|
|
1664
|
+
sendJSON(res, 502, { ok: false, error: "Stream error" });
|
|
1598
1665
|
});
|
|
1599
1666
|
return;
|
|
1600
1667
|
}
|