openclaw-navigator 5.8.1 → 5.8.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 +249 -176
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -381,7 +381,9 @@ function getChatSession(sessionKey = "main") {
|
|
|
381
381
|
let bffCookieJar = "";
|
|
382
382
|
|
|
383
383
|
function captureBFFCookies(setCookieHeaders) {
|
|
384
|
-
if (!setCookieHeaders)
|
|
384
|
+
if (!setCookieHeaders) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
385
387
|
const cookies = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders];
|
|
386
388
|
// Extract just the cookie name=value (strip attributes like path, domain, etc.)
|
|
387
389
|
const extracted = cookies.map((c) => c.split(";")[0].trim()).filter(Boolean);
|
|
@@ -1010,11 +1012,10 @@ function handleRequest(req, res) {
|
|
|
1010
1012
|
}
|
|
1011
1013
|
|
|
1012
1014
|
// ── Reverse proxy: /ui/* → OC Web UI (localhost:ocUIPort) ──────────────
|
|
1013
|
-
//
|
|
1014
|
-
// /ui
|
|
1015
|
+
// Forward /ui/* paths as-is — Next.js has basePath: "/ui" so it expects
|
|
1016
|
+
// the /ui prefix on all routes: /ui/ → home, /ui/api/chat → API, etc.
|
|
1015
1017
|
if (path === "/ui" || path.startsWith("/ui/")) {
|
|
1016
|
-
const
|
|
1017
|
-
const targetURL = `${strippedPath}${url.search}`;
|
|
1018
|
+
const targetURL = `${path}${url.search}`;
|
|
1018
1019
|
const incomingHost = req.headers.host || "localhost";
|
|
1019
1020
|
|
|
1020
1021
|
const proxyOpts = {
|
|
@@ -1040,20 +1041,12 @@ function handleRequest(req, res) {
|
|
|
1040
1041
|
headers["access-control-allow-headers"] = "Content-Type, Authorization, Cookie";
|
|
1041
1042
|
headers["access-control-allow-credentials"] = "true";
|
|
1042
1043
|
|
|
1043
|
-
// Fix redirects —
|
|
1044
|
-
//
|
|
1045
|
-
// (e.g. /login, /dashboard) needs the /ui prefix added back for the
|
|
1046
|
-
// browser to stay within our /ui/* proxy.
|
|
1044
|
+
// Fix redirects — strip localhost so browser stays on tunnel URL.
|
|
1045
|
+
// Next.js basePath "/ui" means all redirects already include /ui/.
|
|
1047
1046
|
if (headers.location) {
|
|
1048
|
-
|
|
1047
|
+
headers.location = headers.location
|
|
1049
1048
|
.replace(`http://127.0.0.1:${ocUIPort}`, "")
|
|
1050
1049
|
.replace(`http://localhost:${ocUIPort}`, "");
|
|
1051
|
-
// If the redirect is a relative path (starts with /), add /ui prefix
|
|
1052
|
-
// BUT don't double-prefix if it already starts with /ui
|
|
1053
|
-
if (loc.startsWith("/") && !loc.startsWith("/ui")) {
|
|
1054
|
-
loc = "/ui" + loc;
|
|
1055
|
-
}
|
|
1056
|
-
headers.location = loc;
|
|
1057
1050
|
}
|
|
1058
1051
|
|
|
1059
1052
|
// Fix cookies — remove domain restriction so they work through tunnel
|
|
@@ -1225,7 +1218,7 @@ function handleRequest(req, res) {
|
|
|
1225
1218
|
const proxyOpts = {
|
|
1226
1219
|
hostname: "127.0.0.1",
|
|
1227
1220
|
port: ocUIPort,
|
|
1228
|
-
path: `/api/chat`,
|
|
1221
|
+
path: `/ui/api/chat`,
|
|
1229
1222
|
method: "POST",
|
|
1230
1223
|
timeout: 120000, // 2 min — agent may take a while
|
|
1231
1224
|
headers: {
|
|
@@ -1235,176 +1228,223 @@ function handleRequest(req, res) {
|
|
|
1235
1228
|
},
|
|
1236
1229
|
};
|
|
1237
1230
|
|
|
1238
|
-
console.log(
|
|
1239
|
-
|
|
1231
|
+
console.log(
|
|
1232
|
+
` ${DIM}→ Relaying to BFF /ui/api/chat (port ${ocUIPort}) with ${chatHistory.length} messages${relayCookie ? " +cookie" : " NO-COOKIE"}${RESET}`,
|
|
1233
|
+
);
|
|
1234
|
+
console.log(
|
|
1235
|
+
` ${DIM} Body: ${proxyBody.substring(0, 300)}${proxyBody.length > 300 ? "..." : ""}${RESET}`,
|
|
1236
|
+
);
|
|
1240
1237
|
|
|
1241
1238
|
// Helper: send the relay request (with optional retry after login)
|
|
1242
1239
|
function sendBFFRelay(opts, body, retryCount = 0) {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1240
|
+
const proxyReq = httpRequest(opts, (proxyRes) => {
|
|
1241
|
+
// Capture any cookies the BFF sends (login session, etc.)
|
|
1242
|
+
if (proxyRes.headers["set-cookie"]) {
|
|
1243
|
+
captureBFFCookies(proxyRes.headers["set-cookie"]);
|
|
1244
|
+
}
|
|
1248
1245
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1246
|
+
// If redirect (307/302 to /login) — follow it to seed cookies, then retry
|
|
1247
|
+
if ((proxyRes.statusCode === 307 || proxyRes.statusCode === 302) && retryCount < 2) {
|
|
1248
|
+
proxyRes.resume(); // drain
|
|
1249
|
+
const loginPath = proxyRes.headers.location || "/login";
|
|
1250
|
+
console.log(
|
|
1251
|
+
` ${DIM}← BFF redirect → ${loginPath} — following to seed cookies...${RESET}`,
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
// Hit the login page to get session cookies
|
|
1255
|
+
const loginReq = httpRequest(
|
|
1256
|
+
{
|
|
1257
|
+
hostname: "127.0.0.1",
|
|
1258
|
+
port: ocUIPort,
|
|
1259
|
+
path: loginPath,
|
|
1260
|
+
method: "GET",
|
|
1261
|
+
timeout: 5000,
|
|
1262
|
+
headers: bffCookieJar ? { cookie: bffCookieJar } : {},
|
|
1263
|
+
},
|
|
1264
|
+
(loginRes) => {
|
|
1265
|
+
if (loginRes.headers["set-cookie"]) {
|
|
1266
|
+
captureBFFCookies(loginRes.headers["set-cookie"]);
|
|
1267
|
+
}
|
|
1268
|
+
loginRes.resume(); // drain
|
|
1269
|
+
console.log(
|
|
1270
|
+
` ${DIM}← Login page: ${loginRes.statusCode} — cookies: ${bffCookieJar ? "yes" : "no"}${RESET}`,
|
|
1271
|
+
);
|
|
1272
|
+
|
|
1273
|
+
// Retry the original request with new cookies
|
|
1274
|
+
if (bffCookieJar) {
|
|
1275
|
+
opts.headers = { ...opts.headers, cookie: bffCookieJar };
|
|
1276
|
+
console.log(` ${DIM}→ Retrying /ui/api/chat with cookies...${RESET}`);
|
|
1277
|
+
sendBFFRelay(opts, body, retryCount + 1);
|
|
1278
|
+
} else {
|
|
1279
|
+
console.log(
|
|
1280
|
+
` ${DIM}No cookies from login — BFF may require real auth${RESET}`,
|
|
1281
|
+
);
|
|
1282
|
+
broadcastToWS({
|
|
1283
|
+
type: "chat.final",
|
|
1284
|
+
text: "⚠️ Could not authenticate with OC — try opening the web UI first to log in.",
|
|
1285
|
+
content:
|
|
1286
|
+
"⚠️ Could not authenticate with OC — try opening the web UI first to log in.",
|
|
1287
|
+
sessionKey,
|
|
1288
|
+
role: "assistant",
|
|
1289
|
+
timestamp: Date.now(),
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
},
|
|
1293
|
+
);
|
|
1294
|
+
loginReq.on("error", () => {
|
|
1295
|
+
console.log(` ${DIM}Login page unreachable${RESET}`);
|
|
1296
|
+
});
|
|
1297
|
+
loginReq.end();
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1254
1300
|
|
|
1255
|
-
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
port: ocUIPort,
|
|
1260
|
-
path: loginPath,
|
|
1261
|
-
method: "GET",
|
|
1262
|
-
timeout: 5000,
|
|
1263
|
-
headers: bffCookieJar ? { cookie: bffCookieJar } : {},
|
|
1264
|
-
},
|
|
1265
|
-
(loginRes) => {
|
|
1266
|
-
if (loginRes.headers["set-cookie"]) {
|
|
1267
|
-
captureBFFCookies(loginRes.headers["set-cookie"]);
|
|
1268
|
-
}
|
|
1269
|
-
loginRes.resume(); // drain
|
|
1270
|
-
console.log(` ${DIM}← Login page: ${loginRes.statusCode} — cookies: ${bffCookieJar ? "yes" : "no"}${RESET}`);
|
|
1271
|
-
|
|
1272
|
-
// Retry the original request with new cookies
|
|
1273
|
-
if (bffCookieJar) {
|
|
1274
|
-
opts.headers = { ...opts.headers, cookie: bffCookieJar };
|
|
1275
|
-
console.log(` ${DIM}→ Retrying /api/chat with cookies...${RESET}`);
|
|
1276
|
-
sendBFFRelay(opts, body, retryCount + 1);
|
|
1277
|
-
} else {
|
|
1278
|
-
console.log(` ${DIM}No cookies from login — BFF may require real auth${RESET}`);
|
|
1279
|
-
broadcastToWS({
|
|
1280
|
-
type: "chat.final",
|
|
1281
|
-
text: "⚠️ Could not authenticate with OC — try opening the web UI first to log in.",
|
|
1282
|
-
content: "⚠️ Could not authenticate with OC — try opening the web UI first to log in.",
|
|
1283
|
-
sessionKey,
|
|
1284
|
-
role: "assistant",
|
|
1285
|
-
timestamp: Date.now(),
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
},
|
|
1301
|
+
const contentType = (proxyRes.headers["content-type"] || "").toLowerCase();
|
|
1302
|
+
const isSSE = contentType.includes("text/event-stream");
|
|
1303
|
+
console.log(
|
|
1304
|
+
` ${DIM}← BFF response: ${proxyRes.statusCode} ${contentType || "no-content-type"}${RESET}`,
|
|
1289
1305
|
);
|
|
1290
|
-
loginReq.on("error", () => {
|
|
1291
|
-
console.log(` ${DIM}Login page unreachable${RESET}`);
|
|
1292
|
-
});
|
|
1293
|
-
loginReq.end();
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
1306
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1307
|
+
// Collect the response (SSE or JSON) and extract the assistant's text
|
|
1308
|
+
let fullText = "";
|
|
1309
|
+
let sseBuffer = "";
|
|
1310
|
+
|
|
1311
|
+
proxyRes.setEncoding("utf-8");
|
|
1312
|
+
proxyRes.on("data", (chunk) => {
|
|
1313
|
+
if (isSSE) {
|
|
1314
|
+
sseBuffer += chunk;
|
|
1315
|
+
const lines = sseBuffer.split("\n");
|
|
1316
|
+
sseBuffer = lines.pop() || "";
|
|
1317
|
+
for (const line of lines) {
|
|
1318
|
+
if (line.startsWith("data: ")) {
|
|
1319
|
+
const raw = line.slice(6).trim();
|
|
1320
|
+
if (raw === "[DONE]" || !raw) {
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
try {
|
|
1324
|
+
const evt = JSON.parse(raw);
|
|
1325
|
+
const delta =
|
|
1326
|
+
evt.choices?.[0]?.delta?.content ||
|
|
1327
|
+
evt.delta?.text ||
|
|
1328
|
+
evt.text ||
|
|
1329
|
+
evt.content ||
|
|
1330
|
+
"";
|
|
1331
|
+
if (delta) {
|
|
1332
|
+
fullText += delta;
|
|
1333
|
+
broadcastToWS({
|
|
1334
|
+
type: "chat.delta",
|
|
1335
|
+
text: fullText,
|
|
1336
|
+
delta,
|
|
1337
|
+
sessionKey,
|
|
1338
|
+
timestamp: Date.now(),
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
} catch {
|
|
1342
|
+
if (raw && raw !== "[DONE]") {
|
|
1343
|
+
fullText += raw;
|
|
1344
|
+
}
|
|
1332
1345
|
}
|
|
1333
|
-
} catch {
|
|
1334
|
-
if (raw && raw !== "[DONE]") fullText += raw;
|
|
1335
1346
|
}
|
|
1336
1347
|
}
|
|
1348
|
+
} else {
|
|
1349
|
+
sseBuffer += chunk; // Collect JSON body
|
|
1337
1350
|
}
|
|
1338
|
-
}
|
|
1339
|
-
sseBuffer += chunk; // Collect JSON body
|
|
1340
|
-
}
|
|
1341
|
-
});
|
|
1351
|
+
});
|
|
1342
1352
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1353
|
+
proxyRes.on("end", () => {
|
|
1354
|
+
// Handle remaining buffer for SSE
|
|
1355
|
+
if (isSSE && sseBuffer) {
|
|
1356
|
+
for (const line of sseBuffer.split("\n")) {
|
|
1357
|
+
if (line.startsWith("data: ")) {
|
|
1358
|
+
const raw = line.slice(6).trim();
|
|
1359
|
+
if (raw && raw !== "[DONE]") {
|
|
1360
|
+
try {
|
|
1361
|
+
const evt = JSON.parse(raw);
|
|
1362
|
+
const delta =
|
|
1363
|
+
evt.choices?.[0]?.delta?.content ||
|
|
1364
|
+
evt.delta?.text ||
|
|
1365
|
+
evt.text ||
|
|
1366
|
+
evt.content ||
|
|
1367
|
+
"";
|
|
1368
|
+
if (delta) {
|
|
1369
|
+
fullText += delta;
|
|
1370
|
+
}
|
|
1371
|
+
} catch {
|
|
1372
|
+
fullText += raw;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1355
1375
|
}
|
|
1356
1376
|
}
|
|
1357
1377
|
}
|
|
1358
|
-
}
|
|
1359
1378
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1379
|
+
// Handle JSON response
|
|
1380
|
+
if (!isSSE && sseBuffer) {
|
|
1381
|
+
try {
|
|
1382
|
+
const jsonBody = JSON.parse(sseBuffer);
|
|
1383
|
+
// Check for error response (BFF returns 400/500 with error details)
|
|
1384
|
+
if (proxyRes.statusCode >= 400) {
|
|
1385
|
+
const errMsg =
|
|
1386
|
+
jsonBody.error?.message ||
|
|
1387
|
+
jsonBody.error ||
|
|
1388
|
+
jsonBody.message ||
|
|
1389
|
+
JSON.stringify(jsonBody);
|
|
1390
|
+
console.log(
|
|
1391
|
+
` ${RED}✗${RESET} BFF error ${proxyRes.statusCode}: ${String(errMsg).substring(0, 300)}`,
|
|
1392
|
+
);
|
|
1393
|
+
// Send error to Navigator so user sees it
|
|
1394
|
+
fullText = `⚠️ Error from AI: ${String(errMsg).substring(0, 200)}`;
|
|
1395
|
+
} else {
|
|
1396
|
+
fullText =
|
|
1397
|
+
jsonBody.choices?.[0]?.message?.content ||
|
|
1398
|
+
jsonBody.response ||
|
|
1399
|
+
jsonBody.message ||
|
|
1400
|
+
jsonBody.text ||
|
|
1401
|
+
"";
|
|
1402
|
+
}
|
|
1403
|
+
} catch {
|
|
1404
|
+
console.log(
|
|
1405
|
+
` ${DIM}BFF returned non-JSON (${sseBuffer.length} bytes): ${sseBuffer.substring(0, 200)}${RESET}`,
|
|
1406
|
+
);
|
|
1372
1407
|
}
|
|
1373
|
-
} catch {
|
|
1374
|
-
console.log(` ${DIM}BFF returned non-JSON (${sseBuffer.length} bytes): ${sseBuffer.substring(0, 200)}${RESET}`);
|
|
1375
1408
|
}
|
|
1376
|
-
}
|
|
1377
1409
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1410
|
+
if (fullText) {
|
|
1411
|
+
session.messages.push({
|
|
1412
|
+
role: "assistant",
|
|
1413
|
+
content: fullText,
|
|
1414
|
+
timestamp: Date.now(),
|
|
1415
|
+
});
|
|
1416
|
+
broadcastToWS({
|
|
1417
|
+
type: "chat.final",
|
|
1418
|
+
text: fullText,
|
|
1419
|
+
content: fullText,
|
|
1420
|
+
sessionKey,
|
|
1421
|
+
role: "assistant",
|
|
1422
|
+
timestamp: Date.now(),
|
|
1423
|
+
});
|
|
1424
|
+
console.log(
|
|
1425
|
+
` ${GREEN}✓${RESET} AI response (${fullText.length} chars): ${fullText.substring(0, 80)}...`,
|
|
1426
|
+
);
|
|
1427
|
+
} else {
|
|
1428
|
+
console.log(
|
|
1429
|
+
` ${DIM}BFF returned no content (status ${proxyRes.statusCode})${RESET}`,
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1393
1433
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1434
|
+
proxyRes.on("error", () => {});
|
|
1435
|
+
});
|
|
1396
1436
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1437
|
+
proxyReq.on("timeout", () => {
|
|
1438
|
+
proxyReq.destroy();
|
|
1439
|
+
console.log(` ${DIM}BFF relay timed out${RESET}`);
|
|
1440
|
+
});
|
|
1401
1441
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1442
|
+
proxyReq.on("error", (err) => {
|
|
1443
|
+
console.log(` ${DIM}BFF relay failed: ${err.message}${RESET}`);
|
|
1444
|
+
});
|
|
1405
1445
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1446
|
+
proxyReq.write(body);
|
|
1447
|
+
proxyReq.end();
|
|
1408
1448
|
} // end sendBFFRelay
|
|
1409
1449
|
|
|
1410
1450
|
sendBFFRelay(proxyOpts, proxyBody);
|
|
@@ -1477,12 +1517,21 @@ function handleRequest(req, res) {
|
|
|
1477
1517
|
// SSE safety: If the web UI returns text/event-stream but the browser
|
|
1478
1518
|
// expects JSON (e.g. /api/agents), we collect the SSE events and return
|
|
1479
1519
|
// them as a JSON array. This prevents "data: ... is not valid JSON" errors.
|
|
1520
|
+
//
|
|
1521
|
+
// Next.js basePath is "/ui", so we prefix fallback paths with /ui/ to match.
|
|
1522
|
+
// Paths already starting with /ui/ won't reach here (handled by /ui/* proxy above).
|
|
1480
1523
|
{
|
|
1481
|
-
|
|
1524
|
+
// Add /ui prefix for Next.js basePath — browser legacy paths like /login, /api/*,
|
|
1525
|
+
// /_next/* need the basePath to match Next.js routes on port 4000.
|
|
1526
|
+
const needsBasePath = path.startsWith("/api/") || path.startsWith("/_next/") ||
|
|
1527
|
+
path === "/login" || path.startsWith("/login?") ||
|
|
1528
|
+
path === "/favicon.ico" || path.startsWith("/static/");
|
|
1529
|
+
const prefixedPath = needsBasePath ? `/ui${path}` : path;
|
|
1530
|
+
const targetURL = `${prefixedPath}${url.search}`;
|
|
1482
1531
|
const incomingHost = req.headers.host || "localhost";
|
|
1483
1532
|
// Log API calls for diagnostics (helps debug web UI chat)
|
|
1484
1533
|
if (path.startsWith("/api/")) {
|
|
1485
|
-
console.log(` ${DIM}→ Proxy ${req.method} ${path} → localhost:${ocUIPort}${RESET}`);
|
|
1534
|
+
console.log(` ${DIM}→ Proxy ${req.method} ${path} → localhost:${ocUIPort}${prefixedPath !== path ? ` (→ ${prefixedPath})` : ""}${RESET}`);
|
|
1486
1535
|
}
|
|
1487
1536
|
|
|
1488
1537
|
// Encourage JSON responses from the web UI's API routes
|
|
@@ -1620,35 +1669,48 @@ function handleRequest(req, res) {
|
|
|
1620
1669
|
if (isSSE && isStreamingEndpoint) {
|
|
1621
1670
|
let sseData = "";
|
|
1622
1671
|
proxyRes.setEncoding("utf-8");
|
|
1623
|
-
proxyRes.on("data", (chunk) => {
|
|
1672
|
+
proxyRes.on("data", (chunk) => {
|
|
1673
|
+
sseData += chunk;
|
|
1674
|
+
});
|
|
1624
1675
|
proxyRes.on("end", () => {
|
|
1625
1676
|
// Extract text from SSE events
|
|
1626
1677
|
let fullText = "";
|
|
1627
1678
|
for (const line of sseData.split("\n")) {
|
|
1628
1679
|
if (line.startsWith("data: ")) {
|
|
1629
1680
|
const raw = line.slice(6).trim();
|
|
1630
|
-
if (raw === "[DONE]" || !raw)
|
|
1681
|
+
if (raw === "[DONE]" || !raw) {
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1631
1684
|
try {
|
|
1632
1685
|
const evt = JSON.parse(raw);
|
|
1633
1686
|
const delta =
|
|
1634
1687
|
evt.choices?.[0]?.delta?.content ||
|
|
1635
1688
|
evt.delta?.text ||
|
|
1636
1689
|
evt.text ||
|
|
1637
|
-
evt.content ||
|
|
1638
|
-
|
|
1690
|
+
evt.content ||
|
|
1691
|
+
"";
|
|
1692
|
+
if (delta) {
|
|
1693
|
+
fullText += delta;
|
|
1694
|
+
}
|
|
1639
1695
|
} catch {
|
|
1640
|
-
if (raw)
|
|
1696
|
+
if (raw) {
|
|
1697
|
+
fullText += raw;
|
|
1698
|
+
}
|
|
1641
1699
|
}
|
|
1642
1700
|
}
|
|
1643
1701
|
// Also handle Vercel AI stream format (0:"text") passthrough
|
|
1644
1702
|
if (line.startsWith("0:")) {
|
|
1645
1703
|
try {
|
|
1646
1704
|
fullText += JSON.parse(line.slice(2));
|
|
1647
|
-
} catch {
|
|
1705
|
+
} catch {
|
|
1706
|
+
/* skip */
|
|
1707
|
+
}
|
|
1648
1708
|
}
|
|
1649
1709
|
}
|
|
1650
1710
|
|
|
1651
|
-
console.log(
|
|
1711
|
+
console.log(
|
|
1712
|
+
` ${GREEN}✓${RESET} Chat (${fullText.length} chars): ${fullText.substring(0, 80)}...`,
|
|
1713
|
+
);
|
|
1652
1714
|
|
|
1653
1715
|
// Return as flat JSON with text in every common field name.
|
|
1654
1716
|
// The frontend does JSON.parse() and looks for the text at top level.
|
|
@@ -1659,7 +1721,13 @@ function handleRequest(req, res) {
|
|
|
1659
1721
|
message: fullText,
|
|
1660
1722
|
answer: fullText,
|
|
1661
1723
|
reply: fullText,
|
|
1662
|
-
choices: [
|
|
1724
|
+
choices: [
|
|
1725
|
+
{
|
|
1726
|
+
index: 0,
|
|
1727
|
+
message: { role: "assistant", content: fullText },
|
|
1728
|
+
finish_reason: "stop",
|
|
1729
|
+
},
|
|
1730
|
+
],
|
|
1663
1731
|
});
|
|
1664
1732
|
headers["content-type"] = "application/json";
|
|
1665
1733
|
delete headers["content-length"];
|
|
@@ -1680,7 +1748,10 @@ function handleRequest(req, res) {
|
|
|
1680
1748
|
if (path.startsWith("/api/")) {
|
|
1681
1749
|
console.log(` ${DIM}✗ Proxy error for ${path}: ${err.message}${RESET}`);
|
|
1682
1750
|
}
|
|
1683
|
-
sendJSON(res, 502, {
|
|
1751
|
+
sendJSON(res, 502, {
|
|
1752
|
+
ok: false,
|
|
1753
|
+
error: `Web UI (port ${ocUIPort}) not reachable: ${err.message}`,
|
|
1754
|
+
});
|
|
1684
1755
|
});
|
|
1685
1756
|
|
|
1686
1757
|
req.pipe(proxyReq, { end: true });
|
|
@@ -1723,7 +1794,9 @@ async function registerWithRelay(code, url, token, name) {
|
|
|
1723
1794
|
signal: c2.signal,
|
|
1724
1795
|
});
|
|
1725
1796
|
clearTimeout(t2);
|
|
1726
|
-
} catch {
|
|
1797
|
+
} catch {
|
|
1798
|
+
/* non-critical */
|
|
1799
|
+
}
|
|
1727
1800
|
|
|
1728
1801
|
return data.ok === true;
|
|
1729
1802
|
} catch {
|