@mindstudio-ai/local-model-tunnel 0.5.21 → 0.5.23
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/dist/{chunk-YTHFWJU6.js → chunk-ALEWMAQ4.js} +219 -236
- package/dist/chunk-ALEWMAQ4.js.map +1 -0
- package/dist/{chunk-VJTZSOKC.js → chunk-HZZVPY7J.js} +293 -188
- package/dist/chunk-HZZVPY7J.js.map +1 -0
- package/dist/{chunk-TMIU4S53.js → chunk-KOEOMG4Z.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/headless.js +2 -2
- package/dist/index.js +3 -3
- package/dist/{tui-TUREGKKI.js → tui-JG3DY3LK.js} +8 -12
- package/dist/{tui-TUREGKKI.js.map → tui-JG3DY3LK.js.map} +1 -1
- package/package.json +4 -2
- package/dist/chunk-VJTZSOKC.js.map +0 -1
- package/dist/chunk-YTHFWJU6.js.map +0 -1
- /package/dist/{chunk-TMIU4S53.js.map → chunk-KOEOMG4Z.js.map} +0 -0
|
@@ -442,15 +442,6 @@ var DevEventEmitter = class extends EventEmitter {
|
|
|
442
442
|
emitConnectionRestored() {
|
|
443
443
|
this.emit("dev:connection-restored");
|
|
444
444
|
}
|
|
445
|
-
emitImpersonate(event) {
|
|
446
|
-
this.emit("dev:impersonate", event);
|
|
447
|
-
}
|
|
448
|
-
emitScenarioStart(event) {
|
|
449
|
-
this.emit("dev:scenario-start", event);
|
|
450
|
-
}
|
|
451
|
-
emitScenarioComplete(event) {
|
|
452
|
-
this.emit("dev:scenario-complete", event);
|
|
453
|
-
}
|
|
454
445
|
onStart(handler) {
|
|
455
446
|
this.on("dev:start", handler);
|
|
456
447
|
return () => this.off("dev:start", handler);
|
|
@@ -483,18 +474,6 @@ var DevEventEmitter = class extends EventEmitter {
|
|
|
483
474
|
this.on("dev:connection-restored", handler);
|
|
484
475
|
return () => this.off("dev:connection-restored", handler);
|
|
485
476
|
}
|
|
486
|
-
onImpersonate(handler) {
|
|
487
|
-
this.on("dev:impersonate", handler);
|
|
488
|
-
return () => this.off("dev:impersonate", handler);
|
|
489
|
-
}
|
|
490
|
-
onScenarioStart(handler) {
|
|
491
|
-
this.on("dev:scenario-start", handler);
|
|
492
|
-
return () => this.off("dev:scenario-start", handler);
|
|
493
|
-
}
|
|
494
|
-
onScenarioComplete(handler) {
|
|
495
|
-
this.on("dev:scenario-complete", handler);
|
|
496
|
-
return () => this.off("dev:scenario-complete", handler);
|
|
497
|
-
}
|
|
498
477
|
};
|
|
499
478
|
var devRequestEvents = new DevEventEmitter();
|
|
500
479
|
|
|
@@ -1049,17 +1028,15 @@ var DevRunner = class {
|
|
|
1049
1028
|
async setImpersonation(roles) {
|
|
1050
1029
|
if (!this.session) return;
|
|
1051
1030
|
log.info("Setting role override", { roles });
|
|
1052
|
-
|
|
1031
|
+
await impersonate(this.appId, this.session.sessionId, roles);
|
|
1053
1032
|
await this.refreshClientContext();
|
|
1054
|
-
devRequestEvents.emitImpersonate({ roles: result.roles });
|
|
1055
1033
|
}
|
|
1056
1034
|
// Clear role override — revert to session's default roles.
|
|
1057
1035
|
async clearImpersonation() {
|
|
1058
1036
|
if (!this.session) return;
|
|
1059
1037
|
log.info("Clearing role override");
|
|
1060
|
-
|
|
1038
|
+
await impersonate(this.appId, this.session.sessionId, null);
|
|
1061
1039
|
await this.refreshClientContext();
|
|
1062
|
-
devRequestEvents.emitImpersonate({ roles: result.roles });
|
|
1063
1040
|
}
|
|
1064
1041
|
// Fetch fresh clientContext from platform and update the proxy.
|
|
1065
1042
|
// Called after impersonation changes so the browser gets a new ms_iface token.
|
|
@@ -1081,12 +1058,6 @@ var DevRunner = class {
|
|
|
1081
1058
|
}
|
|
1082
1059
|
const requestId = randomBytes2(8).toString("hex");
|
|
1083
1060
|
const startTime = Date.now();
|
|
1084
|
-
devRequestEvents.emitStart({
|
|
1085
|
-
id: requestId,
|
|
1086
|
-
type: "execute",
|
|
1087
|
-
method: opts.methodExport,
|
|
1088
|
-
timestamp: startTime
|
|
1089
|
-
});
|
|
1090
1061
|
log.info("Method received (direct)", { requestId, method: opts.methodExport });
|
|
1091
1062
|
try {
|
|
1092
1063
|
const authorizationToken = await fetchCallbackToken(this.appId, this.session.sessionId);
|
|
@@ -1123,12 +1094,6 @@ var DevRunner = class {
|
|
|
1123
1094
|
result,
|
|
1124
1095
|
duration
|
|
1125
1096
|
});
|
|
1126
|
-
devRequestEvents.emitComplete({
|
|
1127
|
-
id: requestId,
|
|
1128
|
-
success: result.success,
|
|
1129
|
-
duration,
|
|
1130
|
-
error: result.error ? formatErrorForDisplay(result.error) : void 0
|
|
1131
|
-
});
|
|
1132
1097
|
return {
|
|
1133
1098
|
success: result.success,
|
|
1134
1099
|
output: result.output,
|
|
@@ -1151,12 +1116,6 @@ var DevRunner = class {
|
|
|
1151
1116
|
result: { success: false, error: { message } },
|
|
1152
1117
|
duration
|
|
1153
1118
|
});
|
|
1154
|
-
devRequestEvents.emitComplete({
|
|
1155
|
-
id: requestId,
|
|
1156
|
-
success: false,
|
|
1157
|
-
duration,
|
|
1158
|
-
error: message
|
|
1159
|
-
});
|
|
1160
1119
|
return { success: false, error: { message }, duration };
|
|
1161
1120
|
}
|
|
1162
1121
|
}
|
|
@@ -1168,11 +1127,6 @@ var DevRunner = class {
|
|
|
1168
1127
|
}
|
|
1169
1128
|
const startTime = Date.now();
|
|
1170
1129
|
const scenarioName = scenario.name ?? scenario.export;
|
|
1171
|
-
devRequestEvents.emitScenarioStart({
|
|
1172
|
-
id: scenario.id,
|
|
1173
|
-
name: scenarioName,
|
|
1174
|
-
timestamp: startTime
|
|
1175
|
-
});
|
|
1176
1130
|
log.info("Scenario starting", { id: scenario.id, name: scenarioName });
|
|
1177
1131
|
try {
|
|
1178
1132
|
log.info("Resetting database for scenario");
|
|
@@ -1202,13 +1156,6 @@ var DevRunner = class {
|
|
|
1202
1156
|
result,
|
|
1203
1157
|
duration: Date.now() - startTime
|
|
1204
1158
|
});
|
|
1205
|
-
devRequestEvents.emitScenarioComplete({
|
|
1206
|
-
id: scenario.id,
|
|
1207
|
-
success: false,
|
|
1208
|
-
duration: Date.now() - startTime,
|
|
1209
|
-
roles: scenario.roles,
|
|
1210
|
-
error
|
|
1211
|
-
});
|
|
1212
1159
|
return { success: false, databases, error };
|
|
1213
1160
|
}
|
|
1214
1161
|
if (scenario.roles.length > 0) {
|
|
@@ -1225,12 +1172,6 @@ var DevRunner = class {
|
|
|
1225
1172
|
result,
|
|
1226
1173
|
duration
|
|
1227
1174
|
});
|
|
1228
|
-
devRequestEvents.emitScenarioComplete({
|
|
1229
|
-
id: scenario.id,
|
|
1230
|
-
success: true,
|
|
1231
|
-
duration,
|
|
1232
|
-
roles: scenario.roles
|
|
1233
|
-
});
|
|
1234
1175
|
return { success: true, databases };
|
|
1235
1176
|
} catch (err) {
|
|
1236
1177
|
const error = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -1243,13 +1184,6 @@ var DevRunner = class {
|
|
|
1243
1184
|
infrastructureError: error,
|
|
1244
1185
|
duration: Date.now() - startTime
|
|
1245
1186
|
});
|
|
1246
|
-
devRequestEvents.emitScenarioComplete({
|
|
1247
|
-
id: scenario.id,
|
|
1248
|
-
success: false,
|
|
1249
|
-
duration: Date.now() - startTime,
|
|
1250
|
-
roles: scenario.roles,
|
|
1251
|
-
error
|
|
1252
|
-
});
|
|
1253
1187
|
return { success: false, databases: this.session.databases, error };
|
|
1254
1188
|
}
|
|
1255
1189
|
}
|
|
@@ -1481,7 +1415,99 @@ function closeBrowserLog() {
|
|
|
1481
1415
|
|
|
1482
1416
|
// src/dev/proxy.ts
|
|
1483
1417
|
import http from "http";
|
|
1418
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
1419
|
+
import { WebSocketServer } from "ws";
|
|
1420
|
+
|
|
1421
|
+
// src/dev/ws-clients.ts
|
|
1484
1422
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
1423
|
+
var ClientRegistry = class {
|
|
1424
|
+
clients = /* @__PURE__ */ new Map();
|
|
1425
|
+
add(ws, hello) {
|
|
1426
|
+
const id = randomBytes3(4).toString("hex");
|
|
1427
|
+
this.clients.set(id, {
|
|
1428
|
+
id,
|
|
1429
|
+
ws,
|
|
1430
|
+
mode: hello.mode,
|
|
1431
|
+
url: hello.url,
|
|
1432
|
+
viewport: hello.viewport,
|
|
1433
|
+
connectedAt: Date.now(),
|
|
1434
|
+
alive: true,
|
|
1435
|
+
activeCommandId: null
|
|
1436
|
+
});
|
|
1437
|
+
log.info("Browser client connected", { clientId: id, mode: hello.mode, url: hello.url });
|
|
1438
|
+
return id;
|
|
1439
|
+
}
|
|
1440
|
+
remove(id) {
|
|
1441
|
+
const client = this.clients.get(id);
|
|
1442
|
+
if (client) {
|
|
1443
|
+
this.clients.delete(id);
|
|
1444
|
+
log.info("Browser client disconnected", { clientId: id, mode: client.mode });
|
|
1445
|
+
}
|
|
1446
|
+
return client;
|
|
1447
|
+
}
|
|
1448
|
+
get(id) {
|
|
1449
|
+
return this.clients.get(id);
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Get the preferred client for C&C commands.
|
|
1453
|
+
* Prefers idle iframe clients, falls back to any idle client.
|
|
1454
|
+
* Skips clients that are already executing a command.
|
|
1455
|
+
*/
|
|
1456
|
+
getCommandTarget() {
|
|
1457
|
+
let fallback = null;
|
|
1458
|
+
for (const client of this.clients.values()) {
|
|
1459
|
+
if (client.activeCommandId) continue;
|
|
1460
|
+
if (client.mode === "iframe") return client;
|
|
1461
|
+
if (!fallback) fallback = client;
|
|
1462
|
+
}
|
|
1463
|
+
return fallback;
|
|
1464
|
+
}
|
|
1465
|
+
getAll() {
|
|
1466
|
+
return [...this.clients.values()];
|
|
1467
|
+
}
|
|
1468
|
+
hasConnected() {
|
|
1469
|
+
return this.clients.size > 0;
|
|
1470
|
+
}
|
|
1471
|
+
count() {
|
|
1472
|
+
return this.clients.size;
|
|
1473
|
+
}
|
|
1474
|
+
/** Find the client executing a given command. */
|
|
1475
|
+
findByCommandId(commandId) {
|
|
1476
|
+
for (const client of this.clients.values()) {
|
|
1477
|
+
if (client.activeCommandId === commandId) return client;
|
|
1478
|
+
}
|
|
1479
|
+
return void 0;
|
|
1480
|
+
}
|
|
1481
|
+
markAlive(id) {
|
|
1482
|
+
const client = this.clients.get(id);
|
|
1483
|
+
if (client) client.alive = true;
|
|
1484
|
+
}
|
|
1485
|
+
/** Mark all clients as not-alive, then ping. Clients that respond set alive=true. */
|
|
1486
|
+
pingAll() {
|
|
1487
|
+
for (const client of this.clients.values()) {
|
|
1488
|
+
client.alive = false;
|
|
1489
|
+
try {
|
|
1490
|
+
client.ws.ping();
|
|
1491
|
+
} catch {
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
/** Remove clients that didn't respond to the last ping. Returns active command IDs that need rejection. */
|
|
1496
|
+
sweepDead() {
|
|
1497
|
+
const removed = [];
|
|
1498
|
+
for (const client of this.clients.values()) {
|
|
1499
|
+
if (!client.alive) {
|
|
1500
|
+
log.warn("Browser client timed out (no pong)", { clientId: client.id, activeCommandId: client.activeCommandId });
|
|
1501
|
+
removed.push({ clientId: client.id, activeCommandId: client.activeCommandId });
|
|
1502
|
+
this.clients.delete(client.id);
|
|
1503
|
+
client.ws.terminate();
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return removed;
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
|
|
1510
|
+
// src/dev/proxy.ts
|
|
1485
1511
|
var DevProxy = class _DevProxy {
|
|
1486
1512
|
constructor(upstreamPort, clientContext, bindAddress = "127.0.0.1", browserAgentUrl) {
|
|
1487
1513
|
this.upstreamPort = upstreamPort;
|
|
@@ -1491,58 +1517,104 @@ var DevProxy = class _DevProxy {
|
|
|
1491
1517
|
}
|
|
1492
1518
|
server = null;
|
|
1493
1519
|
proxyPort = null;
|
|
1494
|
-
|
|
1520
|
+
wss = null;
|
|
1521
|
+
clients = new ClientRegistry();
|
|
1495
1522
|
pendingResults = /* @__PURE__ */ new Map();
|
|
1496
|
-
|
|
1497
|
-
/** Long-poll waiters — browser agents waiting for the next command. */
|
|
1498
|
-
commandWaiters = [];
|
|
1523
|
+
commandQueue = [];
|
|
1499
1524
|
/** Upstream dev server health tracking. */
|
|
1500
1525
|
upstreamUp = true;
|
|
1501
1526
|
healthCheckTimer = null;
|
|
1527
|
+
pingTimer = null;
|
|
1502
1528
|
static HEALTH_CHECK_INTERVAL = 3e3;
|
|
1503
1529
|
static HEALTH_CHECK_INTERVAL_DOWN = 1e3;
|
|
1504
1530
|
static HEALTH_CHECK_TIMEOUT = 2e3;
|
|
1531
|
+
static PING_INTERVAL = 3e4;
|
|
1532
|
+
static HELLO_TIMEOUT = 5e3;
|
|
1505
1533
|
updateClientContext(context) {
|
|
1506
1534
|
this.clientContext = context;
|
|
1507
1535
|
log.info("Dev proxy context updated after role change");
|
|
1508
1536
|
}
|
|
1509
1537
|
/**
|
|
1510
|
-
* Whether
|
|
1511
|
-
* True if there's a long-poll waiter or we've seen activity recently.
|
|
1538
|
+
* Whether any browser agent is actively connected via WebSocket.
|
|
1512
1539
|
*/
|
|
1513
1540
|
isBrowserConnected() {
|
|
1514
|
-
return this.
|
|
1541
|
+
return this.clients.hasConnected();
|
|
1515
1542
|
}
|
|
1516
1543
|
/**
|
|
1517
|
-
* Dispatch a browser
|
|
1518
|
-
*
|
|
1519
|
-
* Returns a promise that resolves when the browser posts the result back.
|
|
1544
|
+
* Dispatch a command to the preferred browser client and wait for the result.
|
|
1545
|
+
* Commands are queued and executed one at a time per client (FIFO).
|
|
1520
1546
|
*/
|
|
1521
1547
|
dispatchBrowserCommand(steps, timeoutMs = 3e4) {
|
|
1522
|
-
if (!this.
|
|
1548
|
+
if (!this.clients.hasConnected()) {
|
|
1523
1549
|
return Promise.reject(
|
|
1524
1550
|
new Error("No browser connected, please refresh the MindStudio preview")
|
|
1525
1551
|
);
|
|
1526
1552
|
}
|
|
1527
|
-
const id =
|
|
1528
|
-
log.info("Browser command queued", { id, stepCount: steps.length, commands: steps.map((s) => s.command) });
|
|
1553
|
+
const id = randomBytes4(4).toString("hex");
|
|
1529
1554
|
return new Promise((resolve2, reject) => {
|
|
1555
|
+
this.commandQueue.push({ id, steps, timeoutMs, resolve: resolve2, reject, queuedAt: Date.now() });
|
|
1556
|
+
log.info("Browser command queued", { id, queueLength: this.commandQueue.length, commands: steps.map((s) => s.command) });
|
|
1557
|
+
this.drainCommandQueue();
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Try to send the next queued command to an available client.
|
|
1562
|
+
*/
|
|
1563
|
+
drainCommandQueue() {
|
|
1564
|
+
while (this.commandQueue.length > 0) {
|
|
1565
|
+
const target = this.clients.getCommandTarget();
|
|
1566
|
+
if (!target) break;
|
|
1567
|
+
const queued = this.commandQueue.shift();
|
|
1568
|
+
const { id, steps, timeoutMs, resolve: resolve2, reject } = queued;
|
|
1569
|
+
log.info("Browser command sent", { id, clientId: target.id, mode: target.mode, stepCount: steps.length, commands: steps.map((s) => s.command) });
|
|
1530
1570
|
const timeout = setTimeout(() => {
|
|
1531
1571
|
this.pendingResults.delete(id);
|
|
1532
|
-
|
|
1572
|
+
const client = this.clients.findByCommandId(id);
|
|
1573
|
+
if (client) client.activeCommandId = null;
|
|
1574
|
+
log.warn("Browser command timed out", { id, pendingCount: this.pendingResults.size });
|
|
1533
1575
|
reject(new Error("Browser command timed out"));
|
|
1576
|
+
this.drainCommandQueue();
|
|
1534
1577
|
}, timeoutMs);
|
|
1535
|
-
this.pendingResults.set(id, { resolve: resolve2, timeout });
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1578
|
+
this.pendingResults.set(id, { resolve: resolve2, timeout, clientId: target.id });
|
|
1579
|
+
target.activeCommandId = id;
|
|
1580
|
+
try {
|
|
1581
|
+
target.ws.send(JSON.stringify({ type: "command", id, steps }));
|
|
1582
|
+
} catch {
|
|
1583
|
+
this.pendingResults.delete(id);
|
|
1584
|
+
clearTimeout(timeout);
|
|
1585
|
+
target.activeCommandId = null;
|
|
1586
|
+
reject(new Error("Failed to send command to browser"));
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Send a broadcast message to all connected browser clients.
|
|
1592
|
+
*/
|
|
1593
|
+
broadcastToClients(action, payload) {
|
|
1594
|
+
const msg = JSON.stringify({ type: "broadcast", action, payload });
|
|
1595
|
+
const clients = this.clients.getAll();
|
|
1596
|
+
log.info("Broadcasting to browser clients", { action, clientCount: clients.length });
|
|
1597
|
+
for (const client of clients) {
|
|
1598
|
+
try {
|
|
1599
|
+
client.ws.send(msg);
|
|
1600
|
+
} catch {
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1539
1603
|
}
|
|
1540
1604
|
async start(preferredPort) {
|
|
1541
1605
|
const server = http.createServer((req, res) => {
|
|
1542
1606
|
this.handleRequest(req, res);
|
|
1543
1607
|
});
|
|
1608
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
1609
|
+
this.wss.on("connection", (ws) => this.handleWsConnection(ws));
|
|
1544
1610
|
server.on("upgrade", (req, socket, head) => {
|
|
1545
|
-
|
|
1611
|
+
if (req.url === "/__mindstudio_dev__/ws") {
|
|
1612
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1613
|
+
this.wss.emit("connection", ws, req);
|
|
1614
|
+
});
|
|
1615
|
+
} else {
|
|
1616
|
+
this.handleUpstreamUpgrade(req, socket, head);
|
|
1617
|
+
}
|
|
1546
1618
|
});
|
|
1547
1619
|
const portsToTry = preferredPort ? [preferredPort, 0] : [0];
|
|
1548
1620
|
for (const port of portsToTry) {
|
|
@@ -1551,6 +1623,7 @@ var DevProxy = class _DevProxy {
|
|
|
1551
1623
|
this.server = server;
|
|
1552
1624
|
this.proxyPort = assignedPort;
|
|
1553
1625
|
this.startHealthCheck();
|
|
1626
|
+
this.startPingTimer();
|
|
1554
1627
|
log.info("Dev proxy started", { port: assignedPort, bind: this.bindAddress });
|
|
1555
1628
|
return assignedPort;
|
|
1556
1629
|
} catch {
|
|
@@ -1579,6 +1652,18 @@ var DevProxy = class _DevProxy {
|
|
|
1579
1652
|
}
|
|
1580
1653
|
stop() {
|
|
1581
1654
|
this.stopHealthCheck();
|
|
1655
|
+
this.stopPingTimer();
|
|
1656
|
+
for (const client of this.clients.getAll()) {
|
|
1657
|
+
try {
|
|
1658
|
+
client.ws.close(1001, "Proxy stopping");
|
|
1659
|
+
} catch {
|
|
1660
|
+
client.ws.terminate();
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (this.wss) {
|
|
1664
|
+
this.wss.close();
|
|
1665
|
+
this.wss = null;
|
|
1666
|
+
}
|
|
1582
1667
|
if (this.server) {
|
|
1583
1668
|
log.info("Dev proxy stopping");
|
|
1584
1669
|
this.server.close();
|
|
@@ -1590,19 +1675,124 @@ var DevProxy = class _DevProxy {
|
|
|
1590
1675
|
pending2.resolve({ id, steps: [], error: "Proxy stopped" });
|
|
1591
1676
|
}
|
|
1592
1677
|
this.pendingResults.clear();
|
|
1593
|
-
this.commandQueue
|
|
1594
|
-
|
|
1595
|
-
clearTimeout(waiter.timer);
|
|
1596
|
-
if (!waiter.res.writableEnded) {
|
|
1597
|
-
waiter.res.writeHead(204).end();
|
|
1598
|
-
}
|
|
1678
|
+
for (const queued of this.commandQueue) {
|
|
1679
|
+
queued.reject(new Error("Proxy stopped"));
|
|
1599
1680
|
}
|
|
1600
|
-
this.
|
|
1681
|
+
this.commandQueue.length = 0;
|
|
1601
1682
|
}
|
|
1602
1683
|
getPort() {
|
|
1603
1684
|
return this.proxyPort;
|
|
1604
1685
|
}
|
|
1605
1686
|
// ---------------------------------------------------------------------------
|
|
1687
|
+
// WebSocket connection handler
|
|
1688
|
+
// ---------------------------------------------------------------------------
|
|
1689
|
+
handleWsConnection(ws) {
|
|
1690
|
+
let clientId = null;
|
|
1691
|
+
const helloTimeout = setTimeout(() => {
|
|
1692
|
+
if (!clientId) {
|
|
1693
|
+
log.warn("Browser WS client did not send hello in time, closing");
|
|
1694
|
+
ws.close(4e3, "Hello timeout");
|
|
1695
|
+
}
|
|
1696
|
+
}, _DevProxy.HELLO_TIMEOUT);
|
|
1697
|
+
ws.on("message", (data) => {
|
|
1698
|
+
let msg;
|
|
1699
|
+
try {
|
|
1700
|
+
msg = JSON.parse(data.toString());
|
|
1701
|
+
} catch {
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (!clientId) {
|
|
1705
|
+
if (msg.type !== "hello") {
|
|
1706
|
+
ws.close(4001, "Expected hello");
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
clearTimeout(helloTimeout);
|
|
1710
|
+
const mode = msg.mode === "iframe" ? "iframe" : "standalone";
|
|
1711
|
+
const viewport = msg.viewport || { w: 0, h: 0 };
|
|
1712
|
+
clientId = this.clients.add(ws, {
|
|
1713
|
+
mode,
|
|
1714
|
+
url: String(msg.url || ""),
|
|
1715
|
+
viewport
|
|
1716
|
+
});
|
|
1717
|
+
ws.send(JSON.stringify({ type: "ack", clientId }));
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
switch (msg.type) {
|
|
1721
|
+
case "result":
|
|
1722
|
+
this.handleCommandResult(msg);
|
|
1723
|
+
break;
|
|
1724
|
+
case "log":
|
|
1725
|
+
if (Array.isArray(msg.entries)) {
|
|
1726
|
+
appendBrowserLogEntries(msg.entries);
|
|
1727
|
+
}
|
|
1728
|
+
break;
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
ws.on("pong", () => {
|
|
1732
|
+
if (clientId) this.clients.markAlive(clientId);
|
|
1733
|
+
});
|
|
1734
|
+
ws.on("close", () => {
|
|
1735
|
+
clearTimeout(helloTimeout);
|
|
1736
|
+
if (clientId) {
|
|
1737
|
+
const client = this.clients.remove(clientId);
|
|
1738
|
+
if (client?.activeCommandId) {
|
|
1739
|
+
this.rejectPendingCommand(client.activeCommandId, "Browser disconnected during command execution");
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
ws.on("error", () => {
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
handleCommandResult(msg) {
|
|
1747
|
+
const id = msg.id;
|
|
1748
|
+
if (!id) {
|
|
1749
|
+
log.warn("Browser command result received with no id");
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
const pending2 = this.pendingResults.get(id);
|
|
1753
|
+
if (pending2) {
|
|
1754
|
+
log.info("Browser command result received", { id, stepCount: msg.steps?.length, duration: msg.duration });
|
|
1755
|
+
clearTimeout(pending2.timeout);
|
|
1756
|
+
this.pendingResults.delete(id);
|
|
1757
|
+
const client = this.clients.findByCommandId(id);
|
|
1758
|
+
if (client) client.activeCommandId = null;
|
|
1759
|
+
pending2.resolve(msg);
|
|
1760
|
+
this.drainCommandQueue();
|
|
1761
|
+
} else {
|
|
1762
|
+
log.warn("Browser command result received but no pending command found", { id, pendingIds: [...this.pendingResults.keys()] });
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
rejectPendingCommand(commandId, reason) {
|
|
1766
|
+
const pending2 = this.pendingResults.get(commandId);
|
|
1767
|
+
if (pending2) {
|
|
1768
|
+
clearTimeout(pending2.timeout);
|
|
1769
|
+
this.pendingResults.delete(commandId);
|
|
1770
|
+
pending2.resolve({ id: commandId, steps: [], error: reason });
|
|
1771
|
+
log.warn("Pending command rejected", { id: commandId, reason });
|
|
1772
|
+
this.drainCommandQueue();
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
// ---------------------------------------------------------------------------
|
|
1776
|
+
// Ping/pong liveness
|
|
1777
|
+
// ---------------------------------------------------------------------------
|
|
1778
|
+
startPingTimer() {
|
|
1779
|
+
this.pingTimer = setInterval(() => {
|
|
1780
|
+
const removed = this.clients.sweepDead();
|
|
1781
|
+
for (const { activeCommandId } of removed) {
|
|
1782
|
+
if (activeCommandId) {
|
|
1783
|
+
this.rejectPendingCommand(activeCommandId, "Browser client timed out");
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
this.clients.pingAll();
|
|
1787
|
+
}, _DevProxy.PING_INTERVAL);
|
|
1788
|
+
}
|
|
1789
|
+
stopPingTimer() {
|
|
1790
|
+
if (this.pingTimer) {
|
|
1791
|
+
clearInterval(this.pingTimer);
|
|
1792
|
+
this.pingTimer = null;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
// ---------------------------------------------------------------------------
|
|
1606
1796
|
// Upstream health check
|
|
1607
1797
|
// ---------------------------------------------------------------------------
|
|
1608
1798
|
/**
|
|
@@ -1644,8 +1834,7 @@ var DevProxy = class _DevProxy {
|
|
|
1644
1834
|
log.warn("Upstream dev server is down");
|
|
1645
1835
|
} else if (!wasUp && this.upstreamUp) {
|
|
1646
1836
|
log.info("Upstream dev server is back up, reloading browser");
|
|
1647
|
-
this.
|
|
1648
|
-
});
|
|
1837
|
+
this.broadcastToClients("reload");
|
|
1649
1838
|
}
|
|
1650
1839
|
const interval = this.upstreamUp ? _DevProxy.HEALTH_CHECK_INTERVAL : _DevProxy.HEALTH_CHECK_INTERVAL_DOWN;
|
|
1651
1840
|
this.scheduleHealthCheck(interval);
|
|
@@ -1670,14 +1859,6 @@ var DevProxy = class _DevProxy {
|
|
|
1670
1859
|
this.handleBrowserLogs(clientReq, clientRes);
|
|
1671
1860
|
return;
|
|
1672
1861
|
}
|
|
1673
|
-
if (clientReq.url === "/__mindstudio_dev__/commands" && clientReq.method === "GET") {
|
|
1674
|
-
this.handleGetCommand(clientReq, clientRes);
|
|
1675
|
-
return;
|
|
1676
|
-
}
|
|
1677
|
-
if (clientReq.url === "/__mindstudio_dev__/results" && clientReq.method === "POST") {
|
|
1678
|
-
this.handlePostResult(clientReq, clientRes);
|
|
1679
|
-
return;
|
|
1680
|
-
}
|
|
1681
1862
|
if (clientReq.url?.startsWith("/__mindstudio_dev__/font-proxy?") && clientReq.method === "GET") {
|
|
1682
1863
|
this.handleFontProxy(clientReq, clientRes);
|
|
1683
1864
|
return;
|
|
@@ -1748,8 +1929,9 @@ var DevProxy = class _DevProxy {
|
|
|
1748
1929
|
clientReq.pipe(upstreamReq);
|
|
1749
1930
|
}
|
|
1750
1931
|
// ---------------------------------------------------------------------------
|
|
1751
|
-
// Browser agent endpoints
|
|
1932
|
+
// Browser agent HTTP endpoints (fallbacks)
|
|
1752
1933
|
// ---------------------------------------------------------------------------
|
|
1934
|
+
/** Accept log entries via HTTP POST — used by sendBeacon on page unload. */
|
|
1753
1935
|
handleBrowserLogs(clientReq, clientRes) {
|
|
1754
1936
|
const chunks = [];
|
|
1755
1937
|
clientReq.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -1810,86 +1992,6 @@ var DevProxy = class _DevProxy {
|
|
|
1810
1992
|
clientRes.end(`Font proxy error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1811
1993
|
}
|
|
1812
1994
|
}
|
|
1813
|
-
handleGetCommand(clientReq, clientRes) {
|
|
1814
|
-
this.lastBrowserPoll = Date.now();
|
|
1815
|
-
const command = this.commandQueue.shift();
|
|
1816
|
-
if (command) {
|
|
1817
|
-
log.info("Browser command dispatched to agent", { id: command.id, commands: command.steps.map((s) => s.command) });
|
|
1818
|
-
clientRes.writeHead(200, {
|
|
1819
|
-
...this.corsHeaders(clientReq),
|
|
1820
|
-
"content-type": "application/json",
|
|
1821
|
-
"cache-control": "no-store"
|
|
1822
|
-
});
|
|
1823
|
-
clientRes.end(JSON.stringify(command));
|
|
1824
|
-
return;
|
|
1825
|
-
}
|
|
1826
|
-
const timer = setTimeout(() => {
|
|
1827
|
-
this.removeCommandWaiter(clientRes);
|
|
1828
|
-
clientRes.writeHead(204, {
|
|
1829
|
-
...this.corsHeaders(clientReq),
|
|
1830
|
-
"cache-control": "no-store"
|
|
1831
|
-
});
|
|
1832
|
-
clientRes.end();
|
|
1833
|
-
}, 25e3);
|
|
1834
|
-
this.commandWaiters.push({ req: clientReq, res: clientRes, timer });
|
|
1835
|
-
clientReq.on("close", () => {
|
|
1836
|
-
this.removeCommandWaiter(clientRes);
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
/**
|
|
1840
|
-
* Flush a queued command to a waiting long-poll connection, if any.
|
|
1841
|
-
*/
|
|
1842
|
-
flushCommandToWaiter() {
|
|
1843
|
-
while (this.commandWaiters.length > 0 && this.commandQueue.length > 0) {
|
|
1844
|
-
const waiter = this.commandWaiters.shift();
|
|
1845
|
-
clearTimeout(waiter.timer);
|
|
1846
|
-
if (waiter.res.writableEnded) continue;
|
|
1847
|
-
const command = this.commandQueue.shift();
|
|
1848
|
-
this.lastBrowserPoll = Date.now();
|
|
1849
|
-
log.info("Browser command dispatched to agent", { id: command.id, commands: command.steps.map((s) => s.command) });
|
|
1850
|
-
waiter.res.writeHead(200, {
|
|
1851
|
-
...this.corsHeaders(waiter.req),
|
|
1852
|
-
"content-type": "application/json",
|
|
1853
|
-
"cache-control": "no-store"
|
|
1854
|
-
});
|
|
1855
|
-
waiter.res.end(JSON.stringify(command));
|
|
1856
|
-
return;
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
removeCommandWaiter(res) {
|
|
1860
|
-
const idx = this.commandWaiters.findIndex((w) => w.res === res);
|
|
1861
|
-
if (idx !== -1) {
|
|
1862
|
-
clearTimeout(this.commandWaiters[idx].timer);
|
|
1863
|
-
this.commandWaiters.splice(idx, 1);
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
handlePostResult(clientReq, clientRes) {
|
|
1867
|
-
const chunks = [];
|
|
1868
|
-
clientReq.on("data", (chunk) => chunks.push(chunk));
|
|
1869
|
-
clientReq.on("end", () => {
|
|
1870
|
-
try {
|
|
1871
|
-
const body = Buffer.concat(chunks).toString("utf-8");
|
|
1872
|
-
const result = JSON.parse(body);
|
|
1873
|
-
if (result?.id) {
|
|
1874
|
-
const pending2 = this.pendingResults.get(result.id);
|
|
1875
|
-
if (pending2) {
|
|
1876
|
-
log.info("Browser command result received", { id: result.id, stepCount: result.steps?.length, duration: result.duration });
|
|
1877
|
-
clearTimeout(pending2.timeout);
|
|
1878
|
-
this.pendingResults.delete(result.id);
|
|
1879
|
-
pending2.resolve(result);
|
|
1880
|
-
} else {
|
|
1881
|
-
log.warn("Browser command result received but no pending command found", { id: result.id, pendingIds: [...this.pendingResults.keys()] });
|
|
1882
|
-
}
|
|
1883
|
-
} else {
|
|
1884
|
-
log.warn("Browser command result received with no id", { bodyLength: body.length });
|
|
1885
|
-
}
|
|
1886
|
-
} catch (err) {
|
|
1887
|
-
log.warn("Browser command result parse error", { error: err instanceof Error ? err.message : String(err) });
|
|
1888
|
-
}
|
|
1889
|
-
clientRes.writeHead(204, this.corsHeaders(clientReq));
|
|
1890
|
-
clientRes.end();
|
|
1891
|
-
});
|
|
1892
|
-
}
|
|
1893
1995
|
/**
|
|
1894
1996
|
* Inject window.__MINDSTUDIO__ context and browser agent script tag into HTML.
|
|
1895
1997
|
*/
|
|
@@ -1905,8 +2007,11 @@ ${agentScript}`;
|
|
|
1905
2007
|
}
|
|
1906
2008
|
return injection + "\n" + html;
|
|
1907
2009
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
2010
|
+
// ---------------------------------------------------------------------------
|
|
2011
|
+
// Upstream WebSocket forwarding (HMR etc.)
|
|
2012
|
+
// ---------------------------------------------------------------------------
|
|
2013
|
+
handleUpstreamUpgrade(clientReq, clientSocket, head) {
|
|
2014
|
+
log.debug("Dev proxy WebSocket upgrade (upstream)", { path: clientReq.url });
|
|
1910
2015
|
const options = {
|
|
1911
2016
|
hostname: "127.0.0.1",
|
|
1912
2017
|
port: this.upstreamPort,
|
|
@@ -2189,4 +2294,4 @@ export {
|
|
|
2189
2294
|
watchTableFiles,
|
|
2190
2295
|
watchConfigFile
|
|
2191
2296
|
};
|
|
2192
|
-
//# sourceMappingURL=chunk-
|
|
2297
|
+
//# sourceMappingURL=chunk-HZZVPY7J.js.map
|