openclaw-navigator 5.5.1 → 5.5.3
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 -9
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* openclaw-navigator v5.5.
|
|
4
|
+
* openclaw-navigator v5.5.3
|
|
5
5
|
*
|
|
6
6
|
* One-command bridge + tunnel for the Navigator browser.
|
|
7
7
|
* Starts a local bridge, creates a Cloudflare tunnel automatically,
|
|
@@ -1330,25 +1330,37 @@ function handleRequest(req, res) {
|
|
|
1330
1330
|
}
|
|
1331
1331
|
|
|
1332
1332
|
// ── Fallback: proxy unmatched paths → OC Web UI ──────────────────────
|
|
1333
|
-
// Catches /login, /_next/*, /static/*, /favicon.ico, etc.
|
|
1333
|
+
// Catches /api/*, /login, /_next/*, /static/*, /favicon.ico, etc.
|
|
1334
1334
|
// MUST apply the same cookie/redirect/forwarded-header treatment as /ui/*
|
|
1335
1335
|
// or login won't work (session cookies get lost through the tunnel).
|
|
1336
|
+
//
|
|
1337
|
+
// SSE safety: If the web UI returns text/event-stream but the browser
|
|
1338
|
+
// expects JSON (e.g. /api/agents), we collect the SSE events and return
|
|
1339
|
+
// them as a JSON array. This prevents "data: ... is not valid JSON" errors.
|
|
1336
1340
|
{
|
|
1337
1341
|
const targetURL = `${path}${url.search}`;
|
|
1338
1342
|
const incomingHost = req.headers.host || "localhost";
|
|
1339
1343
|
|
|
1344
|
+
// Encourage JSON responses from the web UI's API routes
|
|
1345
|
+
const proxyHeaders = {
|
|
1346
|
+
...req.headers,
|
|
1347
|
+
host: `127.0.0.1:${ocUIPort}`,
|
|
1348
|
+
"x-forwarded-host": incomingHost,
|
|
1349
|
+
"x-forwarded-proto": activeTunnelURL ? "https" : "http",
|
|
1350
|
+
"x-forwarded-for": req.socket.remoteAddress || "127.0.0.1",
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
// For /api/* requests, prefer JSON over SSE
|
|
1354
|
+
if (path.startsWith("/api/")) {
|
|
1355
|
+
proxyHeaders["accept"] = "application/json, text/plain, */*";
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1340
1358
|
const proxyOpts = {
|
|
1341
1359
|
hostname: "127.0.0.1",
|
|
1342
1360
|
port: ocUIPort,
|
|
1343
1361
|
path: targetURL,
|
|
1344
1362
|
method: req.method,
|
|
1345
|
-
headers:
|
|
1346
|
-
...req.headers,
|
|
1347
|
-
host: `127.0.0.1:${ocUIPort}`,
|
|
1348
|
-
"x-forwarded-host": incomingHost,
|
|
1349
|
-
"x-forwarded-proto": activeTunnelURL ? "https" : "http",
|
|
1350
|
-
"x-forwarded-for": req.socket.remoteAddress || "127.0.0.1",
|
|
1351
|
-
},
|
|
1363
|
+
headers: proxyHeaders,
|
|
1352
1364
|
};
|
|
1353
1365
|
|
|
1354
1366
|
const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
|
|
@@ -1377,6 +1389,140 @@ function handleRequest(req, res) {
|
|
|
1377
1389
|
);
|
|
1378
1390
|
}
|
|
1379
1391
|
|
|
1392
|
+
// ── SSE→JSON conversion for non-streaming /api/* endpoints ──────
|
|
1393
|
+
// Some API endpoints (e.g. /api/agents) return SSE when the frontend
|
|
1394
|
+
// expects JSON. We convert those to JSON. But STREAMING endpoints
|
|
1395
|
+
// like /api/chat must pass through as SSE so the frontend can read
|
|
1396
|
+
// the real-time token stream via EventSource/fetch streaming.
|
|
1397
|
+
const contentType = (proxyRes.headers["content-type"] || "").toLowerCase();
|
|
1398
|
+
const isSSE = contentType.includes("text/event-stream");
|
|
1399
|
+
const isStreamingEndpoint = path.startsWith("/api/chat") || path.startsWith("/api/stream");
|
|
1400
|
+
|
|
1401
|
+
if (isSSE && path.startsWith("/api/") && !isStreamingEndpoint) {
|
|
1402
|
+
console.log(` ${DIM}SSE→JSON: converting ${path}${RESET}`);
|
|
1403
|
+
let buffer = "";
|
|
1404
|
+
const items = [];
|
|
1405
|
+
|
|
1406
|
+
proxyRes.setEncoding("utf-8");
|
|
1407
|
+
proxyRes.on("data", (chunk) => {
|
|
1408
|
+
buffer += chunk;
|
|
1409
|
+
const lines = buffer.split("\n");
|
|
1410
|
+
buffer = lines.pop() || "";
|
|
1411
|
+
for (const line of lines) {
|
|
1412
|
+
if (line.startsWith("data: ")) {
|
|
1413
|
+
const raw = line.slice(6).trim();
|
|
1414
|
+
if (raw === "[DONE]" || !raw) {
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
try {
|
|
1418
|
+
items.push(JSON.parse(raw));
|
|
1419
|
+
} catch {
|
|
1420
|
+
// Non-JSON SSE data — wrap as text
|
|
1421
|
+
if (raw) {
|
|
1422
|
+
items.push({ text: raw });
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
proxyRes.on("end", () => {
|
|
1430
|
+
// Process remaining buffer
|
|
1431
|
+
if (buffer.startsWith("data: ")) {
|
|
1432
|
+
const raw = buffer.slice(6).trim();
|
|
1433
|
+
if (raw && raw !== "[DONE]") {
|
|
1434
|
+
try {
|
|
1435
|
+
items.push(JSON.parse(raw));
|
|
1436
|
+
} catch {
|
|
1437
|
+
if (raw) {
|
|
1438
|
+
items.push({ text: raw });
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// Return as JSON — single object if 1 item, array if multiple
|
|
1445
|
+
const result = items.length === 1 ? items[0] : items;
|
|
1446
|
+
headers["content-type"] = "application/json";
|
|
1447
|
+
delete headers["content-length"];
|
|
1448
|
+
delete headers["transfer-encoding"];
|
|
1449
|
+
res.writeHead(proxyRes.statusCode ?? 200, headers);
|
|
1450
|
+
res.end(JSON.stringify(result));
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
proxyRes.on("error", () => {
|
|
1454
|
+
sendJSON(res, 502, { ok: false, error: "SSE stream error" });
|
|
1455
|
+
});
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// ── SSE passthrough for streaming endpoints (/api/chat) ──────────
|
|
1460
|
+
// Let the SSE stream through to the browser AND tap it to broadcast
|
|
1461
|
+
// chunks via our WebSocket (so Navigator's sidepane chat gets them).
|
|
1462
|
+
if (isSSE && isStreamingEndpoint) {
|
|
1463
|
+
console.log(` ${DIM}SSE passthrough + WS tap: ${path}${RESET}`);
|
|
1464
|
+
res.writeHead(proxyRes.statusCode ?? 200, headers);
|
|
1465
|
+
|
|
1466
|
+
let fullText = "";
|
|
1467
|
+
proxyRes.on("data", (chunk) => {
|
|
1468
|
+
// Forward the chunk to the browser immediately
|
|
1469
|
+
res.write(chunk);
|
|
1470
|
+
|
|
1471
|
+
// Also parse and broadcast to WebSocket
|
|
1472
|
+
const text = chunk.toString("utf-8");
|
|
1473
|
+
for (const line of text.split("\n")) {
|
|
1474
|
+
if (line.startsWith("data: ")) {
|
|
1475
|
+
const raw = line.slice(6).trim();
|
|
1476
|
+
if (raw === "[DONE]" || !raw) {
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
try {
|
|
1480
|
+
const evt = JSON.parse(raw);
|
|
1481
|
+
const delta =
|
|
1482
|
+
evt.choices?.[0]?.delta?.content ||
|
|
1483
|
+
evt.delta?.text ||
|
|
1484
|
+
evt.text ||
|
|
1485
|
+
evt.content ||
|
|
1486
|
+
"";
|
|
1487
|
+
if (delta) {
|
|
1488
|
+
fullText += delta;
|
|
1489
|
+
broadcastToWS({
|
|
1490
|
+
type: "chat.delta",
|
|
1491
|
+
text: fullText,
|
|
1492
|
+
delta,
|
|
1493
|
+
sessionKey: "main",
|
|
1494
|
+
timestamp: Date.now(),
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
} catch {
|
|
1498
|
+
/* non-JSON SSE event — skip */
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
proxyRes.on("end", () => {
|
|
1505
|
+
// Stream ended — broadcast final message via WebSocket
|
|
1506
|
+
if (fullText) {
|
|
1507
|
+
broadcastToWS({
|
|
1508
|
+
type: "chat.final",
|
|
1509
|
+
text: fullText,
|
|
1510
|
+
content: fullText,
|
|
1511
|
+
sessionKey: "main",
|
|
1512
|
+
role: "assistant",
|
|
1513
|
+
timestamp: Date.now(),
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
res.end();
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
proxyRes.on("error", () => {
|
|
1520
|
+
res.end();
|
|
1521
|
+
});
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Non-SSE responses — stream through as-is
|
|
1380
1526
|
res.writeHead(proxyRes.statusCode ?? 502, headers);
|
|
1381
1527
|
proxyRes.pipe(res, { end: true });
|
|
1382
1528
|
});
|