openclaw-navigator 5.4.2 → 5.5.1
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 +173 -41
- 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.
|
|
4
|
+
* openclaw-navigator v5.5.1
|
|
5
5
|
*
|
|
6
6
|
* One-command bridge + tunnel for the Navigator browser.
|
|
7
7
|
* Starts a local bridge, creates a Cloudflare tunnel automatically,
|
|
@@ -1116,51 +1116,183 @@ function handleRequest(req, res) {
|
|
|
1116
1116
|
return;
|
|
1117
1117
|
}
|
|
1118
1118
|
|
|
1119
|
-
// ──
|
|
1120
|
-
//
|
|
1121
|
-
//
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1125
|
-
if (path
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1119
|
+
// ── Smart proxy: /api/sessions/send → OC Gateway + SSE bridge ───────────
|
|
1120
|
+
// This is the main chat send endpoint. The gateway may return:
|
|
1121
|
+
// (a) JSON: { ok, response, message, ... } — pass through + broadcast
|
|
1122
|
+
// (b) SSE: text/event-stream with data lines — collect + broadcast + return JSON
|
|
1123
|
+
// Either way, the response is also broadcast via WebSocket so Navigator's
|
|
1124
|
+
// chat pane (OCChatService) gets it via both HTTP and WS.
|
|
1125
|
+
if (path === "/api/sessions/send" && req.method === "POST") {
|
|
1126
|
+
readBody(req)
|
|
1127
|
+
.then((bodyStr) => {
|
|
1128
|
+
const proxyOpts = {
|
|
1129
|
+
hostname: "127.0.0.1",
|
|
1130
|
+
port: ocGatewayPort,
|
|
1131
|
+
path: `${path}${url.search}`,
|
|
1132
|
+
method: "POST",
|
|
1133
|
+
headers: {
|
|
1134
|
+
...req.headers,
|
|
1135
|
+
host: `127.0.0.1:${ocGatewayPort}`,
|
|
1136
|
+
"content-type": "application/json",
|
|
1137
|
+
"content-length": Buffer.byteLength(bodyStr),
|
|
1138
|
+
},
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
|
|
1142
|
+
const contentType = (proxyRes.headers["content-type"] || "").toLowerCase();
|
|
1143
|
+
const isSSE = contentType.includes("text/event-stream");
|
|
1144
|
+
|
|
1145
|
+
if (isSSE) {
|
|
1146
|
+
// ── SSE response: collect stream, broadcast chunks, return JSON ──
|
|
1147
|
+
let fullText = "";
|
|
1148
|
+
let buffer = "";
|
|
1149
|
+
|
|
1150
|
+
proxyRes.setEncoding("utf-8");
|
|
1151
|
+
proxyRes.on("data", (chunk) => {
|
|
1152
|
+
buffer += chunk;
|
|
1153
|
+
// Parse SSE lines
|
|
1154
|
+
const lines = buffer.split("\n");
|
|
1155
|
+
buffer = lines.pop() || ""; // keep incomplete line
|
|
1156
|
+
for (const line of lines) {
|
|
1157
|
+
if (line.startsWith("data: ")) {
|
|
1158
|
+
const raw = line.slice(6).trim();
|
|
1159
|
+
if (raw === "[DONE]") {
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
try {
|
|
1163
|
+
const evt = JSON.parse(raw);
|
|
1164
|
+
// Extract text from common SSE shapes
|
|
1165
|
+
const text =
|
|
1166
|
+
evt.text || evt.content || evt.delta?.text || evt.delta?.content || "";
|
|
1167
|
+
if (text) {
|
|
1168
|
+
fullText += text;
|
|
1169
|
+
broadcastToWS({
|
|
1170
|
+
type: "chat.delta",
|
|
1171
|
+
text: fullText,
|
|
1172
|
+
delta: text,
|
|
1173
|
+
sessionKey: "main",
|
|
1174
|
+
timestamp: Date.now(),
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
} catch {
|
|
1178
|
+
// Non-JSON SSE line — treat as plain text
|
|
1179
|
+
if (raw && raw !== "[DONE]") {
|
|
1180
|
+
fullText += raw;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
proxyRes.on("end", () => {
|
|
1188
|
+
// Process any remaining buffer
|
|
1189
|
+
if (buffer.startsWith("data: ")) {
|
|
1190
|
+
const raw = buffer.slice(6).trim();
|
|
1191
|
+
if (raw && raw !== "[DONE]") {
|
|
1192
|
+
try {
|
|
1193
|
+
const evt = JSON.parse(raw);
|
|
1194
|
+
fullText += evt.text || evt.content || evt.delta?.text || "";
|
|
1195
|
+
} catch {
|
|
1196
|
+
fullText += raw;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (fullText) {
|
|
1202
|
+
// Broadcast final via WebSocket
|
|
1203
|
+
broadcastToWS({
|
|
1204
|
+
type: "chat.final",
|
|
1205
|
+
text: fullText,
|
|
1206
|
+
content: fullText,
|
|
1207
|
+
sessionKey: "main",
|
|
1208
|
+
role: "assistant",
|
|
1209
|
+
timestamp: Date.now(),
|
|
1210
|
+
});
|
|
1211
|
+
console.log(` ${DIM}SSE→WS bridge: ${fullText.substring(0, 80)}...${RESET}`);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Return clean JSON to the caller (OCChatService)
|
|
1215
|
+
sendJSON(res, 200, {
|
|
1216
|
+
ok: true,
|
|
1217
|
+
response: fullText || null,
|
|
1218
|
+
format: "sse-bridged",
|
|
1219
|
+
delivered: wsClients.size,
|
|
1220
|
+
});
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
proxyRes.on("error", () => {
|
|
1224
|
+
sendJSON(res, 502, { ok: false, error: "SSE stream error from gateway" });
|
|
1225
|
+
});
|
|
1226
|
+
} else {
|
|
1227
|
+
// ── JSON response: collect body, broadcast if it has a response, pass through ──
|
|
1228
|
+
const chunks = [];
|
|
1229
|
+
proxyRes.on("data", (c) => chunks.push(c));
|
|
1230
|
+
proxyRes.on("end", () => {
|
|
1231
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
1232
|
+
let jsonBody;
|
|
1233
|
+
try {
|
|
1234
|
+
jsonBody = JSON.parse(body);
|
|
1235
|
+
} catch {
|
|
1236
|
+
// Not JSON — pass through raw
|
|
1237
|
+
const headers = { ...proxyRes.headers };
|
|
1238
|
+
headers["access-control-allow-origin"] = "*";
|
|
1239
|
+
res.writeHead(proxyRes.statusCode ?? 200, headers);
|
|
1240
|
+
res.end(body);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// If the gateway returned an inline response, broadcast it via WebSocket
|
|
1245
|
+
const inlineResponse =
|
|
1246
|
+
jsonBody.response || jsonBody.message || jsonBody.answer || jsonBody.text || "";
|
|
1247
|
+
if (inlineResponse) {
|
|
1248
|
+
broadcastToWS({
|
|
1249
|
+
type: "chat.final",
|
|
1250
|
+
text: inlineResponse,
|
|
1251
|
+
content: inlineResponse,
|
|
1252
|
+
sessionKey: "main",
|
|
1253
|
+
role: "assistant",
|
|
1254
|
+
timestamp: Date.now(),
|
|
1255
|
+
});
|
|
1256
|
+
console.log(
|
|
1257
|
+
` ${DIM}Inline response→WS: ${inlineResponse.substring(0, 80)}...${RESET}`,
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
sendJSON(res, proxyRes.statusCode ?? 200, jsonBody);
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1147
1265
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
ocGatewayPort +
|
|
1156
|
-
" (openclaw gateway start)",
|
|
1157
|
-
});
|
|
1158
|
-
});
|
|
1266
|
+
proxyReq.on("error", (err) => {
|
|
1267
|
+
sendJSON(res, 502, {
|
|
1268
|
+
ok: false,
|
|
1269
|
+
error: `OC Gateway not reachable on port ${ocGatewayPort}`,
|
|
1270
|
+
detail: err.message,
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1159
1273
|
|
|
1160
|
-
|
|
1274
|
+
proxyReq.write(bodyStr);
|
|
1275
|
+
proxyReq.end();
|
|
1276
|
+
})
|
|
1277
|
+
.catch(() => sendJSON(res, 400, { ok: false, error: "Bad request body" }));
|
|
1161
1278
|
return;
|
|
1162
1279
|
}
|
|
1163
1280
|
|
|
1281
|
+
// ── /api/* routing strategy ──────────────────────────────────────────────
|
|
1282
|
+
// NO direct gateway proxy for /api/* here. The web UI (Next.js on port 4000)
|
|
1283
|
+
// has its own /api/* routes that act as a BFF (Backend for Frontend) —
|
|
1284
|
+
// they call the gateway internally (localhost:18789) and return clean JSON.
|
|
1285
|
+
// Routing /api/* directly to the gateway bypasses the BFF and breaks things
|
|
1286
|
+
// (e.g. /api/agents returns raw SSE instead of JSON).
|
|
1287
|
+
//
|
|
1288
|
+
// Specific bridge-handled routes (above this point):
|
|
1289
|
+
// - /api/sessions/respond → local WebSocket broadcast
|
|
1290
|
+
// - /api/sessions/stream → local WebSocket broadcast
|
|
1291
|
+
// - /api/sessions/send → smart SSE bridge to gateway (sidepane chat)
|
|
1292
|
+
//
|
|
1293
|
+
// Everything else (/api/agents, /api/auth/*, /api/chat/*, etc.) falls through
|
|
1294
|
+
// to the fallback proxy below, which sends it to port 4000 (web UI).
|
|
1295
|
+
|
|
1164
1296
|
// ── Root → redirect to /ui/ (Next.js basePath) or show bridge status ──
|
|
1165
1297
|
if (req.method === "GET" && path === "") {
|
|
1166
1298
|
// Quick probe to check if OC UI is running
|