openclaw-navigator 5.4.2 → 5.5.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 +164 -1
- 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.0
|
|
5
5
|
*
|
|
6
6
|
* One-command bridge + tunnel for the Navigator browser.
|
|
7
7
|
* Starts a local bridge, creates a Cloudflare tunnel automatically,
|
|
@@ -1116,10 +1116,173 @@ function handleRequest(req, res) {
|
|
|
1116
1116
|
return;
|
|
1117
1117
|
}
|
|
1118
1118
|
|
|
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
|
+
});
|
|
1265
|
+
|
|
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
|
+
});
|
|
1273
|
+
|
|
1274
|
+
proxyReq.write(bodyStr);
|
|
1275
|
+
proxyReq.end();
|
|
1276
|
+
})
|
|
1277
|
+
.catch(() => sendJSON(res, 400, { ok: false, error: "Bad request body" }));
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1119
1281
|
// ── Reverse proxy: /api/* → OC Gateway (localhost:ocGatewayPort) ──────────
|
|
1120
1282
|
// ALL /api/* requests go to the OC gateway EXCEPT:
|
|
1121
1283
|
// - /api/auth/* → falls through to web UI (NextAuth login/session)
|
|
1122
1284
|
// - /api/sessions/respond + /api/sessions/stream → handled locally above
|
|
1285
|
+
// - /api/sessions/send → smart SSE bridge above
|
|
1123
1286
|
// The OC web UI frontend expects /api/agents, /api/sessions/*, /api/chat/*,
|
|
1124
1287
|
// etc. to reach the gateway. Only /api/auth/* is a Next.js API route.
|
|
1125
1288
|
if (path.startsWith("/api/") && !path.startsWith("/api/auth/") && path !== "/api/auth") {
|