openclaw-navigator 5.5.3 → 5.6.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 +110 -59
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -362,6 +362,19 @@ function validateBridgeAuth(req) {
|
|
|
362
362
|
return validTokens.has(token);
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
// ── Chat session storage (in-memory) ────────────────────────────────────
|
|
366
|
+
// Stores messages per session key. Navigator sends user messages here,
|
|
367
|
+
// the OC agent reads them via navigator_get_chat_messages MCP tool,
|
|
368
|
+
// and pushes responses via navigator_chat_respond MCP tool.
|
|
369
|
+
const chatSessions = new Map();
|
|
370
|
+
|
|
371
|
+
function getChatSession(sessionKey = "main") {
|
|
372
|
+
if (!chatSessions.has(sessionKey)) {
|
|
373
|
+
chatSessions.set(sessionKey, { messages: [], createdAt: Date.now() });
|
|
374
|
+
}
|
|
375
|
+
return chatSessions.get(sessionKey);
|
|
376
|
+
}
|
|
377
|
+
|
|
365
378
|
// ── WebSocket server for chat (minimal, no dependencies) ────────────────
|
|
366
379
|
// Tracks connected WebSocket clients. When the OC agent pushes messages
|
|
367
380
|
// via /api/sessions/respond or /api/sessions/stream, we broadcast to all
|
|
@@ -1069,6 +1082,13 @@ function handleRequest(req, res) {
|
|
|
1069
1082
|
sendJSON(res, 400, { ok: false, error: "Missing 'content' or 'message'" });
|
|
1070
1083
|
return;
|
|
1071
1084
|
}
|
|
1085
|
+
// Store assistant message in local session
|
|
1086
|
+
const session = getChatSession(sessionKey);
|
|
1087
|
+
session.messages.push({
|
|
1088
|
+
role: "assistant",
|
|
1089
|
+
content: message,
|
|
1090
|
+
timestamp: Date.now(),
|
|
1091
|
+
});
|
|
1072
1092
|
// Broadcast as a final chat message via WebSocket
|
|
1073
1093
|
broadcastToWS({
|
|
1074
1094
|
type: "chat.final",
|
|
@@ -1116,23 +1136,54 @@ function handleRequest(req, res) {
|
|
|
1116
1136
|
return;
|
|
1117
1137
|
}
|
|
1118
1138
|
|
|
1119
|
-
// ──
|
|
1120
|
-
//
|
|
1121
|
-
//
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1139
|
+
// ── Chat send: store locally + relay to OC Gateway for processing ──────
|
|
1140
|
+
// HYBRID approach:
|
|
1141
|
+
// 1. Store user message in bridge (for MCP tools to read via history)
|
|
1142
|
+
// 2. Relay to OC Gateway (port 18789) so the AI agent processes it
|
|
1143
|
+
// 3. Gateway may return response inline (JSON/SSE) → store + broadcast
|
|
1144
|
+
// 4. Or agent responds async via navigator_chat_respond MCP tool
|
|
1125
1145
|
if (path === "/api/sessions/send" && req.method === "POST") {
|
|
1126
1146
|
readBody(req)
|
|
1127
1147
|
.then((bodyStr) => {
|
|
1148
|
+
let data;
|
|
1149
|
+
try {
|
|
1150
|
+
data = JSON.parse(bodyStr);
|
|
1151
|
+
} catch {
|
|
1152
|
+
sendJSON(res, 400, { ok: false, error: "Invalid JSON" });
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const message = data.message || data.content || data.text || "";
|
|
1157
|
+
const sessionKey = data.sessionKey || "main";
|
|
1158
|
+
if (!message) {
|
|
1159
|
+
sendJSON(res, 400, { ok: false, error: "Missing 'message'" });
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// 1. Store user message locally
|
|
1164
|
+
const session = getChatSession(sessionKey);
|
|
1165
|
+
session.messages.push({
|
|
1166
|
+
role: "user",
|
|
1167
|
+
content: message,
|
|
1168
|
+
timestamp: Date.now(),
|
|
1169
|
+
});
|
|
1170
|
+
broadcastToWS({
|
|
1171
|
+
type: "chat.user",
|
|
1172
|
+
text: message,
|
|
1173
|
+
content: message,
|
|
1174
|
+
sessionKey,
|
|
1175
|
+
role: "user",
|
|
1176
|
+
timestamp: Date.now(),
|
|
1177
|
+
});
|
|
1178
|
+
console.log(` ${DIM}Chat send [${sessionKey}]: ${message.substring(0, 60)}...${RESET}`);
|
|
1179
|
+
|
|
1180
|
+
// 2. Relay to OC Gateway for AI processing
|
|
1128
1181
|
const proxyOpts = {
|
|
1129
1182
|
hostname: "127.0.0.1",
|
|
1130
1183
|
port: ocGatewayPort,
|
|
1131
|
-
path:
|
|
1184
|
+
path: `/api/sessions/send`,
|
|
1132
1185
|
method: "POST",
|
|
1133
1186
|
headers: {
|
|
1134
|
-
...req.headers,
|
|
1135
|
-
host: `127.0.0.1:${ocGatewayPort}`,
|
|
1136
1187
|
"content-type": "application/json",
|
|
1137
1188
|
"content-length": Buffer.byteLength(bodyStr),
|
|
1138
1189
|
},
|
|
@@ -1150,18 +1201,14 @@ function handleRequest(req, res) {
|
|
|
1150
1201
|
proxyRes.setEncoding("utf-8");
|
|
1151
1202
|
proxyRes.on("data", (chunk) => {
|
|
1152
1203
|
buffer += chunk;
|
|
1153
|
-
// Parse SSE lines
|
|
1154
1204
|
const lines = buffer.split("\n");
|
|
1155
|
-
buffer = lines.pop() || "";
|
|
1205
|
+
buffer = lines.pop() || "";
|
|
1156
1206
|
for (const line of lines) {
|
|
1157
1207
|
if (line.startsWith("data: ")) {
|
|
1158
1208
|
const raw = line.slice(6).trim();
|
|
1159
|
-
if (raw === "[DONE]")
|
|
1160
|
-
continue;
|
|
1161
|
-
}
|
|
1209
|
+
if (raw === "[DONE]") continue;
|
|
1162
1210
|
try {
|
|
1163
1211
|
const evt = JSON.parse(raw);
|
|
1164
|
-
// Extract text from common SSE shapes
|
|
1165
1212
|
const text =
|
|
1166
1213
|
evt.text || evt.content || evt.delta?.text || evt.delta?.content || "";
|
|
1167
1214
|
if (text) {
|
|
@@ -1170,22 +1217,18 @@ function handleRequest(req, res) {
|
|
|
1170
1217
|
type: "chat.delta",
|
|
1171
1218
|
text: fullText,
|
|
1172
1219
|
delta: text,
|
|
1173
|
-
sessionKey
|
|
1220
|
+
sessionKey,
|
|
1174
1221
|
timestamp: Date.now(),
|
|
1175
1222
|
});
|
|
1176
1223
|
}
|
|
1177
1224
|
} catch {
|
|
1178
|
-
|
|
1179
|
-
if (raw && raw !== "[DONE]") {
|
|
1180
|
-
fullText += raw;
|
|
1181
|
-
}
|
|
1225
|
+
if (raw && raw !== "[DONE]") fullText += raw;
|
|
1182
1226
|
}
|
|
1183
1227
|
}
|
|
1184
1228
|
}
|
|
1185
1229
|
});
|
|
1186
1230
|
|
|
1187
1231
|
proxyRes.on("end", () => {
|
|
1188
|
-
// Process any remaining buffer
|
|
1189
1232
|
if (buffer.startsWith("data: ")) {
|
|
1190
1233
|
const raw = buffer.slice(6).trim();
|
|
1191
1234
|
if (raw && raw !== "[DONE]") {
|
|
@@ -1197,34 +1240,36 @@ function handleRequest(req, res) {
|
|
|
1197
1240
|
}
|
|
1198
1241
|
}
|
|
1199
1242
|
}
|
|
1200
|
-
|
|
1201
1243
|
if (fullText) {
|
|
1202
|
-
//
|
|
1244
|
+
// Store assistant response
|
|
1245
|
+
session.messages.push({
|
|
1246
|
+
role: "assistant",
|
|
1247
|
+
content: fullText,
|
|
1248
|
+
timestamp: Date.now(),
|
|
1249
|
+
});
|
|
1203
1250
|
broadcastToWS({
|
|
1204
1251
|
type: "chat.final",
|
|
1205
1252
|
text: fullText,
|
|
1206
1253
|
content: fullText,
|
|
1207
|
-
sessionKey
|
|
1254
|
+
sessionKey,
|
|
1208
1255
|
role: "assistant",
|
|
1209
1256
|
timestamp: Date.now(),
|
|
1210
1257
|
});
|
|
1211
|
-
console.log(` ${DIM}SSE→WS
|
|
1258
|
+
console.log(` ${DIM}Gateway SSE→WS: ${fullText.substring(0, 80)}...${RESET}`);
|
|
1212
1259
|
}
|
|
1213
|
-
|
|
1214
|
-
// Return clean JSON to the caller (OCChatService)
|
|
1215
1260
|
sendJSON(res, 200, {
|
|
1216
1261
|
ok: true,
|
|
1262
|
+
stored: true,
|
|
1217
1263
|
response: fullText || null,
|
|
1218
|
-
|
|
1219
|
-
delivered: wsClients.size,
|
|
1264
|
+
source: "gateway-sse",
|
|
1220
1265
|
});
|
|
1221
1266
|
});
|
|
1222
1267
|
|
|
1223
1268
|
proxyRes.on("error", () => {
|
|
1224
|
-
sendJSON(res,
|
|
1269
|
+
sendJSON(res, 200, { ok: true, stored: true, response: null, source: "gateway-sse-error" });
|
|
1225
1270
|
});
|
|
1226
1271
|
} else {
|
|
1227
|
-
// ── JSON response
|
|
1272
|
+
// ── JSON response from gateway ──
|
|
1228
1273
|
const chunks = [];
|
|
1229
1274
|
proxyRes.on("data", (c) => chunks.push(c));
|
|
1230
1275
|
proxyRes.on("end", () => {
|
|
@@ -1233,42 +1278,43 @@ function handleRequest(req, res) {
|
|
|
1233
1278
|
try {
|
|
1234
1279
|
jsonBody = JSON.parse(body);
|
|
1235
1280
|
} catch {
|
|
1236
|
-
//
|
|
1237
|
-
|
|
1238
|
-
headers["access-control-allow-origin"] = "*";
|
|
1239
|
-
res.writeHead(proxyRes.statusCode ?? 200, headers);
|
|
1240
|
-
res.end(body);
|
|
1281
|
+
// Non-JSON — message is stored, agent will respond via MCP
|
|
1282
|
+
sendJSON(res, 200, { ok: true, stored: true, response: null, source: "gateway-raw" });
|
|
1241
1283
|
return;
|
|
1242
1284
|
}
|
|
1243
|
-
|
|
1244
|
-
// If the gateway returned an inline response, broadcast it via WebSocket
|
|
1285
|
+
// Check if gateway returned an inline AI response
|
|
1245
1286
|
const inlineResponse =
|
|
1246
1287
|
jsonBody.response || jsonBody.message || jsonBody.answer || jsonBody.text || "";
|
|
1247
1288
|
if (inlineResponse) {
|
|
1289
|
+
session.messages.push({
|
|
1290
|
+
role: "assistant",
|
|
1291
|
+
content: inlineResponse,
|
|
1292
|
+
timestamp: Date.now(),
|
|
1293
|
+
});
|
|
1248
1294
|
broadcastToWS({
|
|
1249
1295
|
type: "chat.final",
|
|
1250
1296
|
text: inlineResponse,
|
|
1251
1297
|
content: inlineResponse,
|
|
1252
|
-
sessionKey
|
|
1298
|
+
sessionKey,
|
|
1253
1299
|
role: "assistant",
|
|
1254
1300
|
timestamp: Date.now(),
|
|
1255
1301
|
});
|
|
1256
|
-
console.log(
|
|
1257
|
-
` ${DIM}Inline response→WS: ${inlineResponse.substring(0, 80)}...${RESET}`,
|
|
1258
|
-
);
|
|
1302
|
+
console.log(` ${DIM}Gateway inline→WS: ${inlineResponse.substring(0, 80)}...${RESET}`);
|
|
1259
1303
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1304
|
+
sendJSON(res, 200, {
|
|
1305
|
+
ok: true,
|
|
1306
|
+
stored: true,
|
|
1307
|
+
response: inlineResponse || null,
|
|
1308
|
+
source: "gateway-json",
|
|
1309
|
+
});
|
|
1262
1310
|
});
|
|
1263
1311
|
}
|
|
1264
1312
|
});
|
|
1265
1313
|
|
|
1266
1314
|
proxyReq.on("error", (err) => {
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
detail: err.message,
|
|
1271
|
-
});
|
|
1315
|
+
// Gateway not reachable — message is still stored, agent can use MCP tools
|
|
1316
|
+
console.log(` ${DIM}Gateway not reachable (${err.message}) — message stored for MCP polling${RESET}`);
|
|
1317
|
+
sendJSON(res, 200, { ok: true, stored: true, response: null, source: "local-only" });
|
|
1272
1318
|
});
|
|
1273
1319
|
|
|
1274
1320
|
proxyReq.write(bodyStr);
|
|
@@ -1278,17 +1324,22 @@ function handleRequest(req, res) {
|
|
|
1278
1324
|
return;
|
|
1279
1325
|
}
|
|
1280
1326
|
|
|
1327
|
+
// ── Chat history: return stored messages for a session ────────────────
|
|
1328
|
+
// Called by the OC agent (via navigator_get_chat_messages MCP tool)
|
|
1329
|
+
// and by Navigator's OCChatService to load history on tab switch.
|
|
1330
|
+
if (path === "/api/sessions/history" && req.method === "GET") {
|
|
1331
|
+
const sessionKey = url.searchParams?.get("sessionKey") || "main";
|
|
1332
|
+
const session = getChatSession(sessionKey);
|
|
1333
|
+
sendJSON(res, 200, { ok: true, messages: session.messages });
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1281
1337
|
// ── /api/* routing strategy ──────────────────────────────────────────────
|
|
1282
|
-
//
|
|
1283
|
-
//
|
|
1284
|
-
//
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
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)
|
|
1338
|
+
// The bridge handles chat endpoints locally:
|
|
1339
|
+
// - /api/sessions/send → store user message + WS broadcast
|
|
1340
|
+
// - /api/sessions/history → return stored messages
|
|
1341
|
+
// - /api/sessions/respond → store assistant message + WS broadcast
|
|
1342
|
+
// - /api/sessions/stream → WS broadcast (streaming delta)
|
|
1292
1343
|
//
|
|
1293
1344
|
// Everything else (/api/agents, /api/auth/*, /api/chat/*, etc.) falls through
|
|
1294
1345
|
// to the fallback proxy below, which sends it to port 4000 (web UI).
|