openclaw-navigator 5.2.0 → 5.2.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 +33 -7
- package/mcp.mjs +54 -34
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -755,6 +755,7 @@ function handleRequest(req, res) {
|
|
|
755
755
|
// Keep /ui prefix — Next.js basePath: "/ui" expects it
|
|
756
756
|
if (path === "/ui" || path.startsWith("/ui/")) {
|
|
757
757
|
const targetURL = `${path}${url.search}`;
|
|
758
|
+
const incomingHost = req.headers.host || "localhost";
|
|
758
759
|
|
|
759
760
|
const proxyOpts = {
|
|
760
761
|
hostname: "127.0.0.1",
|
|
@@ -764,15 +765,38 @@ function handleRequest(req, res) {
|
|
|
764
765
|
headers: {
|
|
765
766
|
...req.headers,
|
|
766
767
|
host: `127.0.0.1:${ocUIPort}`,
|
|
768
|
+
"x-forwarded-host": incomingHost,
|
|
769
|
+
"x-forwarded-proto": activeTunnelURL ? "https" : "http",
|
|
770
|
+
"x-forwarded-for": req.socket.remoteAddress || "127.0.0.1",
|
|
767
771
|
},
|
|
768
772
|
};
|
|
769
773
|
|
|
770
774
|
const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
|
|
771
|
-
// Forward status + headers (add CORS for cross-origin tunnel access)
|
|
772
775
|
const headers = { ...proxyRes.headers };
|
|
776
|
+
|
|
777
|
+
// CORS for cross-origin tunnel access
|
|
773
778
|
headers["access-control-allow-origin"] = "*";
|
|
774
779
|
headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, OPTIONS";
|
|
775
|
-
headers["access-control-allow-headers"] = "Content-Type, Authorization";
|
|
780
|
+
headers["access-control-allow-headers"] = "Content-Type, Authorization, Cookie";
|
|
781
|
+
headers["access-control-allow-credentials"] = "true";
|
|
782
|
+
|
|
783
|
+
// Fix redirects — rewrite Location from localhost:4000 to tunnel URL
|
|
784
|
+
if (headers.location) {
|
|
785
|
+
headers.location = headers.location
|
|
786
|
+
.replace(`http://127.0.0.1:${ocUIPort}`, "")
|
|
787
|
+
.replace(`http://localhost:${ocUIPort}`, "");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Fix cookies — remove domain restriction so they work through tunnel
|
|
791
|
+
if (headers["set-cookie"]) {
|
|
792
|
+
const cookies = Array.isArray(headers["set-cookie"])
|
|
793
|
+
? headers["set-cookie"]
|
|
794
|
+
: [headers["set-cookie"]];
|
|
795
|
+
headers["set-cookie"] = cookies.map((c) =>
|
|
796
|
+
c.replace(/;\s*domain=[^;]*/gi, "").replace(/;\s*secure/gi, ""),
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
776
800
|
res.writeHead(proxyRes.statusCode ?? 502, headers);
|
|
777
801
|
proxyRes.pipe(res, { end: true });
|
|
778
802
|
});
|
|
@@ -1119,13 +1143,14 @@ module.exports = {
|
|
|
1119
1143
|
});
|
|
1120
1144
|
|
|
1121
1145
|
// ── WebSocket proxy — forward /ws connections to OC gateway ─────────
|
|
1122
|
-
// The bridge is a transparent relay: Navigator connects here,
|
|
1123
|
-
// the OC gateway WebSocket at ws://localhost:ocGatewayPort/
|
|
1146
|
+
// The bridge is a transparent relay: Navigator connects to /ws here,
|
|
1147
|
+
// we pipe to the OC gateway WebSocket at ws://localhost:ocGatewayPort/
|
|
1148
|
+
// (OC Core accepts WebSocket at root /, not /ws)
|
|
1124
1149
|
server.on("upgrade", (req, socket, head) => {
|
|
1125
1150
|
const reqUrl = new URL(req.url ?? "/", "http://localhost");
|
|
1126
1151
|
const reqPath = reqUrl.pathname;
|
|
1127
1152
|
|
|
1128
|
-
// Only handle /ws paths
|
|
1153
|
+
// Only handle /ws paths (Navigator connects to /ws)
|
|
1129
1154
|
if (reqPath !== "/ws" && !reqPath.startsWith("/ws/")) {
|
|
1130
1155
|
socket.destroy();
|
|
1131
1156
|
return;
|
|
@@ -1133,9 +1158,10 @@ module.exports = {
|
|
|
1133
1158
|
|
|
1134
1159
|
// Connect to the gateway WebSocket
|
|
1135
1160
|
const gwSocket = netConnect(ocGatewayPort, "127.0.0.1", () => {
|
|
1136
|
-
//
|
|
1161
|
+
// Rewrite path: Navigator uses /ws but OC Core expects / (root)
|
|
1162
|
+
const gwPath = reqPath === "/ws" ? "/" : reqPath.replace(/^\/ws/, "");
|
|
1137
1163
|
const upgradeHeaders = [
|
|
1138
|
-
`${req.method} ${
|
|
1164
|
+
`${req.method} ${gwPath || "/"} HTTP/${req.httpVersion}`,
|
|
1139
1165
|
...Object.entries(req.headers).map(([k, v]) => `${k}: ${v}`),
|
|
1140
1166
|
"",
|
|
1141
1167
|
"",
|
package/mcp.mjs
CHANGED
|
@@ -63,46 +63,66 @@ function authHeaders() {
|
|
|
63
63
|
|
|
64
64
|
// ── HTTP helpers ──────────────────────────────────────────────────────────
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
const MAX_RETRIES = 3;
|
|
67
|
+
const RETRY_DELAYS = [500, 1500, 3000]; // ms — escalating backoff
|
|
68
|
+
|
|
69
|
+
/** Retry wrapper for bridge HTTP calls — handles ECONNREFUSED and 401 */
|
|
70
|
+
async function withRetry(fn, label) {
|
|
71
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
72
|
+
try {
|
|
73
|
+
return await fn();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const isConnErr =
|
|
76
|
+
err.cause?.code === "ECONNREFUSED" ||
|
|
77
|
+
err.message?.includes("ECONNREFUSED") ||
|
|
78
|
+
err.message?.includes("fetch failed");
|
|
79
|
+
const is401 = err.message?.includes("returned 401");
|
|
80
|
+
|
|
81
|
+
if (is401 && attempt === 0) {
|
|
82
|
+
// Auth mismatch — bridge may have restarted with new token
|
|
83
|
+
log(`${label}: 401 — reloading bridge token and retrying`);
|
|
84
|
+
bridgeToken = loadBridgeToken();
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isConnErr && attempt < MAX_RETRIES) {
|
|
89
|
+
const delay = RETRY_DELAYS[attempt] || 3000;
|
|
90
|
+
log(`${label}: bridge unreachable — retry ${attempt + 1}/${MAX_RETRIES} in ${delay}ms`);
|
|
91
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
92
|
+
// Also reload token in case bridge restarted with new identity
|
|
93
|
+
bridgeToken = loadBridgeToken();
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw err;
|
|
78
98
|
}
|
|
79
|
-
bridgeGet._retrying = false;
|
|
80
|
-
throw new Error(`Bridge GET ${path} returned ${res.status}`);
|
|
81
99
|
}
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function bridgeGet(path) {
|
|
103
|
+
return withRetry(async () => {
|
|
104
|
+
const res = await fetch(`${BRIDGE_URL}${path}`, {
|
|
105
|
+
headers: authHeaders(),
|
|
106
|
+
});
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
throw new Error(`Bridge GET ${path} returned ${res.status}`);
|
|
109
|
+
}
|
|
110
|
+
return res.json();
|
|
111
|
+
}, `GET ${path}`);
|
|
84
112
|
}
|
|
85
113
|
|
|
86
114
|
async function bridgePost(path, body) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
bridgePost._retrying = true;
|
|
96
|
-
bridgeToken = loadBridgeToken();
|
|
97
|
-
const retry = await bridgePost(path, body);
|
|
98
|
-
bridgePost._retrying = false;
|
|
99
|
-
return retry;
|
|
115
|
+
return withRetry(async () => {
|
|
116
|
+
const res = await fetch(`${BRIDGE_URL}${path}`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: authHeaders(),
|
|
119
|
+
body: JSON.stringify(body),
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
throw new Error(`Bridge POST ${path} returned ${res.status}`);
|
|
100
123
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
bridgePost._retrying = false;
|
|
105
|
-
return res.json();
|
|
124
|
+
return res.json();
|
|
125
|
+
}, `POST ${path}`);
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
// ── Event polling engine ──────────────────────────────────────────────────
|