@btraut/browser-bridge 0.11.1 → 0.12.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/CHANGELOG.md +33 -0
- package/README.md +39 -0
- package/dist/api.js +878 -128
- package/dist/api.js.map +4 -4
- package/dist/index.js +861 -378
- package/dist/index.js.map +4 -4
- package/extension/assets/ui.css +32 -1
- package/extension/dist/background.js +207 -17
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/popup-ui.js +44 -3
- package/extension/dist/popup-ui.js.map +2 -2
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
- package/skills/browser-bridge/skill.json +1 -1
package/extension/assets/ui.css
CHANGED
|
@@ -36,7 +36,7 @@ body.bb-page {
|
|
|
36
36
|
body.bb-page.bb-page--popup {
|
|
37
37
|
height: auto;
|
|
38
38
|
padding: 10px 10px 12px;
|
|
39
|
-
min-width:
|
|
39
|
+
min-width: 300px;
|
|
40
40
|
background: var(--bb-surface);
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -68,6 +68,37 @@ body.bb-page.bb-page--popup {
|
|
|
68
68
|
padding: 2px;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
.bb-connection {
|
|
72
|
+
border: 1px solid var(--bb-border-2);
|
|
73
|
+
border-radius: var(--bb-radius-sm);
|
|
74
|
+
background: var(--bb-bg);
|
|
75
|
+
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06);
|
|
76
|
+
padding: 10px 12px;
|
|
77
|
+
margin: 0 0 10px;
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: space-between;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.bb-connection-label {
|
|
84
|
+
font-size: 14px;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
color: var(--bb-ink);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.bb-connection-dot {
|
|
90
|
+
width: 11px;
|
|
91
|
+
height: 11px;
|
|
92
|
+
border-radius: 999px;
|
|
93
|
+
border: 1px solid rgba(162, 43, 43, 0.35);
|
|
94
|
+
background: #c93939;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.bb-connection-dot[data-connected='true'] {
|
|
98
|
+
border-color: rgba(31, 111, 63, 0.4);
|
|
99
|
+
background: #2b9a52;
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
.bb-popup-head {
|
|
72
103
|
display: flex;
|
|
73
104
|
align-items: center;
|
|
@@ -410,10 +410,86 @@ var PermissionPromptController = class {
|
|
|
410
410
|
}
|
|
411
411
|
};
|
|
412
412
|
|
|
413
|
+
// packages/extension/src/connection-state.ts
|
|
414
|
+
var toIso = (ms) => new Date(ms).toISOString();
|
|
415
|
+
var ConnectionStateTracker = class {
|
|
416
|
+
constructor(now = Date.now, failureLogThrottleMs = 15e3) {
|
|
417
|
+
this.now = now;
|
|
418
|
+
this.failureLogThrottleMs = failureLogThrottleMs;
|
|
419
|
+
this.state = "disconnected";
|
|
420
|
+
this.consecutiveFailures = 0;
|
|
421
|
+
this.lastFailureLogAt = 0;
|
|
422
|
+
this.suppressedFailureLogs = 0;
|
|
423
|
+
}
|
|
424
|
+
setEndpoint(endpoint) {
|
|
425
|
+
this.endpoint = endpoint;
|
|
426
|
+
}
|
|
427
|
+
markConnecting() {
|
|
428
|
+
this.state = "connecting";
|
|
429
|
+
this.reconnectDelayMs = void 0;
|
|
430
|
+
this.retryAt = void 0;
|
|
431
|
+
}
|
|
432
|
+
markConnected() {
|
|
433
|
+
this.state = "connected";
|
|
434
|
+
this.lastConnectedAt = toIso(this.now());
|
|
435
|
+
this.reconnectDelayMs = void 0;
|
|
436
|
+
this.retryAt = void 0;
|
|
437
|
+
this.consecutiveFailures = 0;
|
|
438
|
+
}
|
|
439
|
+
markDisconnected() {
|
|
440
|
+
this.state = "disconnected";
|
|
441
|
+
this.reconnectDelayMs = void 0;
|
|
442
|
+
this.retryAt = void 0;
|
|
443
|
+
this.lastDisconnectedAt = toIso(this.now());
|
|
444
|
+
}
|
|
445
|
+
markBackoff(delayMs2) {
|
|
446
|
+
this.state = "backoff";
|
|
447
|
+
this.reconnectDelayMs = delayMs2;
|
|
448
|
+
this.retryAt = toIso(this.now() + Math.max(0, delayMs2));
|
|
449
|
+
}
|
|
450
|
+
recordFailure(message) {
|
|
451
|
+
this.lastErrorAt = toIso(this.now());
|
|
452
|
+
this.lastErrorMessage = message;
|
|
453
|
+
this.consecutiveFailures += 1;
|
|
454
|
+
}
|
|
455
|
+
consumeFailureLogBudget() {
|
|
456
|
+
const now = this.now();
|
|
457
|
+
if (this.lastFailureLogAt === 0 || now - this.lastFailureLogAt >= this.failureLogThrottleMs) {
|
|
458
|
+
const suppressedCount = this.suppressedFailureLogs;
|
|
459
|
+
this.suppressedFailureLogs = 0;
|
|
460
|
+
this.lastFailureLogAt = now;
|
|
461
|
+
return { shouldLog: true, suppressedCount };
|
|
462
|
+
}
|
|
463
|
+
this.suppressedFailureLogs += 1;
|
|
464
|
+
return { shouldLog: false, suppressedCount: this.suppressedFailureLogs };
|
|
465
|
+
}
|
|
466
|
+
flushSuppressedFailureLogs() {
|
|
467
|
+
const count = this.suppressedFailureLogs;
|
|
468
|
+
this.suppressedFailureLogs = 0;
|
|
469
|
+
return count;
|
|
470
|
+
}
|
|
471
|
+
getStatus() {
|
|
472
|
+
return {
|
|
473
|
+
state: this.state,
|
|
474
|
+
endpoint: this.endpoint,
|
|
475
|
+
ws_url: this.endpoint ? `ws://${this.endpoint.host}:${this.endpoint.port}/drive` : void 0,
|
|
476
|
+
reconnect_delay_ms: this.reconnectDelayMs,
|
|
477
|
+
retry_at: this.retryAt,
|
|
478
|
+
last_connected_at: this.lastConnectedAt,
|
|
479
|
+
last_disconnected_at: this.lastDisconnectedAt,
|
|
480
|
+
last_error_at: this.lastErrorAt,
|
|
481
|
+
last_error_message: this.lastErrorMessage,
|
|
482
|
+
consecutive_failures: this.consecutiveFailures
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
413
487
|
// packages/extension/src/background.ts
|
|
414
488
|
var DEFAULT_CORE_PORT = 3210;
|
|
415
489
|
var CORE_PORT_KEY = "corePort";
|
|
416
490
|
var CORE_WS_PATH = "/drive";
|
|
491
|
+
var CORE_HEALTH_PATH = "/health";
|
|
492
|
+
var CORE_HEALTH_TIMEOUT_MS = 1200;
|
|
417
493
|
var DEBUGGER_PROTOCOL_VERSION = "1.3";
|
|
418
494
|
var DEBUGGER_IDLE_TIMEOUT_KEY = "debuggerIdleTimeoutMs";
|
|
419
495
|
var DEFAULT_DEBUGGER_IDLE_TIMEOUT_MS = 15e3;
|
|
@@ -598,24 +674,36 @@ var renderDataUrlToFormat = async (dataUrl, format, quality) => {
|
|
|
598
674
|
bitmap.close();
|
|
599
675
|
}
|
|
600
676
|
};
|
|
601
|
-
var
|
|
677
|
+
var readCoreEndpointConfig = async () => {
|
|
602
678
|
return await new Promise((resolve) => {
|
|
603
679
|
chrome.storage.local.get(
|
|
604
680
|
[CORE_PORT_KEY],
|
|
605
681
|
(result) => {
|
|
606
682
|
const raw = result?.[CORE_PORT_KEY];
|
|
607
683
|
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
608
|
-
resolve(
|
|
684
|
+
resolve({
|
|
685
|
+
host: "127.0.0.1",
|
|
686
|
+
port: raw,
|
|
687
|
+
portSource: "storage"
|
|
688
|
+
});
|
|
609
689
|
return;
|
|
610
690
|
}
|
|
611
691
|
if (typeof raw === "string") {
|
|
612
692
|
const parsed = Number(raw);
|
|
613
693
|
if (Number.isFinite(parsed)) {
|
|
614
|
-
resolve(
|
|
694
|
+
resolve({
|
|
695
|
+
host: "127.0.0.1",
|
|
696
|
+
port: parsed,
|
|
697
|
+
portSource: "storage"
|
|
698
|
+
});
|
|
615
699
|
return;
|
|
616
700
|
}
|
|
617
701
|
}
|
|
618
|
-
resolve(
|
|
702
|
+
resolve({
|
|
703
|
+
host: "127.0.0.1",
|
|
704
|
+
port: DEFAULT_CORE_PORT,
|
|
705
|
+
portSource: "default"
|
|
706
|
+
});
|
|
619
707
|
}
|
|
620
708
|
);
|
|
621
709
|
});
|
|
@@ -1060,10 +1148,14 @@ var waitForDomContentLoaded = async (tabId, timeoutMs) => {
|
|
|
1060
1148
|
}, timeoutMs);
|
|
1061
1149
|
});
|
|
1062
1150
|
};
|
|
1063
|
-
var
|
|
1064
|
-
const
|
|
1065
|
-
return
|
|
1151
|
+
var getWsEndpoint = async () => {
|
|
1152
|
+
const endpoint = await readCoreEndpointConfig();
|
|
1153
|
+
return {
|
|
1154
|
+
endpoint,
|
|
1155
|
+
url: `ws://${endpoint.host}:${endpoint.port}${CORE_WS_PATH}`
|
|
1156
|
+
};
|
|
1066
1157
|
};
|
|
1158
|
+
var getHealthEndpoint = (endpoint) => `http://${endpoint.host}:${endpoint.port}${CORE_HEALTH_PATH}`;
|
|
1067
1159
|
var DriveSocket = class {
|
|
1068
1160
|
constructor() {
|
|
1069
1161
|
this.socket = null;
|
|
@@ -1074,10 +1166,12 @@ var DriveSocket = class {
|
|
|
1074
1166
|
this.keepAliveIntervalMs = 3e4;
|
|
1075
1167
|
this.debuggerSessions = /* @__PURE__ */ new Map();
|
|
1076
1168
|
this.debuggerIdleTimeoutMs = null;
|
|
1169
|
+
this.connection = new ConnectionStateTracker();
|
|
1077
1170
|
}
|
|
1078
1171
|
start() {
|
|
1172
|
+
this.connection.markConnecting();
|
|
1079
1173
|
void this.connect().catch((error) => {
|
|
1080
|
-
|
|
1174
|
+
this.recordConnectionFailure("initial connect", error);
|
|
1081
1175
|
});
|
|
1082
1176
|
}
|
|
1083
1177
|
stop() {
|
|
@@ -1090,6 +1184,7 @@ var DriveSocket = class {
|
|
|
1090
1184
|
this.socket.close();
|
|
1091
1185
|
this.socket = null;
|
|
1092
1186
|
}
|
|
1187
|
+
this.connection.markDisconnected();
|
|
1093
1188
|
}
|
|
1094
1189
|
sendTabReport() {
|
|
1095
1190
|
void this.emitTabReport().catch((error) => {
|
|
@@ -1101,10 +1196,12 @@ var DriveSocket = class {
|
|
|
1101
1196
|
return;
|
|
1102
1197
|
}
|
|
1103
1198
|
const delay2 = this.reconnectDelayMs;
|
|
1199
|
+
this.connection.markBackoff(delay2);
|
|
1104
1200
|
this.reconnectTimer = self.setTimeout(() => {
|
|
1105
1201
|
this.reconnectTimer = null;
|
|
1202
|
+
this.connection.markConnecting();
|
|
1106
1203
|
void this.connect().catch((error) => {
|
|
1107
|
-
|
|
1204
|
+
this.recordConnectionFailure("reconnect", error);
|
|
1108
1205
|
});
|
|
1109
1206
|
}, delay2);
|
|
1110
1207
|
this.reconnectDelayMs = Math.min(
|
|
@@ -1113,12 +1210,30 @@ var DriveSocket = class {
|
|
|
1113
1210
|
);
|
|
1114
1211
|
}
|
|
1115
1212
|
async connect() {
|
|
1116
|
-
const url = await
|
|
1213
|
+
const { endpoint, url } = await getWsEndpoint();
|
|
1214
|
+
this.connection.setEndpoint(endpoint);
|
|
1215
|
+
const health = await this.checkCoreHealth(endpoint);
|
|
1216
|
+
if (!health.ok) {
|
|
1217
|
+
this.connection.markDisconnected();
|
|
1218
|
+
this.recordConnectionFailure(
|
|
1219
|
+
"core unavailable",
|
|
1220
|
+
new Error(health.detail)
|
|
1221
|
+
);
|
|
1222
|
+
this.scheduleReconnect();
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1117
1225
|
try {
|
|
1118
1226
|
const socket2 = new WebSocket(url);
|
|
1119
1227
|
this.socket = socket2;
|
|
1120
1228
|
socket2.addEventListener("open", () => {
|
|
1121
1229
|
this.reconnectDelayMs = 1e3;
|
|
1230
|
+
this.connection.markConnected();
|
|
1231
|
+
const suppressed = this.connection.flushSuppressedFailureLogs();
|
|
1232
|
+
if (suppressed > 0) {
|
|
1233
|
+
console.info(
|
|
1234
|
+
`DriveSocket reconnected after suppressing ${suppressed} repeated connection failures.`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1122
1237
|
this.startKeepAlive();
|
|
1123
1238
|
void this.sendHello().catch((error) => {
|
|
1124
1239
|
console.error("DriveSocket hello failed:", error);
|
|
@@ -1128,22 +1243,82 @@ var DriveSocket = class {
|
|
|
1128
1243
|
this.handleMessage(event.data);
|
|
1129
1244
|
});
|
|
1130
1245
|
socket2.addEventListener("close", () => {
|
|
1131
|
-
this.socket
|
|
1132
|
-
this.stopKeepAlive();
|
|
1133
|
-
this.scheduleReconnect();
|
|
1246
|
+
this.handleSocketUnavailable(socket2, "socket closed");
|
|
1134
1247
|
});
|
|
1135
1248
|
socket2.addEventListener("error", () => {
|
|
1136
|
-
this.socket
|
|
1137
|
-
this.stopKeepAlive();
|
|
1138
|
-
this.scheduleReconnect();
|
|
1249
|
+
this.handleSocketUnavailable(socket2, "socket error");
|
|
1139
1250
|
});
|
|
1140
1251
|
} catch (error) {
|
|
1141
|
-
|
|
1252
|
+
this.recordConnectionFailure("connect", error);
|
|
1253
|
+
this.connection.markDisconnected();
|
|
1142
1254
|
this.scheduleReconnect();
|
|
1143
1255
|
}
|
|
1144
1256
|
}
|
|
1257
|
+
async checkCoreHealth(endpoint) {
|
|
1258
|
+
const controller = new AbortController();
|
|
1259
|
+
const timeoutId = self.setTimeout(() => {
|
|
1260
|
+
controller.abort();
|
|
1261
|
+
}, CORE_HEALTH_TIMEOUT_MS);
|
|
1262
|
+
try {
|
|
1263
|
+
const response = await fetch(getHealthEndpoint(endpoint), {
|
|
1264
|
+
method: "GET",
|
|
1265
|
+
cache: "no-store",
|
|
1266
|
+
signal: controller.signal
|
|
1267
|
+
});
|
|
1268
|
+
if (!response.ok) {
|
|
1269
|
+
return {
|
|
1270
|
+
ok: false,
|
|
1271
|
+
detail: `health returned HTTP ${response.status}`
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
return { ok: true, detail: "ok" };
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
1277
|
+
return {
|
|
1278
|
+
ok: false,
|
|
1279
|
+
detail: `health timed out after ${CORE_HEALTH_TIMEOUT_MS}ms`
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
ok: false,
|
|
1284
|
+
detail: error instanceof Error && error.message.length > 0 ? error.message : "health check failed"
|
|
1285
|
+
};
|
|
1286
|
+
} finally {
|
|
1287
|
+
clearTimeout(timeoutId);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
getConnectionStatus() {
|
|
1291
|
+
return this.connection.getStatus();
|
|
1292
|
+
}
|
|
1293
|
+
handleSocketUnavailable(socket2, reason) {
|
|
1294
|
+
if (this.socket !== socket2) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
this.socket = null;
|
|
1298
|
+
this.stopKeepAlive();
|
|
1299
|
+
this.connection.markDisconnected();
|
|
1300
|
+
this.recordConnectionFailure(reason, new Error(reason));
|
|
1301
|
+
this.scheduleReconnect();
|
|
1302
|
+
}
|
|
1303
|
+
recordConnectionFailure(context, error) {
|
|
1304
|
+
const detail = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
1305
|
+
this.connection.recordFailure(`${context}: ${detail}`);
|
|
1306
|
+
const budget = this.connection.consumeFailureLogBudget();
|
|
1307
|
+
if (!budget.shouldLog) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
if (budget.suppressedCount > 0) {
|
|
1311
|
+
console.warn(
|
|
1312
|
+
`DriveSocket ${context} failed (${budget.suppressedCount} repeated failures suppressed).`,
|
|
1313
|
+
error
|
|
1314
|
+
);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
console.warn(`DriveSocket ${context} failed.`, error);
|
|
1318
|
+
}
|
|
1145
1319
|
async sendHello() {
|
|
1146
1320
|
const manifest = chrome.runtime.getManifest();
|
|
1321
|
+
const endpoint = await readCoreEndpointConfig();
|
|
1147
1322
|
let tabs = [];
|
|
1148
1323
|
try {
|
|
1149
1324
|
tabs = await queryTabs();
|
|
@@ -1153,6 +1328,9 @@ var DriveSocket = class {
|
|
|
1153
1328
|
}
|
|
1154
1329
|
const params = {
|
|
1155
1330
|
version: manifest.version,
|
|
1331
|
+
core_host: endpoint.host,
|
|
1332
|
+
core_port: endpoint.port,
|
|
1333
|
+
core_port_source: endpoint.portSource,
|
|
1156
1334
|
tabs
|
|
1157
1335
|
};
|
|
1158
1336
|
this.sendEvent("drive.hello", params);
|
|
@@ -3188,5 +3366,17 @@ chrome.debugger.onDetach.addListener(
|
|
|
3188
3366
|
});
|
|
3189
3367
|
}
|
|
3190
3368
|
);
|
|
3369
|
+
chrome.runtime.onMessage.addListener(
|
|
3370
|
+
(message, _sender, sendResponse) => {
|
|
3371
|
+
if (!message || typeof message !== "object" || message.action !== "drive.connection_status") {
|
|
3372
|
+
return void 0;
|
|
3373
|
+
}
|
|
3374
|
+
sendResponse({
|
|
3375
|
+
ok: true,
|
|
3376
|
+
result: socket.getConnectionStatus()
|
|
3377
|
+
});
|
|
3378
|
+
return true;
|
|
3379
|
+
}
|
|
3380
|
+
);
|
|
3191
3381
|
socket.start();
|
|
3192
3382
|
//# sourceMappingURL=background.js.map
|