openclaw-navigator 5.7.3 → 5.7.5
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 +67 -19
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -1193,10 +1193,28 @@ function handleRequest(req, res) {
|
|
|
1193
1193
|
|
|
1194
1194
|
// 3. Background: relay to BFF (port 4000) /api/chat for AI processing
|
|
1195
1195
|
// Format: OpenAI-compatible chat completions with message history
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1196
|
+
// Sanitize: BFF expects alternating user/assistant roles, starting with user.
|
|
1197
|
+
// Drop orphaned assistant messages at the start (from web UI SSE tap).
|
|
1198
|
+
let chatHistory = session.messages
|
|
1199
|
+
.map((m) => ({ role: m.role, content: m.content }))
|
|
1200
|
+
.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "system");
|
|
1201
|
+
// Ensure first non-system message is 'user' — drop leading assistant messages
|
|
1202
|
+
while (chatHistory.length > 0 && chatHistory[0].role === "assistant") {
|
|
1203
|
+
chatHistory.shift();
|
|
1204
|
+
}
|
|
1205
|
+
// Collapse consecutive same-role messages (merge them)
|
|
1206
|
+
chatHistory = chatHistory.reduce((acc, msg) => {
|
|
1207
|
+
if (acc.length > 0 && acc[acc.length - 1].role === msg.role) {
|
|
1208
|
+
acc[acc.length - 1].content += "\n" + msg.content;
|
|
1209
|
+
} else {
|
|
1210
|
+
acc.push({ ...msg });
|
|
1211
|
+
}
|
|
1212
|
+
return acc;
|
|
1213
|
+
}, []);
|
|
1214
|
+
// Must have at least one user message
|
|
1215
|
+
if (chatHistory.length === 0 || chatHistory[0].role !== "user") {
|
|
1216
|
+
chatHistory.unshift({ role: "user", content: message });
|
|
1217
|
+
}
|
|
1200
1218
|
const proxyBody = JSON.stringify({
|
|
1201
1219
|
messages: chatHistory,
|
|
1202
1220
|
stream: true,
|
|
@@ -1217,6 +1235,7 @@ function handleRequest(req, res) {
|
|
|
1217
1235
|
};
|
|
1218
1236
|
|
|
1219
1237
|
console.log(` ${DIM}→ Relaying to BFF /api/chat (port ${ocUIPort}) with ${chatHistory.length} messages${relayCookie ? " +cookie" : " NO-COOKIE"}${RESET}`);
|
|
1238
|
+
console.log(` ${DIM} Body: ${proxyBody.substring(0, 300)}${proxyBody.length > 300 ? "..." : ""}${RESET}`);
|
|
1220
1239
|
|
|
1221
1240
|
// Helper: send the relay request (with optional retry after login)
|
|
1222
1241
|
function sendBFFRelay(opts, body, retryCount = 0) {
|
|
@@ -1341,7 +1360,15 @@ function handleRequest(req, res) {
|
|
|
1341
1360
|
if (!isSSE && sseBuffer) {
|
|
1342
1361
|
try {
|
|
1343
1362
|
const jsonBody = JSON.parse(sseBuffer);
|
|
1344
|
-
|
|
1363
|
+
// Check for error response (BFF returns 400/500 with error details)
|
|
1364
|
+
if (proxyRes.statusCode >= 400) {
|
|
1365
|
+
const errMsg = jsonBody.error?.message || jsonBody.error || jsonBody.message || JSON.stringify(jsonBody);
|
|
1366
|
+
console.log(` ${RED}✗${RESET} BFF error ${proxyRes.statusCode}: ${String(errMsg).substring(0, 300)}`);
|
|
1367
|
+
// Send error to Navigator so user sees it
|
|
1368
|
+
fullText = `⚠️ Error from AI: ${String(errMsg).substring(0, 200)}`;
|
|
1369
|
+
} else {
|
|
1370
|
+
fullText = jsonBody.choices?.[0]?.message?.content || jsonBody.response || jsonBody.message || jsonBody.text || "";
|
|
1371
|
+
}
|
|
1345
1372
|
} catch {
|
|
1346
1373
|
console.log(` ${DIM}BFF returned non-JSON (${sseBuffer.length} bytes): ${sseBuffer.substring(0, 200)}${RESET}`);
|
|
1347
1374
|
}
|
|
@@ -1359,7 +1386,7 @@ function handleRequest(req, res) {
|
|
|
1359
1386
|
});
|
|
1360
1387
|
console.log(` ${GREEN}✓${RESET} AI response (${fullText.length} chars): ${fullText.substring(0, 80)}...`);
|
|
1361
1388
|
} else {
|
|
1362
|
-
console.log(` ${DIM}BFF returned no content${RESET}`);
|
|
1389
|
+
console.log(` ${DIM}BFF returned no content (status ${proxyRes.statusCode})${RESET}`);
|
|
1363
1390
|
}
|
|
1364
1391
|
});
|
|
1365
1392
|
|
|
@@ -1640,16 +1667,15 @@ function handleRequest(req, res) {
|
|
|
1640
1667
|
res.writeHead(200, headers);
|
|
1641
1668
|
res.end(JSON.stringify(result));
|
|
1642
1669
|
|
|
1643
|
-
//
|
|
1670
|
+
// Broadcast via WebSocket for live updates (but DON'T store in
|
|
1671
|
+
// bridge chat session — web UI manages its own conversation state
|
|
1672
|
+
// separately from sidepane chat. Mixing them causes 400 errors from
|
|
1673
|
+
// the BFF due to malformed message history.)
|
|
1644
1674
|
if (fullText) {
|
|
1645
|
-
const session = getChatSession("main");
|
|
1646
|
-
session.messages.push({ role: "assistant", content: fullText, timestamp: Date.now() });
|
|
1647
|
-
|
|
1648
1675
|
broadcastToWS({
|
|
1649
|
-
type: "chat.
|
|
1676
|
+
type: "chat.webui",
|
|
1650
1677
|
text: fullText,
|
|
1651
1678
|
content: fullText,
|
|
1652
|
-
sessionKey: "main",
|
|
1653
1679
|
role: "assistant",
|
|
1654
1680
|
timestamp: Date.now(),
|
|
1655
1681
|
});
|
|
@@ -1703,6 +1729,23 @@ async function registerWithRelay(code, url, token, name) {
|
|
|
1703
1729
|
});
|
|
1704
1730
|
clearTimeout(timeout);
|
|
1705
1731
|
const data = await res.json();
|
|
1732
|
+
|
|
1733
|
+
// Also register with token as the lookup key — so Navigator can
|
|
1734
|
+
// resolve by saved token even after the pairing code rotates.
|
|
1735
|
+
// Uses first 8 chars of token as a "token code" (avoids relay key limits).
|
|
1736
|
+
const tokenCode = `T${token.substring(0, 7)}`;
|
|
1737
|
+
try {
|
|
1738
|
+
const c2 = new AbortController();
|
|
1739
|
+
const t2 = setTimeout(() => c2.abort(), 5000);
|
|
1740
|
+
await fetch(`${RELAY_URL}/register`, {
|
|
1741
|
+
method: "POST",
|
|
1742
|
+
headers: { "Content-Type": "application/json" },
|
|
1743
|
+
body: JSON.stringify({ code: tokenCode, url, token, name }),
|
|
1744
|
+
signal: c2.signal,
|
|
1745
|
+
});
|
|
1746
|
+
clearTimeout(t2);
|
|
1747
|
+
} catch { /* non-critical */ }
|
|
1748
|
+
|
|
1706
1749
|
return data.ok === true;
|
|
1707
1750
|
} catch {
|
|
1708
1751
|
return false;
|
|
@@ -1927,25 +1970,30 @@ module.exports = {
|
|
|
1927
1970
|
await killPort(ocUIPort);
|
|
1928
1971
|
startWebUI();
|
|
1929
1972
|
|
|
1930
|
-
// ── Step 2:
|
|
1931
|
-
//
|
|
1932
|
-
//
|
|
1933
|
-
//
|
|
1973
|
+
// ── Step 2: Identity — persistent token, rotating code ──────────────
|
|
1974
|
+
// Token is persistent: Navigator stores it after first pairing and uses
|
|
1975
|
+
// it for all future requests. This is the real auth credential.
|
|
1976
|
+
// Pairing code rotates every startup: it's a one-time handshake to
|
|
1977
|
+
// exchange the token. A new code every time means nobody can reuse
|
|
1978
|
+
// an old code to hijack the connection.
|
|
1934
1979
|
const displayName = hostname().replace(/\.local$/, "");
|
|
1935
1980
|
let token;
|
|
1936
1981
|
|
|
1937
1982
|
const savedIdentity = freshIdentity ? null : loadBridgeIdentity();
|
|
1938
1983
|
if (savedIdentity) {
|
|
1939
|
-
|
|
1984
|
+
// Keep the token (Navigator already has it) but rotate the code
|
|
1940
1985
|
token = savedIdentity.token;
|
|
1941
1986
|
validTokens.add(token);
|
|
1942
|
-
|
|
1987
|
+
pairingCode = generatePairingCode(); // fresh code every startup
|
|
1988
|
+
saveBridgeIdentity(pairingCode, token, displayName);
|
|
1989
|
+
ok(`Token restored (Navigator will auto-reconnect)`);
|
|
1990
|
+
ok(`New pairing code: ${BOLD}${GREEN}${pairingCode}${RESET} (rotated for security)`);
|
|
1943
1991
|
} else {
|
|
1944
1992
|
token = randomUUID().replace(/-/g, "");
|
|
1945
1993
|
validTokens.add(token);
|
|
1946
1994
|
pairingCode = generatePairingCode();
|
|
1947
1995
|
saveBridgeIdentity(pairingCode, token, displayName);
|
|
1948
|
-
ok(`New pairing code
|
|
1996
|
+
ok(`New pairing code: ${BOLD}${GREEN}${pairingCode}${RESET}`);
|
|
1949
1997
|
}
|
|
1950
1998
|
|
|
1951
1999
|
let gatewayURL = `http://localhost:${port}`;
|