@mindexec/cli 0.2.113 → 0.2.115
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/package.json +2 -2
- package/remote-hub.js +17 -0
- package/scripts/remote-frame-ws-smoke.mjs +195 -0
- package/server.js +200 -29
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +1 -1
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +330 -1
- package/wwwroot/index.html +3 -3
- package/wwwroot/service-worker-assets.js +4 -4
- package/wwwroot/service-worker.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindexec/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.115",
|
|
4
4
|
"description": "MindExec local runtime and bridge CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "node launch-bridge.cjs",
|
|
22
22
|
"dev": "node launch-bridge.cjs --watch",
|
|
23
|
-
"test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs",
|
|
23
|
+
"test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs",
|
|
24
24
|
"test:auth": "node scripts/auth-session-smoke.mjs",
|
|
25
25
|
"test:remote": "node scripts/remote-hub-smoke.mjs",
|
|
26
26
|
"test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
|
package/remote-hub.js
CHANGED
|
@@ -601,6 +601,7 @@ export function createRemoteHub(options = {}) {
|
|
|
601
601
|
const logEvent = options.logEvent || (() => {});
|
|
602
602
|
const logWarn = options.logWarn || (() => {});
|
|
603
603
|
const emitEvent = options.emitEvent || (() => {});
|
|
604
|
+
const emitFrame = typeof options.emitFrame === 'function' ? options.emitFrame : (() => {});
|
|
604
605
|
|
|
605
606
|
const enabled = isEnabledValue(env.MINDEXEC_REMOTE_HUB ?? env.REMOTE_HUB_ENABLED, true)
|
|
606
607
|
&& !isDisabledValue(env.REMOTE_HUB_DISABLED);
|
|
@@ -985,6 +986,14 @@ export function createRemoteHub(options = {}) {
|
|
|
985
986
|
});
|
|
986
987
|
device.latestLiveFrame = frame;
|
|
987
988
|
rememberRecentFramePayload(device, 'live', frame);
|
|
989
|
+
emitFrame({
|
|
990
|
+
kind: 'live',
|
|
991
|
+
deviceId: device.deviceId,
|
|
992
|
+
frame: serializeRemoteFrame(frame, device.deviceId, 'live', { includeDataUrl: false }),
|
|
993
|
+
payload: frame.payload,
|
|
994
|
+
mimeType: frame.mimeType,
|
|
995
|
+
byteLength: frame.byteLength
|
|
996
|
+
});
|
|
988
997
|
device.activeLiveStream.lastFrameAt = frame.receivedAt;
|
|
989
998
|
device.activeLiveStream.lastFrameSeq = frame.frameSeq;
|
|
990
999
|
device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
|
|
@@ -1780,6 +1789,14 @@ export function createRemoteHub(options = {}) {
|
|
|
1780
1789
|
accessToken: createFrameAccessToken()
|
|
1781
1790
|
};
|
|
1782
1791
|
rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
|
|
1792
|
+
emitFrame({
|
|
1793
|
+
kind: 'live',
|
|
1794
|
+
deviceId: device.deviceId,
|
|
1795
|
+
frame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', { includeDataUrl: false }),
|
|
1796
|
+
payload: device.latestLiveFrame.payload,
|
|
1797
|
+
mimeType: device.latestLiveFrame.mimeType,
|
|
1798
|
+
byteLength: device.latestLiveFrame.byteLength
|
|
1799
|
+
});
|
|
1783
1800
|
device.activeLiveStream.lastFrameAt = device.lastSeenAt;
|
|
1784
1801
|
device.activeLiveStream.lastFrameSeq = frameSeq;
|
|
1785
1802
|
device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { WebSocket } from 'ws';
|
|
9
|
+
|
|
10
|
+
const BRIDGE_TOKEN = 'remote-frame-ws-smoke-token';
|
|
11
|
+
const PAIR_TOKEN = 'remote-frame-ws-pair-token';
|
|
12
|
+
|
|
13
|
+
function wait(ms) {
|
|
14
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function findFreePort() {
|
|
18
|
+
return await new Promise((resolve, reject) => {
|
|
19
|
+
const server = net.createServer();
|
|
20
|
+
server.unref();
|
|
21
|
+
server.once('error', reject);
|
|
22
|
+
server.listen(0, '127.0.0.1', () => {
|
|
23
|
+
const address = server.address();
|
|
24
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
25
|
+
server.close(() => resolve(port));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function fetchJson(url, options = {}) {
|
|
31
|
+
const response = await fetch(url, {
|
|
32
|
+
...options,
|
|
33
|
+
headers: {
|
|
34
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
35
|
+
'X-Bridge-Token': BRIDGE_TOKEN,
|
|
36
|
+
...(options.headers || {})
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const payload = await response.json().catch(() => null);
|
|
40
|
+
return { ok: response.ok, status: response.status, payload };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function waitForBridge(baseUrl, details) {
|
|
44
|
+
const startedAt = Date.now();
|
|
45
|
+
while (Date.now() - startedAt < 30000) {
|
|
46
|
+
try {
|
|
47
|
+
const result = await fetchJson(`${baseUrl}/api/remote/status`);
|
|
48
|
+
if (result.ok && result.payload?.started === true) {
|
|
49
|
+
return result.payload;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Server is still starting.
|
|
53
|
+
}
|
|
54
|
+
await wait(100);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new Error(`Timed out waiting for LocalBridge.\n${details()}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function spawnBridge({ bridgePort, remoteHubPort }) {
|
|
61
|
+
const bridgeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
62
|
+
const env = {
|
|
63
|
+
...process.env,
|
|
64
|
+
BRIDGE_PORT: String(bridgePort),
|
|
65
|
+
BRIDGE_TOKEN,
|
|
66
|
+
BRIDGE_REQUIRE_TOKEN: '1',
|
|
67
|
+
MINDEXEC_REMOTE_HUB: '1',
|
|
68
|
+
MINDEXEC_REMOTE_SYNTHETIC_FLEET: '1',
|
|
69
|
+
REMOTE_HUB_HOST: '127.0.0.1',
|
|
70
|
+
REMOTE_HUB_PORT: String(remoteHubPort),
|
|
71
|
+
REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
|
|
72
|
+
NO_COLOR: '1'
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const child = spawn(process.execPath, ['server.js'], {
|
|
76
|
+
cwd: bridgeRoot,
|
|
77
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
env
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let stdout = '';
|
|
83
|
+
let stderr = '';
|
|
84
|
+
child.stdout.on('data', chunk => {
|
|
85
|
+
stdout += chunk.toString();
|
|
86
|
+
});
|
|
87
|
+
child.stderr.on('data', chunk => {
|
|
88
|
+
stderr += chunk.toString();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const exitPromise = new Promise(resolve => child.once('exit', resolve));
|
|
92
|
+
const stop = async () => {
|
|
93
|
+
if (child.exitCode === null && !child.killed) {
|
|
94
|
+
child.kill('SIGTERM');
|
|
95
|
+
await Promise.race([exitPromise, wait(5000)]);
|
|
96
|
+
if (child.exitCode === null && !child.killed) {
|
|
97
|
+
child.kill('SIGKILL');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
baseUrl: `http://127.0.0.1:${bridgePort}`,
|
|
104
|
+
details: () => `stdout=${stdout}\nstderr=${stderr}`,
|
|
105
|
+
stop
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseFramePacket(data) {
|
|
110
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
111
|
+
assert.ok(buffer.length > 8, 'binary frame packet should include header, metadata, and payload');
|
|
112
|
+
const metaLength = buffer.readUInt32BE(0);
|
|
113
|
+
assert.ok(metaLength > 0 && metaLength < 64 * 1024, 'metadata length should be bounded');
|
|
114
|
+
assert.ok(4 + metaLength < buffer.length, 'packet should include binary payload after metadata');
|
|
115
|
+
const metadata = JSON.parse(buffer.subarray(4, 4 + metaLength).toString('utf8'));
|
|
116
|
+
const payload = buffer.subarray(4 + metaLength);
|
|
117
|
+
return { metadata, payload };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function waitForBinaryFrame(ws) {
|
|
121
|
+
return await new Promise((resolve, reject) => {
|
|
122
|
+
const timeout = setTimeout(() => reject(new Error('timed out waiting for binary frame')), 10000);
|
|
123
|
+
ws.on('message', data => {
|
|
124
|
+
if (typeof data === 'string') {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const raw = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
128
|
+
if (raw[0] === 0x7b || raw[0] === 0x5b) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
try {
|
|
133
|
+
resolve(parseFramePacket(raw));
|
|
134
|
+
} catch (error) {
|
|
135
|
+
reject(error);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
ws.on('error', reject);
|
|
139
|
+
ws.on('close', () => reject(new Error('websocket closed before binary frame')));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function main() {
|
|
144
|
+
const bridgePort = await findFreePort();
|
|
145
|
+
const remoteHubPort = await findFreePort();
|
|
146
|
+
const bridge = spawnBridge({ bridgePort, remoteHubPort });
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const status = await waitForBridge(bridge.baseUrl, bridge.details);
|
|
150
|
+
assert.equal(status.started, true);
|
|
151
|
+
|
|
152
|
+
const seed = await fetchJson(`${bridge.baseUrl}/api/remote/synthetic/seed`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
body: JSON.stringify({ count: 4, connectedRatio: 1, liveRatio: 1 })
|
|
155
|
+
});
|
|
156
|
+
assert.equal(seed.ok, true);
|
|
157
|
+
|
|
158
|
+
const devicesResult = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
|
|
159
|
+
assert.equal(devicesResult.ok, true);
|
|
160
|
+
const device = devicesResult.payload?.devices?.find(item => item.connected === true);
|
|
161
|
+
assert.ok(device?.deviceId, 'expected connected synthetic device');
|
|
162
|
+
|
|
163
|
+
const ws = new WebSocket(`${bridge.baseUrl.replace(/^http:/, 'ws:')}/api/remote/frames/ws?token=${encodeURIComponent(BRIDGE_TOKEN)}`);
|
|
164
|
+
await new Promise((resolve, reject) => {
|
|
165
|
+
ws.once('open', resolve);
|
|
166
|
+
ws.once('error', reject);
|
|
167
|
+
});
|
|
168
|
+
ws.send(JSON.stringify({ type: 'subscribe', deviceIds: [device.deviceId] }));
|
|
169
|
+
|
|
170
|
+
const framePromise = waitForBinaryFrame(ws);
|
|
171
|
+
const live = await fetchJson(`${bridge.baseUrl}/api/remote/devices/${encodeURIComponent(device.deviceId)}/live/start`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
body: JSON.stringify({ fps: 10 })
|
|
174
|
+
});
|
|
175
|
+
assert.equal(live.ok, true);
|
|
176
|
+
|
|
177
|
+
const frame = await framePromise;
|
|
178
|
+
assert.equal(frame.metadata.type, 'remote.frame.binary');
|
|
179
|
+
assert.equal(frame.metadata.kind, 'live');
|
|
180
|
+
assert.equal(frame.metadata.deviceId, device.deviceId);
|
|
181
|
+
assert.ok(frame.metadata.frameSeq > 0, 'frameSeq should be positive');
|
|
182
|
+
assert.equal(frame.metadata.mimeType, 'image/png');
|
|
183
|
+
assert.ok(frame.payload.length > 0, 'payload should be non-empty');
|
|
184
|
+
ws.close();
|
|
185
|
+
|
|
186
|
+
console.log('Remote frame WebSocket smoke OK');
|
|
187
|
+
} finally {
|
|
188
|
+
await bridge.stop();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main().catch(error => {
|
|
193
|
+
console.error(error);
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
});
|
package/server.js
CHANGED
|
@@ -2120,28 +2120,41 @@ app.get('/api/local-files/:id', async (req, res) => {
|
|
|
2120
2120
|
|
|
2121
2121
|
const httpServer = createServer(app);
|
|
2122
2122
|
const wss = new WebSocketServer({ noServer: true });
|
|
2123
|
+
const remoteFrameWss = new WebSocketServer({ noServer: true });
|
|
2123
2124
|
const wsClients = new Set();
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
const
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2125
|
+
const remoteFrameClients = new Set();
|
|
2126
|
+
const shellJobs = new Map();
|
|
2127
|
+
|
|
2128
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
2129
|
+
try {
|
|
2130
|
+
const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
2131
|
+
const token = parsed.searchParams.get('token') || '';
|
|
2132
|
+
|
|
2133
|
+
if (parsed.pathname === '/events' && token === wsToken) {
|
|
2134
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
2135
|
+
wss.emit('connection', ws, req);
|
|
2136
|
+
});
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
if (parsed.pathname === '/api/remote/frames/ws'
|
|
2141
|
+
&& (!bridgeAuthRequired || token === bridgeToken)) {
|
|
2142
|
+
remoteFrameWss.handleUpgrade(req, socket, head, (ws) => {
|
|
2143
|
+
remoteFrameWss.emit('connection', ws, req);
|
|
2144
|
+
});
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
{
|
|
2149
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
2150
|
+
socket.destroy();
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
} catch {
|
|
2154
|
+
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
2155
|
+
socket.destroy();
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2145
2158
|
|
|
2146
2159
|
wss.on('connection', (ws) => {
|
|
2147
2160
|
wsClients.add(ws);
|
|
@@ -2154,11 +2167,168 @@ wss.on('connection', (ws) => {
|
|
|
2154
2167
|
projectRoot: projectSession.rootPath,
|
|
2155
2168
|
fileCount: projectSession.symbolsByFile.size
|
|
2156
2169
|
}
|
|
2157
|
-
}));
|
|
2158
|
-
});
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2170
|
+
}));
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
function normalizeRemoteFrameWsDeviceIds(value) {
|
|
2174
|
+
const raw = [];
|
|
2175
|
+
if (Array.isArray(value)) {
|
|
2176
|
+
raw.push(...value);
|
|
2177
|
+
} else if (typeof value === 'string') {
|
|
2178
|
+
raw.push(...value.split(','));
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
const ids = [];
|
|
2182
|
+
const seen = new Set();
|
|
2183
|
+
for (const entry of raw) {
|
|
2184
|
+
const id = String(entry || '').trim();
|
|
2185
|
+
if (!id || seen.has(id)) {
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
seen.add(id);
|
|
2190
|
+
ids.push(id);
|
|
2191
|
+
if (ids.length >= 240) {
|
|
2192
|
+
break;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
return ids;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
function sendRemoteFrameClientJson(ws, payload) {
|
|
2200
|
+
if (!ws || ws.readyState !== 1) {
|
|
2201
|
+
return false;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
try {
|
|
2205
|
+
ws.send(JSON.stringify(payload));
|
|
2206
|
+
return true;
|
|
2207
|
+
} catch {
|
|
2208
|
+
return false;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
function updateRemoteFrameClientSubscription(ws, payload = {}) {
|
|
2213
|
+
const deviceIds = normalizeRemoteFrameWsDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
|
|
2214
|
+
ws.remoteFrameDeviceIds = new Set(deviceIds);
|
|
2215
|
+
ws.remoteFrameSubscriptionUpdatedAt = new Date().toISOString();
|
|
2216
|
+
sendRemoteFrameClientJson(ws, {
|
|
2217
|
+
type: 'RemoteFrameSubscription',
|
|
2218
|
+
timestamp: ws.remoteFrameSubscriptionUpdatedAt,
|
|
2219
|
+
deviceIds,
|
|
2220
|
+
mode: deviceIds.length > 0 ? 'selected-devices' : 'all-devices'
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
function buildRemoteFrameBinaryPacket(frameEvent) {
|
|
2225
|
+
const payload = frameEvent?.payload;
|
|
2226
|
+
if (!Buffer.isBuffer(payload) || payload.length === 0) {
|
|
2227
|
+
return null;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
const frame = frameEvent.frame || {};
|
|
2231
|
+
const metadata = {
|
|
2232
|
+
type: 'remote.frame.binary',
|
|
2233
|
+
kind: frameEvent.kind || 'live',
|
|
2234
|
+
deviceId: frameEvent.deviceId || frame.deviceId || '',
|
|
2235
|
+
frameSeq: Number(frame.frameSeq || 0) || 0,
|
|
2236
|
+
streamId: frame.streamId || '',
|
|
2237
|
+
width: Number(frame.width || 0) || 0,
|
|
2238
|
+
height: Number(frame.height || 0) || 0,
|
|
2239
|
+
mimeType: frameEvent.mimeType || frame.mimeType || frame.format || 'image/jpeg',
|
|
2240
|
+
mode: frame.mode || '',
|
|
2241
|
+
fps: Number(frame.fps || 0) || 0,
|
|
2242
|
+
capturedAt: frame.capturedAt || '',
|
|
2243
|
+
receivedAt: frame.receivedAt || '',
|
|
2244
|
+
byteLength: Number(frameEvent.byteLength || payload.length) || payload.length,
|
|
2245
|
+
transport: frame.transport || '',
|
|
2246
|
+
contentHash: frame.contentHash || '',
|
|
2247
|
+
captureMs: Number(frame.captureMs || 0) || 0,
|
|
2248
|
+
sameContentStreak: Number(frame.sameContentStreak || 0) || 0
|
|
2249
|
+
};
|
|
2250
|
+
const metaBuffer = Buffer.from(JSON.stringify(metadata), 'utf8');
|
|
2251
|
+
const header = Buffer.allocUnsafe(4);
|
|
2252
|
+
header.writeUInt32BE(metaBuffer.length, 0);
|
|
2253
|
+
return Buffer.concat([header, metaBuffer, payload], header.length + metaBuffer.length + payload.length);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function broadcastRemoteBinaryFrame(frameEvent) {
|
|
2257
|
+
if (remoteFrameClients.size === 0) {
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
const deviceId = String(frameEvent?.deviceId || frameEvent?.frame?.deviceId || '').trim();
|
|
2262
|
+
if (!deviceId) {
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
const packet = buildRemoteFrameBinaryPacket(frameEvent);
|
|
2267
|
+
if (!packet) {
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
for (const client of remoteFrameClients) {
|
|
2272
|
+
if (client.readyState !== 1) {
|
|
2273
|
+
continue;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
const ids = client.remoteFrameDeviceIds;
|
|
2277
|
+
if (ids instanceof Set && ids.size > 0 && !ids.has(deviceId)) {
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
if (client.bufferedAmount > 8 * 1024 * 1024) {
|
|
2282
|
+
client.remoteFrameDroppedCount = (client.remoteFrameDroppedCount || 0) + 1;
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
try {
|
|
2287
|
+
client.send(packet, { binary: true });
|
|
2288
|
+
client.remoteFrameSentCount = (client.remoteFrameSentCount || 0) + 1;
|
|
2289
|
+
} catch {
|
|
2290
|
+
client.remoteFrameDroppedCount = (client.remoteFrameDroppedCount || 0) + 1;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
remoteFrameWss.on('connection', (ws, req) => {
|
|
2296
|
+
remoteFrameClients.add(ws);
|
|
2297
|
+
ws.binaryType = 'arraybuffer';
|
|
2298
|
+
try {
|
|
2299
|
+
const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
2300
|
+
updateRemoteFrameClientSubscription(ws, {
|
|
2301
|
+
deviceIds: parsed.searchParams.get('devices') || ''
|
|
2302
|
+
});
|
|
2303
|
+
} catch {
|
|
2304
|
+
updateRemoteFrameClientSubscription(ws, {});
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
ws.on('message', data => {
|
|
2308
|
+
try {
|
|
2309
|
+
const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data || '');
|
|
2310
|
+
const payload = JSON.parse(text);
|
|
2311
|
+
if (payload?.type === 'subscribe') {
|
|
2312
|
+
updateRemoteFrameClientSubscription(ws, payload);
|
|
2313
|
+
}
|
|
2314
|
+
} catch {
|
|
2315
|
+
sendRemoteFrameClientJson(ws, {
|
|
2316
|
+
type: 'RemoteFrameSubscriptionError',
|
|
2317
|
+
error: 'invalid-message'
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
ws.on('close', () => remoteFrameClients.delete(ws));
|
|
2322
|
+
ws.on('error', () => remoteFrameClients.delete(ws));
|
|
2323
|
+
sendRemoteFrameClientJson(ws, {
|
|
2324
|
+
type: 'RemoteFrameSocketReady',
|
|
2325
|
+
timestamp: new Date().toISOString(),
|
|
2326
|
+
protocol: 'mindexec.remote.frames.binary.v1'
|
|
2327
|
+
});
|
|
2328
|
+
});
|
|
2329
|
+
|
|
2330
|
+
// Helper: Validate path is within workspace
|
|
2331
|
+
function validatePath(requestedPath) {
|
|
2162
2332
|
const fullPath = resolveWorkspaceBoundPath(requestedPath);
|
|
2163
2333
|
if (!isPathWithin(workspacePath, fullPath)) {
|
|
2164
2334
|
throw new Error('Path must be within workspace');
|
|
@@ -2726,6 +2896,7 @@ const remoteHub = createRemoteHub({
|
|
|
2726
2896
|
logWarn,
|
|
2727
2897
|
logError,
|
|
2728
2898
|
emitEvent: emitBridgeEvent,
|
|
2899
|
+
emitFrame: broadcastRemoteBinaryFrame,
|
|
2729
2900
|
managerPackage: BRIDGE_PACKAGE_NAME,
|
|
2730
2901
|
managerVersion: BRIDGE_VERSION,
|
|
2731
2902
|
hostInstanceId: BRIDGE_INSTANCE_ID
|
|
@@ -3136,7 +3307,6 @@ async function startRemoteAgentConnection(options = {}) {
|
|
|
3136
3307
|
|
|
3137
3308
|
const activeConnectionKeys = new Set(managers.map(manager => createRemoteAgentConnectionKey(manager, leaseId)));
|
|
3138
3309
|
if (isRemoteAgentProcessRunning()
|
|
3139
|
-
&& remoteAgentState.ready === true
|
|
3140
3310
|
&& activeConnectionKeys.has(remoteAgentState.connectionKey)) {
|
|
3141
3311
|
return { ok: true, alreadyRunning: true, agent: serializeRemoteAgentState() };
|
|
3142
3312
|
}
|
|
@@ -7773,8 +7943,9 @@ app.get('/api/status', async (req, res) => {
|
|
|
7773
7943
|
status: 'ok',
|
|
7774
7944
|
version: BRIDGE_VERSION,
|
|
7775
7945
|
workspace: workspacePath,
|
|
7776
|
-
wsToken,
|
|
7777
|
-
wsPath: '/events',
|
|
7946
|
+
wsToken,
|
|
7947
|
+
wsPath: '/events',
|
|
7948
|
+
remoteFrameWsPath: '/api/remote/frames/ws',
|
|
7778
7949
|
bridgeToken,
|
|
7779
7950
|
bridgeTokenHeader,
|
|
7780
7951
|
bridgeAuthRequired,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const DEBUG = false;
|
|
6
6
|
const FPS_DEBUG = false;
|
|
7
7
|
const FRAME_PERF_DEBUG = false;
|
|
8
|
-
const MINDMAP_CORE_BUILD_ID = '20260616-remote-
|
|
8
|
+
const MINDMAP_CORE_BUILD_ID = '20260616-remote-ws-binary-frames-v572';
|
|
9
9
|
const CanvasPhase = Object.freeze({
|
|
10
10
|
Booting: 'booting',
|
|
11
11
|
BoardFileLoading: 'board-file-loading',
|
|
@@ -13351,13 +13351,17 @@
|
|
|
13351
13351
|
const REMOTE_FLEET_FRAME_BLOB_CACHE_LIMIT = 96;
|
|
13352
13352
|
const REMOTE_FLEET_LIVE_FRAME_DECODE_TIMEOUT_MS = 180;
|
|
13353
13353
|
const REMOTE_FLEET_THUMBNAIL_FRAME_DECODE_TIMEOUT_MS = 1200;
|
|
13354
|
+
const REMOTE_FLEET_BINARY_FRAME_WS_RECONNECT_MS = 1500;
|
|
13355
|
+
const REMOTE_FLEET_BINARY_FRAME_SUBSCRIBE_MS = 1000;
|
|
13354
13356
|
const REMOTE_FLEET_TASK_FOLLOW_INITIAL_MS = 250;
|
|
13355
13357
|
const REMOTE_FLEET_TASK_FOLLOW_REFRESH_MS = 2000;
|
|
13356
13358
|
const REMOTE_FLEET_TASK_FOLLOW_MAX_TICKS = 60;
|
|
13357
13359
|
const REMOTE_FLEET_HOST_LEASE_REFRESH_MS = 10000;
|
|
13358
13360
|
const remoteFleetHostLeaseTimers = new Map();
|
|
13359
13361
|
const remoteFleetLocalHostTargets = new Map();
|
|
13362
|
+
const remoteFleetBinaryFrameSessions = new Map();
|
|
13360
13363
|
const remoteFleetFrameBlobCache = new Map();
|
|
13364
|
+
let remoteFleetBridgeStatusPromise = null;
|
|
13361
13365
|
let activeRemoteFleetControlPopup = null;
|
|
13362
13366
|
|
|
13363
13367
|
function findRemoteFleetBodyByNodeId(nodeId) {
|
|
@@ -13495,6 +13499,7 @@
|
|
|
13495
13499
|
|
|
13496
13500
|
function clearRemoteFleetTimers(bodyView) {
|
|
13497
13501
|
if (!bodyView) return;
|
|
13502
|
+
releaseRemoteFleetBinaryFrameSocket(bodyView);
|
|
13498
13503
|
if (bodyView._remoteFleetLiveRefreshTimer) {
|
|
13499
13504
|
clearInterval(bodyView._remoteFleetLiveRefreshTimer);
|
|
13500
13505
|
bodyView._remoteFleetLiveRefreshTimer = null;
|
|
@@ -13519,6 +13524,323 @@
|
|
|
13519
13524
|
}
|
|
13520
13525
|
}
|
|
13521
13526
|
|
|
13527
|
+
function getRemoteFleetBridgeBaseUrlCandidates() {
|
|
13528
|
+
const candidates = [];
|
|
13529
|
+
const push = value => {
|
|
13530
|
+
const text = String(value || '').trim().replace(/\/+$/, '');
|
|
13531
|
+
if (text && !candidates.includes(text)) {
|
|
13532
|
+
candidates.push(text);
|
|
13533
|
+
}
|
|
13534
|
+
};
|
|
13535
|
+
|
|
13536
|
+
push(window.MindExecLocalBridge?.activeBridgeUrl);
|
|
13537
|
+
push(window.location?.origin);
|
|
13538
|
+
return candidates;
|
|
13539
|
+
}
|
|
13540
|
+
|
|
13541
|
+
async function getRemoteFleetBridgeStatusForFrames() {
|
|
13542
|
+
if (remoteFleetBridgeStatusPromise) {
|
|
13543
|
+
return remoteFleetBridgeStatusPromise;
|
|
13544
|
+
}
|
|
13545
|
+
|
|
13546
|
+
remoteFleetBridgeStatusPromise = (async () => {
|
|
13547
|
+
const nonce = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
13548
|
+
let lastError = null;
|
|
13549
|
+
for (const baseUrl of getRemoteFleetBridgeBaseUrlCandidates()) {
|
|
13550
|
+
try {
|
|
13551
|
+
const response = await fetch(`${baseUrl}/api/status?remoteFrames=ws&cb=${encodeURIComponent(nonce)}`, {
|
|
13552
|
+
cache: 'no-store',
|
|
13553
|
+
credentials: 'omit'
|
|
13554
|
+
});
|
|
13555
|
+
if (!response.ok) {
|
|
13556
|
+
lastError = new Error(`status:${response.status}`);
|
|
13557
|
+
continue;
|
|
13558
|
+
}
|
|
13559
|
+
|
|
13560
|
+
const status = await response.json();
|
|
13561
|
+
status.__remoteFleetBridgeBaseUrl = baseUrl;
|
|
13562
|
+
return status;
|
|
13563
|
+
} catch (error) {
|
|
13564
|
+
lastError = error;
|
|
13565
|
+
}
|
|
13566
|
+
}
|
|
13567
|
+
|
|
13568
|
+
throw lastError || new Error('bridge-status-unavailable');
|
|
13569
|
+
})();
|
|
13570
|
+
|
|
13571
|
+
try {
|
|
13572
|
+
return await remoteFleetBridgeStatusPromise;
|
|
13573
|
+
} catch (error) {
|
|
13574
|
+
remoteFleetBridgeStatusPromise = null;
|
|
13575
|
+
throw error;
|
|
13576
|
+
}
|
|
13577
|
+
}
|
|
13578
|
+
|
|
13579
|
+
function buildRemoteFleetBinaryFrameWsUrl(status) {
|
|
13580
|
+
const baseUrl = String(status?.__remoteFleetBridgeBaseUrl || window.location?.origin || '').trim();
|
|
13581
|
+
const path = String(status?.remoteFrameWsPath || '/api/remote/frames/ws').trim() || '/api/remote/frames/ws';
|
|
13582
|
+
if (!baseUrl) {
|
|
13583
|
+
return '';
|
|
13584
|
+
}
|
|
13585
|
+
|
|
13586
|
+
const url = new URL(path, baseUrl);
|
|
13587
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
13588
|
+
const bridgeToken = String(status?.bridgeToken || '').trim();
|
|
13589
|
+
if (bridgeToken) {
|
|
13590
|
+
url.searchParams.set('token', bridgeToken);
|
|
13591
|
+
}
|
|
13592
|
+
return url.toString();
|
|
13593
|
+
}
|
|
13594
|
+
|
|
13595
|
+
async function parseRemoteFleetBinaryFrameMessage(data) {
|
|
13596
|
+
let buffer = null;
|
|
13597
|
+
if (data instanceof ArrayBuffer) {
|
|
13598
|
+
buffer = data;
|
|
13599
|
+
} else if (data?.arrayBuffer && typeof data.arrayBuffer === 'function') {
|
|
13600
|
+
buffer = await data.arrayBuffer();
|
|
13601
|
+
}
|
|
13602
|
+
|
|
13603
|
+
if (!buffer || buffer.byteLength < 5) {
|
|
13604
|
+
return null;
|
|
13605
|
+
}
|
|
13606
|
+
|
|
13607
|
+
const view = new DataView(buffer);
|
|
13608
|
+
const metaLength = view.getUint32(0);
|
|
13609
|
+
if (!Number.isFinite(metaLength)
|
|
13610
|
+
|| metaLength <= 0
|
|
13611
|
+
|| metaLength > 64 * 1024
|
|
13612
|
+
|| 4 + metaLength >= buffer.byteLength) {
|
|
13613
|
+
return null;
|
|
13614
|
+
}
|
|
13615
|
+
|
|
13616
|
+
const metadataBytes = new Uint8Array(buffer, 4, metaLength);
|
|
13617
|
+
const metadataText = new TextDecoder().decode(metadataBytes);
|
|
13618
|
+
const metadata = JSON.parse(metadataText);
|
|
13619
|
+
if (String(metadata?.type || '').toLowerCase() !== 'remote.frame.binary') {
|
|
13620
|
+
return null;
|
|
13621
|
+
}
|
|
13622
|
+
|
|
13623
|
+
const payload = buffer.slice(4 + metaLength);
|
|
13624
|
+
const mimeType = String(metadata.mimeType || metadata.format || 'image/jpeg').trim() || 'image/jpeg';
|
|
13625
|
+
const objectUrl = URL.createObjectURL(new Blob([payload], { type: mimeType }));
|
|
13626
|
+
return {
|
|
13627
|
+
...metadata,
|
|
13628
|
+
kind: String(metadata.kind || 'live').toLowerCase() === 'thumbnail' ? 'thumbnail' : 'live',
|
|
13629
|
+
mimeType,
|
|
13630
|
+
frameUrl: objectUrl,
|
|
13631
|
+
framePath: objectUrl,
|
|
13632
|
+
dataUrl: '',
|
|
13633
|
+
_remoteFleetObjectUrl: objectUrl,
|
|
13634
|
+
_remoteFleetBinaryFrame: true
|
|
13635
|
+
};
|
|
13636
|
+
}
|
|
13637
|
+
|
|
13638
|
+
function releaseRemoteFleetFrameObjectUrl(preview, nextObjectUrl = '') {
|
|
13639
|
+
const previous = preview?._remoteFleetObjectUrl || '';
|
|
13640
|
+
if (previous && previous !== nextObjectUrl && typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function') {
|
|
13641
|
+
try {
|
|
13642
|
+
URL.revokeObjectURL(previous);
|
|
13643
|
+
} catch {
|
|
13644
|
+
// Best-effort browser memory cleanup.
|
|
13645
|
+
}
|
|
13646
|
+
}
|
|
13647
|
+
|
|
13648
|
+
if (preview) {
|
|
13649
|
+
preview._remoteFleetObjectUrl = nextObjectUrl || '';
|
|
13650
|
+
}
|
|
13651
|
+
}
|
|
13652
|
+
|
|
13653
|
+
function getRemoteFleetBinaryFrameDeviceIds(getDeviceIds) {
|
|
13654
|
+
const ids = typeof getDeviceIds === 'function'
|
|
13655
|
+
? getDeviceIds()
|
|
13656
|
+
: [];
|
|
13657
|
+
return Array.from(new Set((Array.isArray(ids) ? ids : [])
|
|
13658
|
+
.map(value => String(value || '').trim())
|
|
13659
|
+
.filter(Boolean)))
|
|
13660
|
+
.slice(0, 240);
|
|
13661
|
+
}
|
|
13662
|
+
|
|
13663
|
+
function refreshRemoteFleetBinaryFrameSubscription(session, force = false) {
|
|
13664
|
+
if (!session?.ws || session.ws.readyState !== WebSocket.OPEN) {
|
|
13665
|
+
return false;
|
|
13666
|
+
}
|
|
13667
|
+
|
|
13668
|
+
const deviceIds = getRemoteFleetBinaryFrameDeviceIds(session.getDeviceIds);
|
|
13669
|
+
const key = deviceIds.join('\n');
|
|
13670
|
+
if (!force && session.subscriptionKey === key) {
|
|
13671
|
+
return true;
|
|
13672
|
+
}
|
|
13673
|
+
|
|
13674
|
+
session.subscriptionKey = key;
|
|
13675
|
+
session.ws.send(JSON.stringify({
|
|
13676
|
+
type: 'subscribe',
|
|
13677
|
+
nodeId: session.nodeId,
|
|
13678
|
+
deviceIds
|
|
13679
|
+
}));
|
|
13680
|
+
return true;
|
|
13681
|
+
}
|
|
13682
|
+
|
|
13683
|
+
function scheduleRemoteFleetBinaryFrameReconnect(session) {
|
|
13684
|
+
if (!session || session.reconnectTimer || session.bodies.size === 0) {
|
|
13685
|
+
return;
|
|
13686
|
+
}
|
|
13687
|
+
|
|
13688
|
+
session.reconnectTimer = setTimeout(() => {
|
|
13689
|
+
session.reconnectTimer = null;
|
|
13690
|
+
openRemoteFleetBinaryFrameSocket(session);
|
|
13691
|
+
}, REMOTE_FLEET_BINARY_FRAME_WS_RECONNECT_MS);
|
|
13692
|
+
}
|
|
13693
|
+
|
|
13694
|
+
async function openRemoteFleetBinaryFrameSocket(session) {
|
|
13695
|
+
if (!session || session.bodies.size === 0 || typeof WebSocket !== 'function') {
|
|
13696
|
+
return false;
|
|
13697
|
+
}
|
|
13698
|
+
|
|
13699
|
+
if (session.ws && (session.ws.readyState === WebSocket.OPEN || session.ws.readyState === WebSocket.CONNECTING)) {
|
|
13700
|
+
refreshRemoteFleetBinaryFrameSubscription(session);
|
|
13701
|
+
return true;
|
|
13702
|
+
}
|
|
13703
|
+
|
|
13704
|
+
try {
|
|
13705
|
+
const status = await getRemoteFleetBridgeStatusForFrames();
|
|
13706
|
+
const wsUrl = buildRemoteFleetBinaryFrameWsUrl(status);
|
|
13707
|
+
if (!wsUrl) {
|
|
13708
|
+
throw new Error('remote-frame-ws-url-unavailable');
|
|
13709
|
+
}
|
|
13710
|
+
|
|
13711
|
+
const ws = new WebSocket(wsUrl);
|
|
13712
|
+
session.ws = ws;
|
|
13713
|
+
session.subscriptionKey = '';
|
|
13714
|
+
ws.binaryType = 'arraybuffer';
|
|
13715
|
+
ws.onopen = () => {
|
|
13716
|
+
window.RuntimeTrace?.emit?.('remote.frame.wsOpen', { nodeId: session.nodeId });
|
|
13717
|
+
refreshRemoteFleetBinaryFrameSubscription(session, true);
|
|
13718
|
+
if (!session.subscriptionTimer) {
|
|
13719
|
+
session.subscriptionTimer = setInterval(
|
|
13720
|
+
() => refreshRemoteFleetBinaryFrameSubscription(session),
|
|
13721
|
+
REMOTE_FLEET_BINARY_FRAME_SUBSCRIBE_MS
|
|
13722
|
+
);
|
|
13723
|
+
}
|
|
13724
|
+
};
|
|
13725
|
+
ws.onmessage = event => {
|
|
13726
|
+
if (typeof event.data === 'string') {
|
|
13727
|
+
window.RuntimeTrace?.emit?.('remote.frame.wsMeta', {
|
|
13728
|
+
nodeId: session.nodeId,
|
|
13729
|
+
message: event.data.slice(0, 120)
|
|
13730
|
+
});
|
|
13731
|
+
return;
|
|
13732
|
+
}
|
|
13733
|
+
|
|
13734
|
+
parseRemoteFleetBinaryFrameMessage(event.data)
|
|
13735
|
+
.then(frame => {
|
|
13736
|
+
if (!frame || session.bodies.size === 0) {
|
|
13737
|
+
if (frame?._remoteFleetObjectUrl && typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function') {
|
|
13738
|
+
URL.revokeObjectURL(frame._remoteFleetObjectUrl);
|
|
13739
|
+
}
|
|
13740
|
+
return;
|
|
13741
|
+
}
|
|
13742
|
+
|
|
13743
|
+
let applied = 0;
|
|
13744
|
+
session.bodies.forEach(body => {
|
|
13745
|
+
if (document.body.contains(body)) {
|
|
13746
|
+
applied += applyRemoteFleetFramePatches(body, [frame]);
|
|
13747
|
+
}
|
|
13748
|
+
});
|
|
13749
|
+
|
|
13750
|
+
if (applied <= 0 && frame._remoteFleetObjectUrl && typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function') {
|
|
13751
|
+
URL.revokeObjectURL(frame._remoteFleetObjectUrl);
|
|
13752
|
+
}
|
|
13753
|
+
})
|
|
13754
|
+
.catch(error => {
|
|
13755
|
+
window.RuntimeTrace?.emit?.('remote.frame.wsParseFailed', {
|
|
13756
|
+
nodeId: session.nodeId,
|
|
13757
|
+
error: error?.message || String(error || '')
|
|
13758
|
+
});
|
|
13759
|
+
});
|
|
13760
|
+
};
|
|
13761
|
+
ws.onclose = () => {
|
|
13762
|
+
if (session.ws === ws) {
|
|
13763
|
+
session.ws = null;
|
|
13764
|
+
}
|
|
13765
|
+
window.RuntimeTrace?.emit?.('remote.frame.wsClosed', { nodeId: session.nodeId });
|
|
13766
|
+
scheduleRemoteFleetBinaryFrameReconnect(session);
|
|
13767
|
+
};
|
|
13768
|
+
ws.onerror = () => {
|
|
13769
|
+
try {
|
|
13770
|
+
ws.close();
|
|
13771
|
+
} catch {
|
|
13772
|
+
// Ignore WebSocket close failures.
|
|
13773
|
+
}
|
|
13774
|
+
};
|
|
13775
|
+
return true;
|
|
13776
|
+
} catch (error) {
|
|
13777
|
+
window.RuntimeTrace?.emit?.('remote.frame.wsOpenFailed', {
|
|
13778
|
+
nodeId: session.nodeId,
|
|
13779
|
+
error: error?.message || String(error || '')
|
|
13780
|
+
});
|
|
13781
|
+
scheduleRemoteFleetBinaryFrameReconnect(session);
|
|
13782
|
+
return false;
|
|
13783
|
+
}
|
|
13784
|
+
}
|
|
13785
|
+
|
|
13786
|
+
function startRemoteFleetBinaryFrameSocket(bodyView, getDeviceIds) {
|
|
13787
|
+
const nodeId = String(bodyView?.dataset?.nodeId || '').trim();
|
|
13788
|
+
if (!nodeId || typeof WebSocket !== 'function') {
|
|
13789
|
+
return false;
|
|
13790
|
+
}
|
|
13791
|
+
|
|
13792
|
+
let session = remoteFleetBinaryFrameSessions.get(nodeId);
|
|
13793
|
+
if (!session) {
|
|
13794
|
+
session = {
|
|
13795
|
+
nodeId,
|
|
13796
|
+
bodies: new Set(),
|
|
13797
|
+
ws: null,
|
|
13798
|
+
reconnectTimer: null,
|
|
13799
|
+
subscriptionTimer: null,
|
|
13800
|
+
subscriptionKey: '',
|
|
13801
|
+
getDeviceIds: null
|
|
13802
|
+
};
|
|
13803
|
+
remoteFleetBinaryFrameSessions.set(nodeId, session);
|
|
13804
|
+
}
|
|
13805
|
+
|
|
13806
|
+
session.bodies.add(bodyView);
|
|
13807
|
+
session.getDeviceIds = getDeviceIds;
|
|
13808
|
+
bodyView._remoteFleetBinaryFrameSession = session;
|
|
13809
|
+
openRemoteFleetBinaryFrameSocket(session).catch(() => undefined);
|
|
13810
|
+
return true;
|
|
13811
|
+
}
|
|
13812
|
+
|
|
13813
|
+
function releaseRemoteFleetBinaryFrameSocket(bodyView) {
|
|
13814
|
+
const session = bodyView?._remoteFleetBinaryFrameSession;
|
|
13815
|
+
if (!session) {
|
|
13816
|
+
return;
|
|
13817
|
+
}
|
|
13818
|
+
|
|
13819
|
+
delete bodyView._remoteFleetBinaryFrameSession;
|
|
13820
|
+
session.bodies.delete(bodyView);
|
|
13821
|
+
if (session.bodies.size > 0) {
|
|
13822
|
+
return;
|
|
13823
|
+
}
|
|
13824
|
+
|
|
13825
|
+
if (session.reconnectTimer) {
|
|
13826
|
+
clearTimeout(session.reconnectTimer);
|
|
13827
|
+
session.reconnectTimer = null;
|
|
13828
|
+
}
|
|
13829
|
+
if (session.subscriptionTimer) {
|
|
13830
|
+
clearInterval(session.subscriptionTimer);
|
|
13831
|
+
session.subscriptionTimer = null;
|
|
13832
|
+
}
|
|
13833
|
+
if (session.ws) {
|
|
13834
|
+
try {
|
|
13835
|
+
session.ws.close();
|
|
13836
|
+
} catch {
|
|
13837
|
+
// Ignore WebSocket close failures.
|
|
13838
|
+
}
|
|
13839
|
+
session.ws = null;
|
|
13840
|
+
}
|
|
13841
|
+
remoteFleetBinaryFrameSessions.delete(session.nodeId);
|
|
13842
|
+
}
|
|
13843
|
+
|
|
13522
13844
|
function requestRemoteFleetFrameLoopFrame(callback) {
|
|
13523
13845
|
if (typeof requestAnimationFrame === 'function') {
|
|
13524
13846
|
return {
|
|
@@ -14021,6 +14343,7 @@
|
|
|
14021
14343
|
}
|
|
14022
14344
|
|
|
14023
14345
|
const nextSeq = Number(frame.frameSeq || 0);
|
|
14346
|
+
releaseRemoteFleetFrameObjectUrl(preview, frame._remoteFleetObjectUrl || '');
|
|
14024
14347
|
preview.dataset.remoteFleetFrameKind = frame.kind;
|
|
14025
14348
|
preview.dataset.remoteFleetFrameSeq = String(nextSeq || 0);
|
|
14026
14349
|
preview.dataset.remoteFleetFrameUrl = frame.frameUrl;
|
|
@@ -17086,7 +17409,13 @@
|
|
|
17086
17409
|
error: error?.message || String(error || '')
|
|
17087
17410
|
});
|
|
17088
17411
|
});
|
|
17089
|
-
|
|
17412
|
+
const binaryFrameSocketStarted = startRemoteFleetBinaryFrameSocket(bodyView, () => {
|
|
17413
|
+
const liveIds = getVisibleLiveFrameDeviceIds();
|
|
17414
|
+
return liveIds.length > 0 ? liveIds : getVisibleFrameDeviceIds();
|
|
17415
|
+
});
|
|
17416
|
+
if (!binaryFrameSocketStarted) {
|
|
17417
|
+
startRemoteFleetFrameLoop(bodyView, REMOTE_FLEET_LIVE_FRAME_REFRESH_MS, () => refreshRemoteFleetVisibleLiveFrames());
|
|
17418
|
+
}
|
|
17090
17419
|
bodyView._remoteFleetLiveRefreshTimer = setInterval(async () => {
|
|
17091
17420
|
if (!document.body.contains(bodyView)) {
|
|
17092
17421
|
clearRemoteFleetTimers(bodyView);
|
package/wwwroot/index.html
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
<title>MindExec | Run your ideas as AI task graphs</title>
|
|
8
8
|
<meta name="description" content="MindExec is an AI execution canvas for solo builders, researchers, developers, and creators. Start with free browser tools, then move serious work into saved MindCanvas projects." />
|
|
9
9
|
<base href="/" />
|
|
10
|
-
<link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260616-remote-
|
|
11
|
-
<link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-remote-
|
|
10
|
+
<link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260616-remote-ws-binary-frames-v572" />
|
|
11
|
+
<link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-remote-ws-binary-frames-v572" />
|
|
12
12
|
<!-- ?쇄뼹??Font Awesome (local) ?쇄뼹??-->
|
|
13
13
|
<link rel="stylesheet" href="_content/MindExecution.Shared/lib/font-awesome/css/all.min.css" />
|
|
14
14
|
<!-- ?꿎뼯??-->
|
|
@@ -579,7 +579,7 @@
|
|
|
579
579
|
}
|
|
580
580
|
|
|
581
581
|
const base = '_content/MindExecution.Shared/js/';
|
|
582
|
-
const scriptVersion = '20260616-remote-
|
|
582
|
+
const scriptVersion = '20260616-remote-ws-binary-frames-v572';
|
|
583
583
|
const scriptUrl = (script) => `${base}${script}?v=${scriptVersion}`;
|
|
584
584
|
console.log(`[Script Loader] Shared JS version: ${scriptVersion}`);
|
|
585
585
|
const criticalScripts = [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
self.assetsManifest = {
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "DOWuXy9Z",
|
|
3
3
|
"assets": [
|
|
4
4
|
{
|
|
5
5
|
"hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"url": "_content/MindExecution.Shared/js/marked.min.js"
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
|
-
"hash": "sha256-
|
|
81
|
+
"hash": "sha256-HZDEcs9fkSz0iNXD6WOKZwOj8VrjJD+r4oRH9XBQhjA=",
|
|
82
82
|
"url": "_content/MindExecution.Shared/js/mind-map-core.js"
|
|
83
83
|
},
|
|
84
84
|
{
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
|
-
"hash": "sha256-
|
|
89
|
+
"hash": "sha256-KgIwiH8bPKxBAXnomKCDPKRgD/hL6CSeV2nM5EpyMdQ=",
|
|
90
90
|
"url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
|
|
91
91
|
},
|
|
92
92
|
{
|
|
@@ -834,7 +834,7 @@
|
|
|
834
834
|
"url": "image-manifest.json"
|
|
835
835
|
},
|
|
836
836
|
{
|
|
837
|
-
"hash": "sha256-
|
|
837
|
+
"hash": "sha256-ANyJXyCBiGthYmrg5ygLEqNR6Vuu+eXOi+fQ/da00pM=",
|
|
838
838
|
"url": "index.html"
|
|
839
839
|
},
|
|
840
840
|
{
|