@btraut/browser-bridge 0.2.0 → 0.4.0
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 +51 -0
- package/README.md +37 -7
- package/dist/api.js +1652 -1528
- package/dist/api.js.map +4 -4
- package/dist/index.js +636 -22
- package/dist/index.js.map +4 -4
- package/extension/dist/background.js +96 -2
- package/extension/dist/background.js.map +3 -3
- package/package.json +4 -2
- package/skills/browser-bridge/SKILL.md +12 -0
- package/skills/browser-bridge/skill.json +4 -0
package/dist/api.js
CHANGED
|
@@ -138,7 +138,8 @@ var SessionRegistry = class {
|
|
|
138
138
|
id,
|
|
139
139
|
state: "INIT" /* INIT */,
|
|
140
140
|
createdAt: now,
|
|
141
|
-
updatedAt: now
|
|
141
|
+
updatedAt: now,
|
|
142
|
+
lastAccessedAt: now
|
|
142
143
|
};
|
|
143
144
|
this.sessions.set(id, session);
|
|
144
145
|
return session;
|
|
@@ -157,8 +158,23 @@ var SessionRegistry = class {
|
|
|
157
158
|
`Session ${sessionId} does not exist.`
|
|
158
159
|
);
|
|
159
160
|
}
|
|
161
|
+
session.lastAccessedAt = /* @__PURE__ */ new Date();
|
|
160
162
|
return session;
|
|
161
163
|
}
|
|
164
|
+
cleanupIdleSessions(ttlMs, now = /* @__PURE__ */ new Date()) {
|
|
165
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
let removed = 0;
|
|
169
|
+
for (const [id, session] of this.sessions.entries()) {
|
|
170
|
+
const idleMs = now.getTime() - session.lastAccessedAt.getTime();
|
|
171
|
+
if (idleMs > ttlMs) {
|
|
172
|
+
this.sessions.delete(id);
|
|
173
|
+
removed += 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return removed;
|
|
177
|
+
}
|
|
162
178
|
apply(sessionId, event) {
|
|
163
179
|
const session = this.require(sessionId);
|
|
164
180
|
if (session.state === "CLOSED" /* CLOSED */ && event !== "CLOSE") {
|
|
@@ -467,7 +483,18 @@ var createSessionRouter = (registry, options = {}) => {
|
|
|
467
483
|
return router;
|
|
468
484
|
};
|
|
469
485
|
|
|
470
|
-
// packages/core/src/inspect.ts
|
|
486
|
+
// packages/core/src/inspect/errors.ts
|
|
487
|
+
var InspectError = class extends Error {
|
|
488
|
+
constructor(code, message, options = {}) {
|
|
489
|
+
super(message);
|
|
490
|
+
this.name = "InspectError";
|
|
491
|
+
this.code = code;
|
|
492
|
+
this.retryable = options.retryable ?? false;
|
|
493
|
+
this.details = options.details;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// packages/core/src/inspect/service.ts
|
|
471
498
|
var import_crypto3 = require("crypto");
|
|
472
499
|
var import_promises2 = require("node:fs/promises");
|
|
473
500
|
var import_node_path2 = __toESM(require("node:path"));
|
|
@@ -506,6 +533,7 @@ var ExtensionBridge = class {
|
|
|
506
533
|
this.pending = /* @__PURE__ */ new Map();
|
|
507
534
|
this.connected = false;
|
|
508
535
|
this.tabs = [];
|
|
536
|
+
this.badMessageLogsRemaining = 3;
|
|
509
537
|
this.heartbeatInterval = null;
|
|
510
538
|
this.awaitingHeartbeat = false;
|
|
511
539
|
this.debuggerListeners = /* @__PURE__ */ new Set();
|
|
@@ -636,7 +664,11 @@ var ExtensionBridge = class {
|
|
|
636
664
|
if (this.socket && this.socket.readyState === import_ws.WebSocket.OPEN) {
|
|
637
665
|
try {
|
|
638
666
|
this.socket.terminate();
|
|
639
|
-
} catch {
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.debug(
|
|
669
|
+
"Extension socket terminate failed; falling back to close().",
|
|
670
|
+
error
|
|
671
|
+
);
|
|
640
672
|
this.socket.close();
|
|
641
673
|
}
|
|
642
674
|
}
|
|
@@ -688,7 +720,11 @@ var ExtensionBridge = class {
|
|
|
688
720
|
let message = null;
|
|
689
721
|
try {
|
|
690
722
|
message = JSON.parse(text);
|
|
691
|
-
} catch {
|
|
723
|
+
} catch (error) {
|
|
724
|
+
if (this.badMessageLogsRemaining > 0) {
|
|
725
|
+
this.badMessageLogsRemaining -= 1;
|
|
726
|
+
console.debug("Failed to parse extension message.", error);
|
|
727
|
+
}
|
|
692
728
|
return;
|
|
693
729
|
}
|
|
694
730
|
if (!message || typeof message !== "object") {
|
|
@@ -920,227 +956,7 @@ var DriveController = class {
|
|
|
920
956
|
}
|
|
921
957
|
};
|
|
922
958
|
|
|
923
|
-
// packages/core/src/
|
|
924
|
-
var PAGE_STATE_SCRIPT = [
|
|
925
|
-
"(() => {",
|
|
926
|
-
" const escape = (value) => {",
|
|
927
|
-
" if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {",
|
|
928
|
-
" return CSS.escape(value);",
|
|
929
|
-
" }",
|
|
930
|
-
` return String(value).replace(/["'\\\\]/g, '\\\\$&');`,
|
|
931
|
-
" };",
|
|
932
|
-
" const truncate = (value, max) => {",
|
|
933
|
-
" const text = String(value ?? '');",
|
|
934
|
-
" return text.length > max ? text.slice(0, max) : text;",
|
|
935
|
-
" };",
|
|
936
|
-
" const selectorFor = (element) => {",
|
|
937
|
-
" if (element.id) {",
|
|
938
|
-
" return `#${escape(element.id)}`;",
|
|
939
|
-
" }",
|
|
940
|
-
" const name = element.getAttribute('name');",
|
|
941
|
-
" if (name) {",
|
|
942
|
-
' return `${element.tagName.toLowerCase()}[name="${escape(name)}"]`;',
|
|
943
|
-
" }",
|
|
944
|
-
" const parts = [];",
|
|
945
|
-
" let node = element;",
|
|
946
|
-
" while (node && node.nodeType === 1 && parts.length < 4) {",
|
|
947
|
-
" let part = node.tagName.toLowerCase();",
|
|
948
|
-
" const parent = node.parentElement;",
|
|
949
|
-
" if (parent) {",
|
|
950
|
-
" const siblings = Array.from(parent.children).filter(",
|
|
951
|
-
" (child) => child.tagName === node.tagName",
|
|
952
|
-
" );",
|
|
953
|
-
" if (siblings.length > 1) {",
|
|
954
|
-
" part += `:nth-of-type(${siblings.indexOf(node) + 1})`;",
|
|
955
|
-
" }",
|
|
956
|
-
" }",
|
|
957
|
-
" parts.unshift(part);",
|
|
958
|
-
" node = parent;",
|
|
959
|
-
" }",
|
|
960
|
-
" return parts.join('>');",
|
|
961
|
-
" };",
|
|
962
|
-
" const readStorage = (storage, limit) => {",
|
|
963
|
-
" try {",
|
|
964
|
-
" return Object.keys(storage)",
|
|
965
|
-
" .slice(0, limit)",
|
|
966
|
-
" .map((key) => ({",
|
|
967
|
-
" key,",
|
|
968
|
-
" value: truncate(storage.getItem(key), 500),",
|
|
969
|
-
" }));",
|
|
970
|
-
" } catch {",
|
|
971
|
-
" return [];",
|
|
972
|
-
" }",
|
|
973
|
-
" };",
|
|
974
|
-
" const forms = Array.from(document.querySelectorAll('form')).map((form) => {",
|
|
975
|
-
" const fields = Array.from(form.elements)",
|
|
976
|
-
" .filter((element) => element && element.tagName)",
|
|
977
|
-
" .map((element) => {",
|
|
978
|
-
" const tag = element.tagName.toLowerCase();",
|
|
979
|
-
" const type = 'type' in element && element.type ? element.type : tag;",
|
|
980
|
-
" const name = element.name || element.getAttribute('name') || element.id || '';",
|
|
981
|
-
" let value = '';",
|
|
982
|
-
" let options;",
|
|
983
|
-
" if (tag === 'select') {",
|
|
984
|
-
" const select = element;",
|
|
985
|
-
" value = select.value ?? '';",
|
|
986
|
-
" options = Array.from(select.options).map((option) => option.text);",
|
|
987
|
-
" } else if (tag === 'input' && element.type === 'password') {",
|
|
988
|
-
" value = '[redacted]';",
|
|
989
|
-
" } else if (tag === 'input' || tag === 'textarea') {",
|
|
990
|
-
" value = element.value ?? '';",
|
|
991
|
-
" } else if (element.isContentEditable) {",
|
|
992
|
-
" value = element.textContent ?? '';",
|
|
993
|
-
" } else if ('value' in element) {",
|
|
994
|
-
" value = element.value ?? '';",
|
|
995
|
-
" }",
|
|
996
|
-
" return {",
|
|
997
|
-
" name,",
|
|
998
|
-
" type,",
|
|
999
|
-
" value: truncate(value, 500),",
|
|
1000
|
-
" ...(options ? { options } : {}),",
|
|
1001
|
-
" };",
|
|
1002
|
-
" });",
|
|
1003
|
-
" return {",
|
|
1004
|
-
" selector: selectorFor(form),",
|
|
1005
|
-
" action: form.getAttribute('action') || undefined,",
|
|
1006
|
-
" method: form.getAttribute('method') || undefined,",
|
|
1007
|
-
" fields,",
|
|
1008
|
-
" };",
|
|
1009
|
-
" });",
|
|
1010
|
-
" const localStorage = readStorage(window.localStorage, 100);",
|
|
1011
|
-
" const sessionStorage = readStorage(window.sessionStorage, 100);",
|
|
1012
|
-
" const cookies = (document.cookie ? document.cookie.split(';') : [])",
|
|
1013
|
-
" .map((entry) => entry.trim())",
|
|
1014
|
-
" .filter((entry) => entry.length > 0)",
|
|
1015
|
-
" .slice(0, 50)",
|
|
1016
|
-
" .map((entry) => {",
|
|
1017
|
-
" const [key, ...rest] = entry.split('=');",
|
|
1018
|
-
" return { key, value: truncate(rest.join('='), 500) };",
|
|
1019
|
-
" });",
|
|
1020
|
-
" return { forms, localStorage, sessionStorage, cookies };",
|
|
1021
|
-
"})()"
|
|
1022
|
-
].join("\n");
|
|
1023
|
-
|
|
1024
|
-
// packages/core/src/target-matching.ts
|
|
1025
|
-
var normalizeText = (value) => (value ?? "").trim().toLowerCase();
|
|
1026
|
-
var normalizeUrl = (value) => {
|
|
1027
|
-
const normalized = normalizeText(value);
|
|
1028
|
-
if (!normalized) {
|
|
1029
|
-
return "";
|
|
1030
|
-
}
|
|
1031
|
-
const hashIndex = normalized.indexOf("#");
|
|
1032
|
-
const withoutHash = hashIndex >= 0 ? normalized.slice(0, hashIndex) : normalized;
|
|
1033
|
-
return withoutHash.endsWith("/") && withoutHash.length > 1 ? withoutHash.slice(0, -1) : withoutHash;
|
|
1034
|
-
};
|
|
1035
|
-
var scoreUrlMatch = (candidateUrl, hintUrl) => {
|
|
1036
|
-
if (!candidateUrl || !hintUrl) {
|
|
1037
|
-
return 0;
|
|
1038
|
-
}
|
|
1039
|
-
if (candidateUrl === hintUrl) {
|
|
1040
|
-
return 100;
|
|
1041
|
-
}
|
|
1042
|
-
if (candidateUrl.includes(hintUrl) || hintUrl.includes(candidateUrl)) {
|
|
1043
|
-
return 60;
|
|
1044
|
-
}
|
|
1045
|
-
return 0;
|
|
1046
|
-
};
|
|
1047
|
-
var scoreTitleMatch = (candidateTitle, hintTitle) => {
|
|
1048
|
-
if (!candidateTitle || !hintTitle) {
|
|
1049
|
-
return 0;
|
|
1050
|
-
}
|
|
1051
|
-
if (candidateTitle === hintTitle) {
|
|
1052
|
-
return 80;
|
|
1053
|
-
}
|
|
1054
|
-
if (candidateTitle.includes(hintTitle) || hintTitle.includes(candidateTitle)) {
|
|
1055
|
-
return 40;
|
|
1056
|
-
}
|
|
1057
|
-
return 0;
|
|
1058
|
-
};
|
|
1059
|
-
var scoreRecency = (lastSeenAt, now) => {
|
|
1060
|
-
if (!lastSeenAt) {
|
|
1061
|
-
return 0;
|
|
1062
|
-
}
|
|
1063
|
-
const ageMinutes = Math.max(0, (now - lastSeenAt) / 6e4);
|
|
1064
|
-
return Math.max(0, 30 - ageMinutes);
|
|
1065
|
-
};
|
|
1066
|
-
var scoreHintRecency = (lastSeenAt, hintLastActiveAt) => {
|
|
1067
|
-
if (!lastSeenAt || !hintLastActiveAt) {
|
|
1068
|
-
return 0;
|
|
1069
|
-
}
|
|
1070
|
-
const deltaMinutes = Math.abs(lastSeenAt - hintLastActiveAt) / 6e4;
|
|
1071
|
-
return Math.max(0, 20 - deltaMinutes);
|
|
1072
|
-
};
|
|
1073
|
-
var isBlankUrl = (url) => url.length === 0 || url === "about:blank";
|
|
1074
|
-
var rankTargetCandidates = (candidates, hint, now = Date.now()) => {
|
|
1075
|
-
const normalizedHintUrl = normalizeUrl(hint?.url);
|
|
1076
|
-
const normalizedHintTitle = normalizeText(hint?.title);
|
|
1077
|
-
const hintLastActiveAt = hint?.lastActiveAt ? Date.parse(hint.lastActiveAt) : void 0;
|
|
1078
|
-
const ranked = candidates.map((candidate) => {
|
|
1079
|
-
const reasons = [];
|
|
1080
|
-
const normalizedUrl = normalizeUrl(candidate.url);
|
|
1081
|
-
const normalizedTitle = normalizeText(candidate.title);
|
|
1082
|
-
let score = 0;
|
|
1083
|
-
const urlScore = scoreUrlMatch(normalizedUrl, normalizedHintUrl);
|
|
1084
|
-
if (urlScore > 0) {
|
|
1085
|
-
score += urlScore;
|
|
1086
|
-
reasons.push(urlScore >= 100 ? "url:exact" : "url:partial");
|
|
1087
|
-
}
|
|
1088
|
-
const titleScore = scoreTitleMatch(normalizedTitle, normalizedHintTitle);
|
|
1089
|
-
if (titleScore > 0) {
|
|
1090
|
-
score += titleScore;
|
|
1091
|
-
reasons.push(titleScore >= 80 ? "title:exact" : "title:partial");
|
|
1092
|
-
}
|
|
1093
|
-
const recencyScore = scoreRecency(candidate.lastSeenAt, now);
|
|
1094
|
-
if (recencyScore > 0) {
|
|
1095
|
-
score += recencyScore;
|
|
1096
|
-
reasons.push("recency");
|
|
1097
|
-
}
|
|
1098
|
-
const hintRecencyScore = scoreHintRecency(
|
|
1099
|
-
candidate.lastSeenAt,
|
|
1100
|
-
Number.isFinite(hintLastActiveAt) ? hintLastActiveAt : void 0
|
|
1101
|
-
);
|
|
1102
|
-
if (hintRecencyScore > 0) {
|
|
1103
|
-
score += hintRecencyScore;
|
|
1104
|
-
reasons.push("hint-recency");
|
|
1105
|
-
}
|
|
1106
|
-
if (isBlankUrl(normalizedUrl)) {
|
|
1107
|
-
score -= 50;
|
|
1108
|
-
reasons.push("blank-url");
|
|
1109
|
-
}
|
|
1110
|
-
return { candidate, score, reasons };
|
|
1111
|
-
});
|
|
1112
|
-
return ranked.sort((a, b) => {
|
|
1113
|
-
if (a.score !== b.score) {
|
|
1114
|
-
return b.score - a.score;
|
|
1115
|
-
}
|
|
1116
|
-
const aSeen = a.candidate.lastSeenAt ?? 0;
|
|
1117
|
-
const bSeen = b.candidate.lastSeenAt ?? 0;
|
|
1118
|
-
if (aSeen !== bSeen) {
|
|
1119
|
-
return bSeen - aSeen;
|
|
1120
|
-
}
|
|
1121
|
-
return a.candidate.url.localeCompare(b.candidate.url);
|
|
1122
|
-
});
|
|
1123
|
-
};
|
|
1124
|
-
var pickBestTarget = (candidates, hint, now = Date.now()) => {
|
|
1125
|
-
const ranked = rankTargetCandidates(candidates, hint, now);
|
|
1126
|
-
return ranked.length > 0 ? ranked[0] : null;
|
|
1127
|
-
};
|
|
1128
|
-
|
|
1129
|
-
// packages/core/src/inspect.ts
|
|
1130
|
-
var InspectError = class extends Error {
|
|
1131
|
-
constructor(code, message, options = {}) {
|
|
1132
|
-
super(message);
|
|
1133
|
-
this.name = "InspectError";
|
|
1134
|
-
this.code = code;
|
|
1135
|
-
this.retryable = options.retryable ?? false;
|
|
1136
|
-
this.details = options.details;
|
|
1137
|
-
}
|
|
1138
|
-
};
|
|
1139
|
-
var DEFAULT_MAX_SNAPSHOTS_PER_SESSION = 20;
|
|
1140
|
-
var DEFAULT_MAX_SNAPSHOT_HISTORY = 100;
|
|
1141
|
-
var SNAPSHOT_REF_ATTRIBUTE = "data-bv-ref";
|
|
1142
|
-
var MAX_REF_ASSIGNMENTS = 500;
|
|
1143
|
-
var MAX_REF_WARNINGS = 5;
|
|
959
|
+
// packages/core/src/inspect/ax-snapshot.ts
|
|
1144
960
|
var INTERACTIVE_AX_ROLES = /* @__PURE__ */ new Set([
|
|
1145
961
|
"button",
|
|
1146
962
|
"link",
|
|
@@ -1167,1126 +983,1686 @@ var LABEL_AX_ROLES = /* @__PURE__ */ new Set([
|
|
|
1167
983
|
"spinbutton",
|
|
1168
984
|
"slider"
|
|
1169
985
|
]);
|
|
1170
|
-
var
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
this.maxSnapshotHistory = options.maxSnapshotHistory ?? DEFAULT_MAX_SNAPSHOT_HISTORY;
|
|
986
|
+
var getAxNodes = (snapshot) => {
|
|
987
|
+
const nodes = Array.isArray(snapshot) ? snapshot : snapshot?.nodes;
|
|
988
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
989
|
+
};
|
|
990
|
+
var replaceAxNodes = (snapshot, nodes) => {
|
|
991
|
+
if (Array.isArray(snapshot)) {
|
|
992
|
+
return nodes;
|
|
1178
993
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
994
|
+
if (snapshot && typeof snapshot === "object") {
|
|
995
|
+
snapshot.nodes = nodes;
|
|
1181
996
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
),
|
|
1200
|
-
at: debuggerError.at
|
|
1201
|
-
};
|
|
997
|
+
return snapshot;
|
|
998
|
+
};
|
|
999
|
+
var getAxRole = (node) => {
|
|
1000
|
+
const role = typeof node.role === "string" ? node.role : node.role?.value ?? "";
|
|
1001
|
+
return typeof role === "string" ? role.toLowerCase() : "";
|
|
1002
|
+
};
|
|
1003
|
+
var getAxName = (node) => {
|
|
1004
|
+
const name = typeof node.name === "string" ? node.name : node.name?.value ?? "";
|
|
1005
|
+
return typeof name === "string" ? name : "";
|
|
1006
|
+
};
|
|
1007
|
+
var hasAxValue = (node) => {
|
|
1008
|
+
if (!Array.isArray(node.properties)) {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
for (const prop of node.properties) {
|
|
1012
|
+
if (!prop || typeof prop !== "object") {
|
|
1013
|
+
continue;
|
|
1202
1014
|
}
|
|
1203
|
-
|
|
1015
|
+
const value = prop.value?.value;
|
|
1016
|
+
if (value === void 0 || value === null) {
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (typeof value === "string" && value.trim().length === 0) {
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
return true;
|
|
1204
1023
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1024
|
+
return false;
|
|
1025
|
+
};
|
|
1026
|
+
var normalizeQuery = (value) => value.trim().toLowerCase();
|
|
1027
|
+
var matchesTextValue = (value, query) => {
|
|
1028
|
+
if (!query) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
return value.toLowerCase().includes(query);
|
|
1032
|
+
};
|
|
1033
|
+
var matchesAxText = (node, query) => {
|
|
1034
|
+
if (!query) {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
const candidates = [getAxName(node)];
|
|
1038
|
+
if (Array.isArray(node.properties)) {
|
|
1039
|
+
for (const prop of node.properties) {
|
|
1040
|
+
if (!prop || typeof prop !== "object") {
|
|
1041
|
+
continue;
|
|
1214
1042
|
}
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
} catch (error) {
|
|
1219
|
-
if (error instanceof InspectError) {
|
|
1220
|
-
this.recordError(error);
|
|
1043
|
+
const value = prop.value?.value;
|
|
1044
|
+
if (value === void 0 || value === null) {
|
|
1045
|
+
continue;
|
|
1221
1046
|
}
|
|
1047
|
+
if (typeof value === "string") {
|
|
1048
|
+
candidates.push(value);
|
|
1049
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
1050
|
+
candidates.push(String(value));
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return candidates.some((text) => matchesTextValue(text, query));
|
|
1055
|
+
};
|
|
1056
|
+
var isInteractiveAxNode = (node) => {
|
|
1057
|
+
const role = getAxRole(node);
|
|
1058
|
+
return Boolean(role && INTERACTIVE_AX_ROLES.has(role));
|
|
1059
|
+
};
|
|
1060
|
+
var filterAxSnapshot = (snapshot, predicate) => {
|
|
1061
|
+
const nodes = getAxNodes(snapshot);
|
|
1062
|
+
if (nodes.length === 0) {
|
|
1063
|
+
return snapshot;
|
|
1064
|
+
}
|
|
1065
|
+
const keepIds = /* @__PURE__ */ new Set();
|
|
1066
|
+
const filtered = nodes.filter((node) => {
|
|
1067
|
+
if (!node || typeof node !== "object") {
|
|
1222
1068
|
return false;
|
|
1223
1069
|
}
|
|
1070
|
+
const keep = predicate(node);
|
|
1071
|
+
if (keep && typeof node.nodeId === "string") {
|
|
1072
|
+
keepIds.add(node.nodeId);
|
|
1073
|
+
}
|
|
1074
|
+
return keep;
|
|
1075
|
+
});
|
|
1076
|
+
for (const node of filtered) {
|
|
1077
|
+
if (Array.isArray(node.childIds)) {
|
|
1078
|
+
node.childIds = node.childIds.filter((id) => keepIds.has(id));
|
|
1079
|
+
}
|
|
1224
1080
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
result2 = await this.debuggerCommand(
|
|
1284
|
-
selection.tabId,
|
|
1285
|
-
"Accessibility.getPartialAXTree",
|
|
1286
|
-
{ nodeId: resolved.nodeId }
|
|
1287
|
-
);
|
|
1288
|
-
} else {
|
|
1289
|
-
result2 = await this.debuggerCommand(
|
|
1290
|
-
selection.tabId,
|
|
1291
|
-
"Accessibility.getFullAXTree",
|
|
1292
|
-
{}
|
|
1293
|
-
);
|
|
1294
|
-
}
|
|
1295
|
-
let snapshot = input.interactive || input.compact ? this.applyAxSnapshotFilters(result2, {
|
|
1296
|
-
interactiveOnly: input.interactive,
|
|
1297
|
-
compact: input.compact
|
|
1298
|
-
}) : result2;
|
|
1299
|
-
let truncated = false;
|
|
1300
|
-
const truncationWarnings = [];
|
|
1301
|
-
if (input.maxNodes !== void 0) {
|
|
1302
|
-
const truncatedResult = this.truncateAxSnapshot(
|
|
1303
|
-
snapshot,
|
|
1304
|
-
input.maxNodes
|
|
1305
|
-
);
|
|
1306
|
-
snapshot = truncatedResult.snapshot;
|
|
1307
|
-
truncated = truncatedResult.truncated;
|
|
1308
|
-
if (truncated) {
|
|
1309
|
-
truncationWarnings.push(
|
|
1310
|
-
`AX snapshot truncated to ${input.maxNodes} nodes.`
|
|
1311
|
-
);
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
const refMap = this.assignRefsToAxSnapshot(snapshot);
|
|
1315
|
-
const refWarnings = await this.applySnapshotRefs(
|
|
1316
|
-
selection.tabId,
|
|
1317
|
-
refMap
|
|
1318
|
-
);
|
|
1319
|
-
const warnings = [
|
|
1320
|
-
...selection.warnings ?? [],
|
|
1321
|
-
...selectorWarnings,
|
|
1322
|
-
...truncationWarnings,
|
|
1323
|
-
...refWarnings ?? []
|
|
1324
|
-
];
|
|
1325
|
-
return {
|
|
1326
|
-
format: "ax",
|
|
1327
|
-
snapshot,
|
|
1328
|
-
...truncated ? { truncated: true } : {},
|
|
1329
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
1330
|
-
};
|
|
1331
|
-
} catch (error) {
|
|
1332
|
-
if (error instanceof InspectError) {
|
|
1333
|
-
const fallbackCodes = [
|
|
1334
|
-
"NOT_SUPPORTED",
|
|
1335
|
-
"INSPECT_UNAVAILABLE",
|
|
1336
|
-
"EVALUATION_FAILED"
|
|
1337
|
-
];
|
|
1338
|
-
if (!fallbackCodes.includes(error.code)) {
|
|
1339
|
-
throw error;
|
|
1340
|
-
}
|
|
1341
|
-
const html = await this.captureHtml(selection.tabId, input.selector);
|
|
1342
|
-
const warnings = [
|
|
1343
|
-
...selection.warnings ?? [],
|
|
1344
|
-
"AX snapshot failed; returned HTML instead.",
|
|
1345
|
-
...input.maxNodes !== void 0 ? ["max_nodes is only supported for AX snapshots."] : [],
|
|
1346
|
-
...input.interactive ? ["Interactive filter is only supported for AX snapshots."] : [],
|
|
1347
|
-
...input.compact ? ["Compact filter is only supported for AX snapshots."] : [],
|
|
1348
|
-
...input.selector && html === "" ? [`Selector not found: ${input.selector}`] : []
|
|
1349
|
-
];
|
|
1350
|
-
return {
|
|
1351
|
-
format: "html",
|
|
1352
|
-
snapshot: html,
|
|
1353
|
-
warnings
|
|
1354
|
-
};
|
|
1355
|
-
}
|
|
1356
|
-
throw error;
|
|
1357
|
-
}
|
|
1358
|
-
};
|
|
1359
|
-
if (input.consistency === "quiesce") {
|
|
1360
|
-
const result2 = await driveMutex.runExclusive(work);
|
|
1361
|
-
this.recordSnapshot(input.sessionId, result2);
|
|
1362
|
-
this.markInspectConnected(input.sessionId);
|
|
1363
|
-
return result2;
|
|
1081
|
+
return replaceAxNodes(snapshot, filtered);
|
|
1082
|
+
};
|
|
1083
|
+
var collectKeptDescendants = (nodeId, nodeById, keepIds, visited = /* @__PURE__ */ new Set()) => {
|
|
1084
|
+
if (visited.has(nodeId)) {
|
|
1085
|
+
return [];
|
|
1086
|
+
}
|
|
1087
|
+
visited.add(nodeId);
|
|
1088
|
+
if (keepIds.has(nodeId)) {
|
|
1089
|
+
return [nodeId];
|
|
1090
|
+
}
|
|
1091
|
+
const node = nodeById.get(nodeId);
|
|
1092
|
+
if (!node || !Array.isArray(node.childIds)) {
|
|
1093
|
+
return [];
|
|
1094
|
+
}
|
|
1095
|
+
const output = [];
|
|
1096
|
+
for (const childId of node.childIds) {
|
|
1097
|
+
output.push(...collectKeptDescendants(childId, nodeById, keepIds, visited));
|
|
1098
|
+
}
|
|
1099
|
+
return output;
|
|
1100
|
+
};
|
|
1101
|
+
var shouldKeepCompactNode = (node) => {
|
|
1102
|
+
if (node.ignored) {
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
const role = getAxRole(node);
|
|
1106
|
+
if (role && INTERACTIVE_AX_ROLES.has(role)) {
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
const name = getAxName(node);
|
|
1110
|
+
const hasName = name.trim().length > 0;
|
|
1111
|
+
const hasValue = hasAxValue(node);
|
|
1112
|
+
if (hasName || hasValue) {
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
const hasChildren = Array.isArray(node.childIds) && node.childIds.length > 0;
|
|
1116
|
+
if (!role || DECORATIVE_AX_ROLES.has(role)) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
return hasChildren;
|
|
1120
|
+
};
|
|
1121
|
+
var compactAxSnapshot = (snapshot) => {
|
|
1122
|
+
const nodes = getAxNodes(snapshot);
|
|
1123
|
+
if (nodes.length === 0) {
|
|
1124
|
+
return snapshot;
|
|
1125
|
+
}
|
|
1126
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
1127
|
+
nodes.forEach((node) => {
|
|
1128
|
+
if (node && typeof node.nodeId === "string") {
|
|
1129
|
+
nodeById.set(node.nodeId, node);
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
const keepIds = /* @__PURE__ */ new Set();
|
|
1133
|
+
for (const node of nodes) {
|
|
1134
|
+
if (!node || typeof node !== "object" || typeof node.nodeId !== "string") {
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (shouldKeepCompactNode(node)) {
|
|
1138
|
+
keepIds.add(node.nodeId);
|
|
1364
1139
|
}
|
|
1365
|
-
const result = await work();
|
|
1366
|
-
this.recordSnapshot(input.sessionId, result);
|
|
1367
|
-
this.markInspectConnected(input.sessionId);
|
|
1368
|
-
return result;
|
|
1369
1140
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
if (snapshots.length < 2) {
|
|
1377
|
-
return {
|
|
1378
|
-
added: [],
|
|
1379
|
-
removed: [],
|
|
1380
|
-
changed: [],
|
|
1381
|
-
summary: "Not enough snapshots to diff."
|
|
1382
|
-
};
|
|
1141
|
+
const filtered = nodes.filter(
|
|
1142
|
+
(node) => node && typeof node.nodeId === "string" && keepIds.has(node.nodeId)
|
|
1143
|
+
);
|
|
1144
|
+
for (const node of filtered) {
|
|
1145
|
+
if (!Array.isArray(node.childIds) || typeof node.nodeId !== "string") {
|
|
1146
|
+
continue;
|
|
1383
1147
|
}
|
|
1384
|
-
const
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
const removed = [];
|
|
1388
|
-
const changed = [];
|
|
1389
|
-
for (const [key, value] of current.entries.entries()) {
|
|
1390
|
-
if (!previous.entries.has(key)) {
|
|
1391
|
-
added.push(key);
|
|
1392
|
-
} else if (previous.entries.get(key) !== value) {
|
|
1393
|
-
changed.push(key);
|
|
1394
|
-
}
|
|
1148
|
+
const nextChildIds = [];
|
|
1149
|
+
for (const childId of node.childIds) {
|
|
1150
|
+
nextChildIds.push(...collectKeptDescendants(childId, nodeById, keepIds));
|
|
1395
1151
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1152
|
+
node.childIds = Array.from(new Set(nextChildIds));
|
|
1153
|
+
}
|
|
1154
|
+
return replaceAxNodes(snapshot, filtered);
|
|
1155
|
+
};
|
|
1156
|
+
var applyAxSnapshotFilters = (snapshot, options) => {
|
|
1157
|
+
let filtered = snapshot;
|
|
1158
|
+
if (options.compact) {
|
|
1159
|
+
filtered = compactAxSnapshot(filtered);
|
|
1160
|
+
}
|
|
1161
|
+
if (options.interactiveOnly) {
|
|
1162
|
+
filtered = filterAxSnapshot(filtered, (node) => isInteractiveAxNode(node));
|
|
1163
|
+
}
|
|
1164
|
+
return filtered;
|
|
1165
|
+
};
|
|
1166
|
+
var truncateAxSnapshot = (snapshot, maxNodes) => {
|
|
1167
|
+
const nodes = getAxNodes(snapshot);
|
|
1168
|
+
if (!Number.isFinite(maxNodes) || maxNodes <= 0) {
|
|
1169
|
+
return { snapshot, truncated: false };
|
|
1170
|
+
}
|
|
1171
|
+
if (nodes.length === 0 || nodes.length <= maxNodes) {
|
|
1172
|
+
return { snapshot, truncated: false };
|
|
1173
|
+
}
|
|
1174
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
1175
|
+
const parentCount = /* @__PURE__ */ new Map();
|
|
1176
|
+
for (const node of nodes) {
|
|
1177
|
+
if (!node || typeof node !== "object" || typeof node.nodeId !== "string") {
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
nodeById.set(node.nodeId, node);
|
|
1181
|
+
parentCount.set(node.nodeId, parentCount.get(node.nodeId) ?? 0);
|
|
1182
|
+
}
|
|
1183
|
+
if (nodeById.size === 0) {
|
|
1184
|
+
const sliced = nodes.slice(0, maxNodes);
|
|
1185
|
+
for (const node of sliced) {
|
|
1186
|
+
if (node && typeof node === "object" && Array.isArray(node.childIds)) {
|
|
1187
|
+
node.childIds = [];
|
|
1399
1188
|
}
|
|
1400
1189
|
}
|
|
1401
1190
|
return {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
changed,
|
|
1405
|
-
summary: `Added ${added.length}, removed ${removed.length}, changed ${changed.length}.`
|
|
1191
|
+
snapshot: replaceAxNodes(snapshot, sliced),
|
|
1192
|
+
truncated: true
|
|
1406
1193
|
};
|
|
1407
1194
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
format: "ax",
|
|
1412
|
-
consistency: "best_effort",
|
|
1413
|
-
targetHint: input.targetHint
|
|
1414
|
-
});
|
|
1415
|
-
const warnings = [...snapshot.warnings ?? []];
|
|
1416
|
-
if (snapshot.format !== "ax") {
|
|
1417
|
-
warnings.push("AX snapshot unavailable; cannot resolve refs.");
|
|
1418
|
-
return {
|
|
1419
|
-
matches: [],
|
|
1420
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
1421
|
-
};
|
|
1195
|
+
for (const node of nodes) {
|
|
1196
|
+
if (!node || typeof node !== "object" || !Array.isArray(node.childIds)) {
|
|
1197
|
+
continue;
|
|
1422
1198
|
}
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1425
|
-
const nameQuery = typeof input.name === "string" ? this.normalizeQuery(input.name) : "";
|
|
1426
|
-
const textQuery = typeof input.text === "string" ? this.normalizeQuery(input.text) : "";
|
|
1427
|
-
const labelQuery = typeof input.label === "string" ? this.normalizeQuery(input.label) : "";
|
|
1428
|
-
const roleQuery = typeof input.role === "string" ? this.normalizeQuery(input.role) : "";
|
|
1429
|
-
for (const node of nodes) {
|
|
1430
|
-
if (!node || typeof node !== "object") {
|
|
1431
|
-
continue;
|
|
1432
|
-
}
|
|
1433
|
-
if (typeof node.ref !== "string" || node.ref.length === 0) {
|
|
1199
|
+
for (const childId of node.childIds) {
|
|
1200
|
+
if (typeof childId !== "string") {
|
|
1434
1201
|
continue;
|
|
1435
1202
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1203
|
+
parentCount.set(childId, (parentCount.get(childId) ?? 0) + 1);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
let roots = Array.from(nodeById.keys()).filter(
|
|
1207
|
+
(id) => (parentCount.get(id) ?? 0) === 0
|
|
1208
|
+
);
|
|
1209
|
+
if (roots.length === 0) {
|
|
1210
|
+
const first = nodes.find(
|
|
1211
|
+
(node) => node && typeof node.nodeId === "string"
|
|
1212
|
+
)?.nodeId;
|
|
1213
|
+
if (first) {
|
|
1214
|
+
roots = [first];
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
const kept = /* @__PURE__ */ new Set();
|
|
1218
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1219
|
+
const queue = [...roots];
|
|
1220
|
+
while (queue.length > 0 && kept.size < maxNodes) {
|
|
1221
|
+
const id = queue.shift();
|
|
1222
|
+
if (!id || visited.has(id)) {
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
visited.add(id);
|
|
1226
|
+
const node = nodeById.get(id);
|
|
1227
|
+
if (!node) {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
kept.add(id);
|
|
1231
|
+
if (Array.isArray(node.childIds)) {
|
|
1232
|
+
for (const childId of node.childIds) {
|
|
1233
|
+
if (typeof childId === "string" && !visited.has(childId)) {
|
|
1234
|
+
queue.push(childId);
|
|
1448
1235
|
}
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (kept.size === 0) {
|
|
1240
|
+
const fallback = [];
|
|
1241
|
+
for (const node of nodes) {
|
|
1242
|
+
if (fallback.length >= maxNodes) {
|
|
1243
|
+
break;
|
|
1244
|
+
}
|
|
1245
|
+
if (node && typeof node.nodeId === "string") {
|
|
1246
|
+
fallback.push(node.nodeId);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
fallback.forEach((id) => kept.add(id));
|
|
1250
|
+
}
|
|
1251
|
+
const filtered = nodes.filter(
|
|
1252
|
+
(node) => node && typeof node.nodeId === "string" && kept.has(node.nodeId)
|
|
1253
|
+
);
|
|
1254
|
+
for (const node of filtered) {
|
|
1255
|
+
if (Array.isArray(node.childIds)) {
|
|
1256
|
+
node.childIds = node.childIds.filter(
|
|
1257
|
+
(id) => typeof id === "string" && kept.has(id)
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
snapshot: replaceAxNodes(snapshot, filtered),
|
|
1263
|
+
truncated: true
|
|
1264
|
+
};
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
// packages/core/src/inspect/console.ts
|
|
1268
|
+
var toSourceLocation = (input) => {
|
|
1269
|
+
const url = typeof input.url === "string" && input.url.length > 0 ? input.url : void 0;
|
|
1270
|
+
const line = typeof input.lineNumber === "number" && Number.isFinite(input.lineNumber) ? Math.max(1, Math.floor(input.lineNumber) + 1) : void 0;
|
|
1271
|
+
const column = typeof input.columnNumber === "number" && Number.isFinite(input.columnNumber) ? Math.max(1, Math.floor(input.columnNumber) + 1) : void 0;
|
|
1272
|
+
if (!url && !line && !column) {
|
|
1273
|
+
return void 0;
|
|
1274
|
+
}
|
|
1275
|
+
return {
|
|
1276
|
+
...url ? { url } : {},
|
|
1277
|
+
...line ? { line } : {},
|
|
1278
|
+
...column ? { column } : {}
|
|
1279
|
+
};
|
|
1280
|
+
};
|
|
1281
|
+
var toStackFrames = (stackTrace) => {
|
|
1282
|
+
const frames = [];
|
|
1283
|
+
const collect = (trace) => {
|
|
1284
|
+
if (!trace || typeof trace !== "object") {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const callFrames = trace.callFrames;
|
|
1288
|
+
if (Array.isArray(callFrames)) {
|
|
1289
|
+
for (const frame of callFrames) {
|
|
1290
|
+
if (!frame || typeof frame !== "object") {
|
|
1451
1291
|
continue;
|
|
1452
1292
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1293
|
+
const functionName = typeof frame.functionName === "string" ? String(frame.functionName) : void 0;
|
|
1294
|
+
const url = typeof frame.url === "string" ? String(frame.url) : void 0;
|
|
1295
|
+
const lineNumber = frame.lineNumber;
|
|
1296
|
+
const columnNumber = frame.columnNumber;
|
|
1297
|
+
const loc = toSourceLocation({ url, lineNumber, columnNumber });
|
|
1298
|
+
frames.push({
|
|
1299
|
+
...functionName ? { functionName } : {},
|
|
1300
|
+
...loc?.url ? { url: loc.url } : {},
|
|
1301
|
+
...loc?.line ? { line: loc.line } : {},
|
|
1302
|
+
...loc?.column ? { column: loc.column } : {}
|
|
1303
|
+
});
|
|
1304
|
+
if (frames.length >= 50) {
|
|
1305
|
+
return;
|
|
1455
1306
|
}
|
|
1456
1307
|
}
|
|
1457
|
-
matches.push({
|
|
1458
|
-
ref: node.ref,
|
|
1459
|
-
...role ? { role } : {},
|
|
1460
|
-
...name ? { name } : {}
|
|
1461
|
-
});
|
|
1462
1308
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
}
|
|
1309
|
+
const parent = trace.parent;
|
|
1310
|
+
if (frames.length < 50 && parent) {
|
|
1311
|
+
collect(parent);
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
collect(stackTrace);
|
|
1315
|
+
return frames.length > 0 ? frames : void 0;
|
|
1316
|
+
};
|
|
1317
|
+
var toRemoteObjectSummary = (obj) => {
|
|
1318
|
+
if (!obj || typeof obj !== "object") {
|
|
1319
|
+
return void 0;
|
|
1467
1320
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1321
|
+
const raw = obj;
|
|
1322
|
+
const type = typeof raw.type === "string" ? raw.type : void 0;
|
|
1323
|
+
const subtype = typeof raw.subtype === "string" ? raw.subtype : void 0;
|
|
1324
|
+
const description = typeof raw.description === "string" ? raw.description : void 0;
|
|
1325
|
+
const unserializableValue = typeof raw.unserializableValue === "string" ? raw.unserializableValue : void 0;
|
|
1326
|
+
const out = {};
|
|
1327
|
+
if (type) out.type = type;
|
|
1328
|
+
if (subtype) out.subtype = subtype;
|
|
1329
|
+
if (description) out.description = description;
|
|
1330
|
+
if (raw.value !== void 0) out.value = raw.value;
|
|
1331
|
+
if (unserializableValue) out.unserializableValue = unserializableValue;
|
|
1332
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1333
|
+
};
|
|
1334
|
+
var stringifyRemoteObject = (value) => {
|
|
1335
|
+
if (!value || typeof value !== "object") {
|
|
1336
|
+
return String(value ?? "");
|
|
1480
1337
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
const har = this.buildHar(events, selection.tab.title);
|
|
1338
|
+
const obj = value;
|
|
1339
|
+
if (obj.unserializableValue) {
|
|
1340
|
+
return obj.unserializableValue;
|
|
1341
|
+
}
|
|
1342
|
+
if (obj.value !== void 0) {
|
|
1487
1343
|
try {
|
|
1488
|
-
|
|
1489
|
-
const artifactId = (0, import_crypto3.randomUUID)();
|
|
1490
|
-
const filePath = import_node_path2.default.join(rootDir, `har-${artifactId}.json`);
|
|
1491
|
-
await (0, import_promises2.writeFile)(filePath, JSON.stringify(har, null, 2), "utf-8");
|
|
1492
|
-
const result = {
|
|
1493
|
-
artifact_id: artifactId,
|
|
1494
|
-
path: filePath,
|
|
1495
|
-
mime: "application/json"
|
|
1496
|
-
};
|
|
1497
|
-
this.markInspectConnected(input.sessionId);
|
|
1498
|
-
return result;
|
|
1344
|
+
return typeof obj.value === "string" ? obj.value : JSON.stringify(obj.value);
|
|
1499
1345
|
} catch {
|
|
1500
|
-
|
|
1501
|
-
"ARTIFACT_IO_ERROR",
|
|
1502
|
-
"Failed to write HAR file."
|
|
1503
|
-
);
|
|
1504
|
-
this.recordError(error);
|
|
1505
|
-
throw error;
|
|
1346
|
+
return String(obj.value);
|
|
1506
1347
|
}
|
|
1507
1348
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1349
|
+
if (obj.description) {
|
|
1350
|
+
return obj.description;
|
|
1351
|
+
}
|
|
1352
|
+
return obj.type ?? "";
|
|
1353
|
+
};
|
|
1354
|
+
var toConsoleEntry = (event) => {
|
|
1355
|
+
const params = event.params ?? {};
|
|
1356
|
+
switch (event.method) {
|
|
1357
|
+
case "Runtime.consoleAPICalled": {
|
|
1358
|
+
const rawArgs = Array.isArray(params.args) ? params.args : [];
|
|
1359
|
+
const text = rawArgs.map((arg) => stringifyRemoteObject(arg)).join(" ");
|
|
1360
|
+
const level = String(params.type ?? "log");
|
|
1361
|
+
const stack = toStackFrames(
|
|
1362
|
+
params.stackTrace
|
|
1363
|
+
);
|
|
1364
|
+
const args = rawArgs.map((arg) => toRemoteObjectSummary(arg)).filter((entry) => Boolean(entry));
|
|
1365
|
+
return {
|
|
1366
|
+
level,
|
|
1367
|
+
text,
|
|
1368
|
+
timestamp: event.timestamp,
|
|
1369
|
+
...stack && stack.length > 0 ? { stack } : {},
|
|
1370
|
+
...args.length > 0 ? { args } : {}
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
case "Runtime.exceptionThrown": {
|
|
1374
|
+
const details = params.exceptionDetails;
|
|
1375
|
+
const exception = toRemoteObjectSummary(details?.exception);
|
|
1376
|
+
const stack = toStackFrames(details?.stackTrace);
|
|
1377
|
+
const source = toSourceLocation({
|
|
1378
|
+
url: details?.url,
|
|
1379
|
+
lineNumber: details?.lineNumber,
|
|
1380
|
+
columnNumber: details?.columnNumber
|
|
1381
|
+
}) ?? // If the top frame exists, treat it as the source.
|
|
1382
|
+
(stack && stack.length > 0 ? {
|
|
1383
|
+
url: stack[0].url,
|
|
1384
|
+
line: stack[0].line,
|
|
1385
|
+
column: stack[0].column
|
|
1386
|
+
} : void 0);
|
|
1387
|
+
const baseText = typeof details?.text === "string" && details.text.trim().length > 0 ? details.text : "Uncaught exception";
|
|
1388
|
+
const exceptionDesc = typeof exception?.description === "string" && exception.description.trim().length > 0 ? exception.description : void 0;
|
|
1389
|
+
const text = baseText === "Uncaught" && exceptionDesc ? `Uncaught: ${exceptionDesc}` : baseText;
|
|
1390
|
+
return {
|
|
1391
|
+
level: "error",
|
|
1392
|
+
text,
|
|
1393
|
+
timestamp: event.timestamp,
|
|
1394
|
+
...source ? { source } : {},
|
|
1395
|
+
...stack && stack.length > 0 ? { stack } : {},
|
|
1396
|
+
...exception ? { exception } : {}
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
case "Log.entryAdded": {
|
|
1400
|
+
const entry = params.entry;
|
|
1401
|
+
if (!entry) {
|
|
1402
|
+
return null;
|
|
1520
1403
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1404
|
+
const stack = toStackFrames(entry.stackTrace);
|
|
1405
|
+
const source = toSourceLocation({
|
|
1406
|
+
url: entry.url,
|
|
1407
|
+
lineNumber: entry.lineNumber,
|
|
1408
|
+
columnNumber: void 0
|
|
1409
|
+
});
|
|
1410
|
+
return {
|
|
1411
|
+
level: entry.level ?? "log",
|
|
1412
|
+
text: entry.text ?? "",
|
|
1413
|
+
timestamp: event.timestamp,
|
|
1414
|
+
...source ? { source } : {},
|
|
1415
|
+
...stack && stack.length > 0 ? { stack } : {}
|
|
1526
1416
|
};
|
|
1527
|
-
this.markInspectConnected(input.sessionId);
|
|
1528
|
-
return output2;
|
|
1529
1417
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
warnings: selection.warnings
|
|
1533
|
-
};
|
|
1534
|
-
this.markInspectConnected(input.sessionId);
|
|
1535
|
-
return output;
|
|
1418
|
+
default:
|
|
1419
|
+
return null;
|
|
1536
1420
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
// packages/core/src/inspect/har.ts
|
|
1424
|
+
var buildHar = (events, title) => {
|
|
1425
|
+
const requests = /* @__PURE__ */ new Map();
|
|
1426
|
+
const toTimestamp = (event, fallback) => {
|
|
1427
|
+
const raw = event.params?.wallTime;
|
|
1428
|
+
if (typeof raw === "number") {
|
|
1429
|
+
return raw * 1e3;
|
|
1430
|
+
}
|
|
1431
|
+
const ts = event.params?.timestamp;
|
|
1432
|
+
if (typeof ts === "number") {
|
|
1433
|
+
return ts * 1e3;
|
|
1434
|
+
}
|
|
1435
|
+
const parsed = Date.parse(event.timestamp);
|
|
1436
|
+
if (Number.isFinite(parsed)) {
|
|
1437
|
+
return parsed;
|
|
1438
|
+
}
|
|
1439
|
+
return fallback ?? Date.now();
|
|
1440
|
+
};
|
|
1441
|
+
for (const event of events) {
|
|
1442
|
+
const params = event.params ?? {};
|
|
1443
|
+
switch (event.method) {
|
|
1444
|
+
case "Network.requestWillBeSent": {
|
|
1445
|
+
const requestId = String(params.requestId);
|
|
1446
|
+
if (!requestId) {
|
|
1447
|
+
break;
|
|
1448
|
+
}
|
|
1449
|
+
const request = params.request;
|
|
1450
|
+
const record = {
|
|
1451
|
+
id: requestId,
|
|
1452
|
+
url: request?.url,
|
|
1453
|
+
method: request?.method,
|
|
1454
|
+
requestHeaders: request?.headers ?? {},
|
|
1455
|
+
startTime: toTimestamp(event)
|
|
1456
|
+
};
|
|
1457
|
+
requests.set(requestId, record);
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
case "Network.responseReceived": {
|
|
1461
|
+
const requestId = String(params.requestId);
|
|
1462
|
+
if (!requestId) {
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
const response = params.response;
|
|
1466
|
+
const record = requests.get(requestId) ?? { id: requestId };
|
|
1467
|
+
record.status = response?.status;
|
|
1468
|
+
record.statusText = response?.statusText;
|
|
1469
|
+
record.mimeType = response?.mimeType;
|
|
1470
|
+
record.responseHeaders = response?.headers ?? {};
|
|
1471
|
+
record.protocol = response?.protocol;
|
|
1472
|
+
record.startTime = record.startTime ?? toTimestamp(event);
|
|
1473
|
+
requests.set(requestId, record);
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
case "Network.loadingFinished": {
|
|
1477
|
+
const requestId = String(params.requestId);
|
|
1478
|
+
if (!requestId) {
|
|
1479
|
+
break;
|
|
1480
|
+
}
|
|
1481
|
+
const record = requests.get(requestId) ?? { id: requestId };
|
|
1482
|
+
record.encodedDataLength = params.encodedDataLength;
|
|
1483
|
+
record.endTime = toTimestamp(event, record.startTime);
|
|
1484
|
+
requests.set(requestId, record);
|
|
1485
|
+
break;
|
|
1486
|
+
}
|
|
1487
|
+
case "Network.loadingFailed": {
|
|
1488
|
+
const requestId = String(params.requestId);
|
|
1489
|
+
if (!requestId) {
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
const record = requests.get(requestId) ?? { id: requestId };
|
|
1493
|
+
record.endTime = toTimestamp(event, record.startTime);
|
|
1494
|
+
requests.set(requestId, record);
|
|
1495
|
+
break;
|
|
1496
|
+
}
|
|
1497
|
+
default:
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const entries = Array.from(requests.values()).map((record) => {
|
|
1502
|
+
const started = record.startTime ?? Date.now();
|
|
1503
|
+
const ended = record.endTime ?? started;
|
|
1504
|
+
const time = Math.max(0, ended - started);
|
|
1505
|
+
const url = record.url ?? "";
|
|
1506
|
+
const queryString = [];
|
|
1543
1507
|
try {
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
|
|
1508
|
+
const parsed = new URL(url);
|
|
1509
|
+
parsed.searchParams.forEach((value, name) => {
|
|
1510
|
+
queryString.push({ name, value });
|
|
1511
|
+
});
|
|
1547
1512
|
} catch {
|
|
1548
|
-
const err = new InspectError(
|
|
1549
|
-
"EVALUATION_FAILED",
|
|
1550
|
-
"Failed to parse page content.",
|
|
1551
|
-
{ retryable: false }
|
|
1552
|
-
);
|
|
1553
|
-
this.recordError(err);
|
|
1554
|
-
throw err;
|
|
1555
1513
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1514
|
+
return {
|
|
1515
|
+
pageref: "page_0",
|
|
1516
|
+
startedDateTime: new Date(started).toISOString(),
|
|
1517
|
+
time,
|
|
1518
|
+
request: {
|
|
1519
|
+
method: record.method ?? "GET",
|
|
1520
|
+
url,
|
|
1521
|
+
httpVersion: record.protocol ?? "HTTP/1.1",
|
|
1522
|
+
cookies: [],
|
|
1523
|
+
headers: [],
|
|
1524
|
+
queryString,
|
|
1525
|
+
headersSize: -1,
|
|
1526
|
+
bodySize: -1
|
|
1527
|
+
},
|
|
1528
|
+
response: {
|
|
1529
|
+
status: record.status ?? 0,
|
|
1530
|
+
statusText: record.statusText ?? "",
|
|
1531
|
+
httpVersion: record.protocol ?? "HTTP/1.1",
|
|
1532
|
+
cookies: [],
|
|
1533
|
+
headers: [],
|
|
1534
|
+
redirectURL: "",
|
|
1535
|
+
headersSize: -1,
|
|
1536
|
+
bodySize: record.encodedDataLength ?? 0,
|
|
1537
|
+
content: {
|
|
1538
|
+
size: record.encodedDataLength ?? 0,
|
|
1539
|
+
mimeType: record.mimeType ?? ""
|
|
1540
|
+
}
|
|
1541
|
+
},
|
|
1542
|
+
cache: {},
|
|
1543
|
+
timings: {
|
|
1544
|
+
send: 0,
|
|
1545
|
+
wait: time,
|
|
1546
|
+
receive: 0
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
});
|
|
1550
|
+
const startedDateTime = entries.length ? entries[0].startedDateTime ?? (/* @__PURE__ */ new Date()).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
1551
|
+
return {
|
|
1552
|
+
log: {
|
|
1553
|
+
version: "1.2",
|
|
1554
|
+
creator: {
|
|
1555
|
+
name: "browser-bridge",
|
|
1556
|
+
version: "0.0.0"
|
|
1557
|
+
},
|
|
1558
|
+
pages: [
|
|
1559
|
+
{
|
|
1560
|
+
id: "page_0",
|
|
1561
|
+
title: title ?? "page",
|
|
1562
|
+
startedDateTime,
|
|
1563
|
+
pageTimings: {
|
|
1564
|
+
onContentLoad: -1,
|
|
1565
|
+
onLoad: -1
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
],
|
|
1569
|
+
entries
|
|
1564
1570
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1571
|
+
};
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
// packages/core/src/inspect/html-snapshot.ts
|
|
1575
|
+
var captureHtml = async (tabId, options) => {
|
|
1576
|
+
await options.debuggerCommand(tabId, "Runtime.enable", {});
|
|
1577
|
+
const expression = options.selector ? `(() => { try { const el = document.querySelector(${JSON.stringify(
|
|
1578
|
+
options.selector
|
|
1579
|
+
)}); return el ? el.outerHTML : ""; } catch { return ""; } })()` : "document.documentElement ? document.documentElement.outerHTML : ''";
|
|
1580
|
+
const result = await options.debuggerCommand(tabId, "Runtime.evaluate", {
|
|
1581
|
+
expression,
|
|
1582
|
+
returnByValue: true,
|
|
1583
|
+
awaitPromise: true
|
|
1584
|
+
});
|
|
1585
|
+
if (result && typeof result === "object" && "exceptionDetails" in result) {
|
|
1586
|
+
return options.onEvaluationFailed();
|
|
1587
|
+
}
|
|
1588
|
+
return String(
|
|
1589
|
+
result?.result?.value ?? ""
|
|
1590
|
+
);
|
|
1591
|
+
};
|
|
1592
|
+
var collectHtmlEntries = (html) => {
|
|
1593
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1594
|
+
const tagPattern = /<([a-zA-Z0-9-]+)([^>]*)>/g;
|
|
1595
|
+
let match;
|
|
1596
|
+
let index = 0;
|
|
1597
|
+
while ((match = tagPattern.exec(html)) && entries.size < 1e3) {
|
|
1598
|
+
const tag = match[1].toLowerCase();
|
|
1599
|
+
const attrs = match[2] ?? "";
|
|
1600
|
+
const idMatch = /\bid=["']([^"']+)["']/.exec(attrs);
|
|
1601
|
+
const classMatch = /\bclass=["']([^"']+)["']/.exec(attrs);
|
|
1602
|
+
const id = idMatch?.[1];
|
|
1603
|
+
const className = classMatch?.[1]?.split(/\s+/)[0];
|
|
1604
|
+
let key = tag;
|
|
1605
|
+
if (id) {
|
|
1606
|
+
key = `${tag}#${id}`;
|
|
1607
|
+
} else if (className) {
|
|
1608
|
+
key = `${tag}.${className}`;
|
|
1570
1609
|
} else {
|
|
1571
|
-
|
|
1572
|
-
content = turndown.turndown(article.content ?? "");
|
|
1610
|
+
key = `${tag}:nth-${index}`;
|
|
1573
1611
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
const output = {
|
|
1577
|
-
content,
|
|
1578
|
-
...includeMetadata ? {
|
|
1579
|
-
title: article.title ?? void 0,
|
|
1580
|
-
byline: article.byline ?? void 0,
|
|
1581
|
-
excerpt: article.excerpt ?? void 0,
|
|
1582
|
-
siteName: article.siteName ?? void 0
|
|
1583
|
-
} : {},
|
|
1584
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
1585
|
-
};
|
|
1586
|
-
this.markInspectConnected(input.sessionId);
|
|
1587
|
-
return output;
|
|
1612
|
+
entries.set(key, attrs.trim());
|
|
1613
|
+
index += 1;
|
|
1588
1614
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
expression,
|
|
1599
|
-
returnByValue: true,
|
|
1600
|
-
awaitPromise: true
|
|
1601
|
-
}
|
|
1602
|
-
);
|
|
1603
|
-
if (result && typeof result === "object" && "exceptionDetails" in result) {
|
|
1604
|
-
const error = new InspectError(
|
|
1605
|
-
"EVALUATION_FAILED",
|
|
1606
|
-
"Failed to capture page state.",
|
|
1607
|
-
{ retryable: false }
|
|
1608
|
-
);
|
|
1609
|
-
this.recordError(error);
|
|
1610
|
-
throw error;
|
|
1611
|
-
}
|
|
1612
|
-
const value = result?.result?.value;
|
|
1613
|
-
const raw = value && typeof value === "object" ? value : {};
|
|
1614
|
-
const warnings = [
|
|
1615
|
-
...Array.isArray(raw.warnings) ? raw.warnings : [],
|
|
1616
|
-
...selection.warnings ?? []
|
|
1617
|
-
];
|
|
1618
|
-
const output = {
|
|
1619
|
-
forms: Array.isArray(raw.forms) ? raw.forms : [],
|
|
1620
|
-
localStorage: Array.isArray(raw.localStorage) ? raw.localStorage : [],
|
|
1621
|
-
sessionStorage: Array.isArray(raw.sessionStorage) ? raw.sessionStorage : [],
|
|
1622
|
-
cookies: Array.isArray(raw.cookies) ? raw.cookies : [],
|
|
1623
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
1624
|
-
};
|
|
1625
|
-
this.markInspectConnected(input.sessionId);
|
|
1626
|
-
return output;
|
|
1627
|
-
}
|
|
1628
|
-
async performanceMetrics(input) {
|
|
1629
|
-
this.requireSession(input.sessionId);
|
|
1630
|
-
const selection = await this.resolveTab(input.targetHint);
|
|
1631
|
-
await this.debuggerCommand(selection.tabId, "Performance.enable", {});
|
|
1632
|
-
const result = await this.debuggerCommand(
|
|
1633
|
-
selection.tabId,
|
|
1634
|
-
"Performance.getMetrics",
|
|
1635
|
-
{}
|
|
1636
|
-
);
|
|
1637
|
-
const metrics = Array.isArray(result?.metrics) ? result.metrics.map((metric) => ({
|
|
1638
|
-
name: metric.name,
|
|
1639
|
-
value: metric.value
|
|
1640
|
-
})) : [];
|
|
1641
|
-
const output = { metrics, warnings: selection.warnings };
|
|
1642
|
-
this.markInspectConnected(input.sessionId);
|
|
1643
|
-
return output;
|
|
1644
|
-
}
|
|
1645
|
-
async screenshot(input) {
|
|
1646
|
-
this.requireSession(input.sessionId);
|
|
1647
|
-
const selection = await this.resolveTab(input.targetHint);
|
|
1648
|
-
await this.debuggerCommand(selection.tabId, "Page.enable", {});
|
|
1649
|
-
const format = input.format ?? "png";
|
|
1650
|
-
let captureParams = {
|
|
1651
|
-
format,
|
|
1652
|
-
fromSurface: true
|
|
1653
|
-
};
|
|
1654
|
-
if (format !== "png" && typeof input.quality === "number") {
|
|
1655
|
-
captureParams = { ...captureParams, quality: input.quality };
|
|
1656
|
-
}
|
|
1657
|
-
if (input.target === "full") {
|
|
1658
|
-
const layout = await this.debuggerCommand(
|
|
1659
|
-
selection.tabId,
|
|
1660
|
-
"Page.getLayoutMetrics",
|
|
1661
|
-
{}
|
|
1662
|
-
);
|
|
1663
|
-
const contentSize = layout?.contentSize;
|
|
1664
|
-
if (contentSize) {
|
|
1665
|
-
captureParams = {
|
|
1666
|
-
...captureParams,
|
|
1667
|
-
clip: {
|
|
1668
|
-
x: 0,
|
|
1669
|
-
y: 0,
|
|
1670
|
-
width: contentSize.width,
|
|
1671
|
-
height: contentSize.height,
|
|
1672
|
-
scale: 1
|
|
1673
|
-
}
|
|
1674
|
-
};
|
|
1675
|
-
} else {
|
|
1676
|
-
captureParams = { ...captureParams, captureBeyondViewport: true };
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
const result = await this.debuggerCommand(
|
|
1680
|
-
selection.tabId,
|
|
1681
|
-
"Page.captureScreenshot",
|
|
1682
|
-
captureParams
|
|
1683
|
-
);
|
|
1684
|
-
const data = result.data;
|
|
1685
|
-
if (!data) {
|
|
1686
|
-
const error = new InspectError(
|
|
1687
|
-
"INSPECT_UNAVAILABLE",
|
|
1688
|
-
"Failed to capture screenshot.",
|
|
1689
|
-
{ retryable: false }
|
|
1690
|
-
);
|
|
1691
|
-
this.recordError(error);
|
|
1692
|
-
throw error;
|
|
1693
|
-
}
|
|
1694
|
-
try {
|
|
1695
|
-
const rootDir = await ensureArtifactRootDir(input.sessionId);
|
|
1696
|
-
const artifactId = (0, import_crypto3.randomUUID)();
|
|
1697
|
-
const extension = format === "jpeg" ? "jpg" : format;
|
|
1698
|
-
const filePath = import_node_path2.default.join(
|
|
1699
|
-
rootDir,
|
|
1700
|
-
`screenshot-${artifactId}.${extension}`
|
|
1701
|
-
);
|
|
1702
|
-
await (0, import_promises2.writeFile)(filePath, Buffer.from(data, "base64"));
|
|
1703
|
-
const mime = format === "jpeg" ? "image/jpeg" : `image/${format}`;
|
|
1704
|
-
const output = {
|
|
1705
|
-
artifact_id: artifactId,
|
|
1706
|
-
path: filePath,
|
|
1707
|
-
mime
|
|
1708
|
-
};
|
|
1709
|
-
this.markInspectConnected(input.sessionId);
|
|
1710
|
-
return output;
|
|
1711
|
-
} catch {
|
|
1712
|
-
const error = new InspectError(
|
|
1713
|
-
"ARTIFACT_IO_ERROR",
|
|
1714
|
-
"Failed to write screenshot file."
|
|
1715
|
-
);
|
|
1716
|
-
this.recordError(error);
|
|
1717
|
-
throw error;
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
ensureDebugger() {
|
|
1721
|
-
if (!this.debugger) {
|
|
1722
|
-
const error = this.buildUnavailableError();
|
|
1723
|
-
this.recordError(error);
|
|
1724
|
-
throw error;
|
|
1725
|
-
}
|
|
1726
|
-
return this.debugger;
|
|
1727
|
-
}
|
|
1728
|
-
async resolveTab(hint) {
|
|
1729
|
-
if (!this.extensionBridge || !this.extensionBridge.isConnected()) {
|
|
1730
|
-
const error = new InspectError(
|
|
1731
|
-
"EXTENSION_DISCONNECTED",
|
|
1732
|
-
"Extension is not connected.",
|
|
1733
|
-
{ retryable: true }
|
|
1734
|
-
);
|
|
1735
|
-
this.recordError(error);
|
|
1736
|
-
throw error;
|
|
1737
|
-
}
|
|
1738
|
-
const tabs = this.extensionBridge.getStatus().tabs ?? [];
|
|
1739
|
-
if (!Array.isArray(tabs) || tabs.length === 0) {
|
|
1740
|
-
const error = new InspectError(
|
|
1741
|
-
"TAB_NOT_FOUND",
|
|
1742
|
-
"No tabs available to inspect."
|
|
1743
|
-
);
|
|
1744
|
-
this.recordError(error);
|
|
1745
|
-
throw error;
|
|
1746
|
-
}
|
|
1747
|
-
const candidates = tabs.map((tab2) => ({
|
|
1748
|
-
id: String(tab2.tab_id),
|
|
1749
|
-
url: tab2.url ?? "",
|
|
1750
|
-
title: tab2.title,
|
|
1751
|
-
lastSeenAt: tab2.last_active_at ? Date.parse(tab2.last_active_at) : void 0
|
|
1752
|
-
}));
|
|
1753
|
-
const ranked = pickBestTarget(candidates, hint);
|
|
1754
|
-
if (!ranked) {
|
|
1755
|
-
const error = new InspectError("TAB_NOT_FOUND", "No matching tab found.");
|
|
1756
|
-
this.recordError(error);
|
|
1757
|
-
throw error;
|
|
1758
|
-
}
|
|
1759
|
-
const tabId = Number(ranked.candidate.id);
|
|
1760
|
-
if (!Number.isFinite(tabId)) {
|
|
1761
|
-
const error = new InspectError(
|
|
1762
|
-
"TAB_NOT_FOUND",
|
|
1763
|
-
"Resolved tab id is invalid."
|
|
1764
|
-
);
|
|
1765
|
-
this.recordError(error);
|
|
1766
|
-
throw error;
|
|
1767
|
-
}
|
|
1768
|
-
const tab = tabs.find((entry) => entry.tab_id === tabId) ?? tabs[0];
|
|
1769
|
-
const warnings = [];
|
|
1770
|
-
if (!hint) {
|
|
1771
|
-
warnings.push("No target hint provided; using the most recent tab.");
|
|
1772
|
-
} else if (ranked.score < 20) {
|
|
1773
|
-
warnings.push("Weak target match; using best available tab.");
|
|
1774
|
-
}
|
|
1775
|
-
return {
|
|
1776
|
-
tabId,
|
|
1777
|
-
tab,
|
|
1778
|
-
warnings: warnings.length > 0 ? warnings : void 0
|
|
1779
|
-
};
|
|
1780
|
-
}
|
|
1781
|
-
async enableConsole(tabId) {
|
|
1782
|
-
await this.debuggerCommand(tabId, "Runtime.enable", {});
|
|
1783
|
-
await this.debuggerCommand(tabId, "Log.enable", {});
|
|
1784
|
-
}
|
|
1785
|
-
async enableNetwork(tabId) {
|
|
1786
|
-
await this.debuggerCommand(tabId, "Network.enable", {});
|
|
1787
|
-
}
|
|
1788
|
-
async enableAccessibility(tabId) {
|
|
1789
|
-
await this.debuggerCommand(tabId, "Accessibility.enable", {});
|
|
1615
|
+
return entries;
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
// packages/core/src/inspect/snapshot-history.ts
|
|
1619
|
+
var SnapshotHistory = class {
|
|
1620
|
+
constructor(options) {
|
|
1621
|
+
this.history = [];
|
|
1622
|
+
this.maxSnapshotsPerSession = Math.max(0, options.maxSnapshotsPerSession);
|
|
1623
|
+
this.maxSnapshotHistory = Math.max(0, options.maxSnapshotHistory);
|
|
1790
1624
|
}
|
|
1791
|
-
|
|
1625
|
+
record(sessionId, snapshot) {
|
|
1792
1626
|
const entries = this.collectSnapshotEntries(snapshot);
|
|
1793
1627
|
if (!entries) {
|
|
1794
1628
|
return;
|
|
1795
1629
|
}
|
|
1796
|
-
this.
|
|
1630
|
+
this.history.push({
|
|
1797
1631
|
sessionId,
|
|
1798
1632
|
format: snapshot.format,
|
|
1799
1633
|
entries,
|
|
1800
1634
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1801
1635
|
});
|
|
1802
1636
|
let count = 0;
|
|
1803
|
-
for (const record of this.
|
|
1637
|
+
for (const record of this.history) {
|
|
1804
1638
|
if (record.sessionId === sessionId) {
|
|
1805
1639
|
count += 1;
|
|
1806
1640
|
}
|
|
1807
1641
|
}
|
|
1808
1642
|
while (count > this.maxSnapshotsPerSession) {
|
|
1809
|
-
const index = this.
|
|
1643
|
+
const index = this.history.findIndex(
|
|
1810
1644
|
(record) => record.sessionId === sessionId
|
|
1811
1645
|
);
|
|
1812
1646
|
if (index === -1) {
|
|
1813
1647
|
break;
|
|
1814
1648
|
}
|
|
1815
|
-
this.
|
|
1649
|
+
this.history.splice(index, 1);
|
|
1816
1650
|
count -= 1;
|
|
1817
1651
|
}
|
|
1818
|
-
while (this.
|
|
1819
|
-
this.
|
|
1652
|
+
while (this.history.length > this.maxSnapshotHistory) {
|
|
1653
|
+
this.history.shift();
|
|
1820
1654
|
}
|
|
1821
1655
|
}
|
|
1656
|
+
diff(sessionId) {
|
|
1657
|
+
const snapshots = this.history.filter(
|
|
1658
|
+
(record) => record.sessionId === sessionId
|
|
1659
|
+
);
|
|
1660
|
+
if (snapshots.length < 2) {
|
|
1661
|
+
return {
|
|
1662
|
+
added: [],
|
|
1663
|
+
removed: [],
|
|
1664
|
+
changed: [],
|
|
1665
|
+
summary: "Not enough snapshots to diff."
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
const previous = snapshots[snapshots.length - 2];
|
|
1669
|
+
const current = snapshots[snapshots.length - 1];
|
|
1670
|
+
const added = [];
|
|
1671
|
+
const removed = [];
|
|
1672
|
+
const changed = [];
|
|
1673
|
+
for (const [key, value] of current.entries.entries()) {
|
|
1674
|
+
if (!previous.entries.has(key)) {
|
|
1675
|
+
added.push(key);
|
|
1676
|
+
} else if (previous.entries.get(key) !== value) {
|
|
1677
|
+
changed.push(key);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
for (const key of previous.entries.keys()) {
|
|
1681
|
+
if (!current.entries.has(key)) {
|
|
1682
|
+
removed.push(key);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
added,
|
|
1687
|
+
removed,
|
|
1688
|
+
changed,
|
|
1689
|
+
summary: `Added ${added.length}, removed ${removed.length}, changed ${changed.length}.`
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1822
1692
|
collectSnapshotEntries(snapshot) {
|
|
1823
1693
|
if (snapshot.format === "html" && typeof snapshot.snapshot === "string") {
|
|
1824
|
-
return
|
|
1694
|
+
return collectHtmlEntries(snapshot.snapshot);
|
|
1825
1695
|
}
|
|
1826
1696
|
if (snapshot.format === "ax") {
|
|
1827
1697
|
return this.collectAxEntries(snapshot.snapshot);
|
|
1828
1698
|
}
|
|
1829
1699
|
return null;
|
|
1830
1700
|
}
|
|
1831
|
-
|
|
1832
|
-
const
|
|
1833
|
-
|
|
1701
|
+
collectAxEntries(snapshot) {
|
|
1702
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1703
|
+
const nodes = getAxNodes(snapshot);
|
|
1704
|
+
if (nodes.length === 0) {
|
|
1705
|
+
return entries;
|
|
1706
|
+
}
|
|
1707
|
+
nodes.forEach((node, index) => {
|
|
1708
|
+
if (!node || typeof node !== "object") {
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
const record = node;
|
|
1712
|
+
const role = typeof record.role === "string" ? record.role : record.role?.value ?? "node";
|
|
1713
|
+
const name = typeof record.name === "string" ? record.name : record.name?.value ?? "";
|
|
1714
|
+
const nodeId = record.nodeId ?? (record.backendDOMNodeId !== void 0 ? String(record.backendDOMNodeId) : void 0);
|
|
1715
|
+
const key = nodeId ? `node-${nodeId}` : `${role}:${name}:${index}`;
|
|
1716
|
+
entries.set(key, `${role}:${name}`);
|
|
1717
|
+
});
|
|
1718
|
+
return entries;
|
|
1834
1719
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
// packages/core/src/inspect/snapshot-refs.ts
|
|
1723
|
+
var SNAPSHOT_REF_ATTRIBUTE = "data-bv-ref";
|
|
1724
|
+
var MAX_REF_ASSIGNMENTS = 500;
|
|
1725
|
+
var MAX_REF_WARNINGS = 5;
|
|
1726
|
+
var isInspectError = (error) => Boolean(
|
|
1727
|
+
error && typeof error === "object" && "name" in error && error.name === "InspectError" && "message" in error && typeof error.message === "string"
|
|
1728
|
+
);
|
|
1729
|
+
var assignRefsToAxSnapshot = (snapshot) => {
|
|
1730
|
+
const nodes = getAxNodes(snapshot);
|
|
1731
|
+
const refs = /* @__PURE__ */ new Map();
|
|
1732
|
+
let index = 1;
|
|
1733
|
+
for (const node of nodes) {
|
|
1734
|
+
if (!node || typeof node !== "object") {
|
|
1735
|
+
continue;
|
|
1839
1736
|
}
|
|
1840
|
-
if (
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1737
|
+
if (node.ignored) {
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
const backendId = node.backendDOMNodeId;
|
|
1741
|
+
if (typeof backendId !== "number") {
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
const ref = `@e${index}`;
|
|
1745
|
+
index += 1;
|
|
1746
|
+
node.ref = ref;
|
|
1747
|
+
refs.set(backendId, ref);
|
|
1748
|
+
}
|
|
1749
|
+
return refs;
|
|
1750
|
+
};
|
|
1751
|
+
var clearSnapshotRefs = async (tabId, debuggerCommand) => {
|
|
1752
|
+
await debuggerCommand(tabId, "Runtime.evaluate", {
|
|
1753
|
+
expression: `document.querySelectorAll('[${SNAPSHOT_REF_ATTRIBUTE}]').forEach((el) => el.removeAttribute('${SNAPSHOT_REF_ATTRIBUTE}'))`,
|
|
1754
|
+
returnByValue: true,
|
|
1755
|
+
awaitPromise: true
|
|
1756
|
+
});
|
|
1757
|
+
};
|
|
1758
|
+
var applySnapshotRefs = async (tabId, refs, debuggerCommand) => {
|
|
1759
|
+
const warnings = [];
|
|
1760
|
+
await debuggerCommand(tabId, "DOM.enable", {});
|
|
1761
|
+
await debuggerCommand(tabId, "Runtime.enable", {});
|
|
1762
|
+
try {
|
|
1763
|
+
await clearSnapshotRefs(tabId, debuggerCommand);
|
|
1764
|
+
} catch {
|
|
1765
|
+
warnings.push("Failed to clear prior snapshot refs.");
|
|
1766
|
+
}
|
|
1767
|
+
if (refs.size === 0) {
|
|
1768
|
+
return warnings;
|
|
1769
|
+
}
|
|
1770
|
+
let applied = 0;
|
|
1771
|
+
for (const [backendNodeId, ref] of refs) {
|
|
1772
|
+
if (applied >= MAX_REF_ASSIGNMENTS) {
|
|
1773
|
+
warnings.push(
|
|
1774
|
+
`Snapshot refs truncated at ${MAX_REF_ASSIGNMENTS} elements.`
|
|
1844
1775
|
);
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
try {
|
|
1779
|
+
const described = await debuggerCommand(tabId, "DOM.describeNode", {
|
|
1780
|
+
backendNodeId
|
|
1781
|
+
});
|
|
1782
|
+
const node = described.node;
|
|
1783
|
+
if (!node || node.nodeType !== 1 || typeof node.nodeId !== "number") {
|
|
1784
|
+
if (warnings.length < MAX_REF_WARNINGS) {
|
|
1785
|
+
warnings.push(`Ref ${ref} could not be applied to a DOM element.`);
|
|
1786
|
+
}
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
await debuggerCommand(tabId, "DOM.setAttributeValue", {
|
|
1790
|
+
nodeId: node.nodeId,
|
|
1791
|
+
name: SNAPSHOT_REF_ATTRIBUTE,
|
|
1792
|
+
value: ref
|
|
1793
|
+
});
|
|
1794
|
+
applied += 1;
|
|
1795
|
+
} catch {
|
|
1796
|
+
if (warnings.length < MAX_REF_WARNINGS) {
|
|
1797
|
+
warnings.push(`Ref ${ref} could not be applied.`);
|
|
1798
|
+
}
|
|
1845
1799
|
}
|
|
1846
|
-
return filtered;
|
|
1847
1800
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
if (
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1801
|
+
return warnings;
|
|
1802
|
+
};
|
|
1803
|
+
var resolveNodeIdForSelector = async (tabId, selector, debuggerCommand) => {
|
|
1804
|
+
await debuggerCommand(tabId, "DOM.enable", {});
|
|
1805
|
+
const document = await debuggerCommand(tabId, "DOM.getDocument", {
|
|
1806
|
+
depth: 1
|
|
1807
|
+
});
|
|
1808
|
+
const rootNodeId = document.root?.nodeId;
|
|
1809
|
+
if (typeof rootNodeId !== "number") {
|
|
1810
|
+
return { warnings: ["Failed to resolve DOM root for selector."] };
|
|
1811
|
+
}
|
|
1812
|
+
try {
|
|
1813
|
+
const result = await debuggerCommand(tabId, "DOM.querySelector", {
|
|
1814
|
+
nodeId: rootNodeId,
|
|
1815
|
+
selector
|
|
1816
|
+
});
|
|
1817
|
+
const nodeId = result.nodeId;
|
|
1818
|
+
if (!nodeId) {
|
|
1819
|
+
return { warnings: [`Selector not found: ${selector}`] };
|
|
1820
|
+
}
|
|
1821
|
+
return { nodeId };
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
if (isInspectError(error)) {
|
|
1824
|
+
return { warnings: [error.message] };
|
|
1825
|
+
}
|
|
1826
|
+
return { warnings: ["Selector query failed."] };
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
// packages/core/src/page-state-script.ts
|
|
1831
|
+
var PAGE_STATE_SCRIPT = [
|
|
1832
|
+
"(() => {",
|
|
1833
|
+
" const escape = (value) => {",
|
|
1834
|
+
" if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {",
|
|
1835
|
+
" return CSS.escape(value);",
|
|
1836
|
+
" }",
|
|
1837
|
+
` return String(value).replace(/["'\\\\]/g, '\\\\$&');`,
|
|
1838
|
+
" };",
|
|
1839
|
+
" const truncate = (value, max) => {",
|
|
1840
|
+
" const text = String(value ?? '');",
|
|
1841
|
+
" return text.length > max ? text.slice(0, max) : text;",
|
|
1842
|
+
" };",
|
|
1843
|
+
" const selectorFor = (element) => {",
|
|
1844
|
+
" if (element.id) {",
|
|
1845
|
+
" return `#${escape(element.id)}`;",
|
|
1846
|
+
" }",
|
|
1847
|
+
" const name = element.getAttribute('name');",
|
|
1848
|
+
" if (name) {",
|
|
1849
|
+
' return `${element.tagName.toLowerCase()}[name="${escape(name)}"]`;',
|
|
1850
|
+
" }",
|
|
1851
|
+
" const parts = [];",
|
|
1852
|
+
" let node = element;",
|
|
1853
|
+
" while (node && node.nodeType === 1 && parts.length < 4) {",
|
|
1854
|
+
" let part = node.tagName.toLowerCase();",
|
|
1855
|
+
" const parent = node.parentElement;",
|
|
1856
|
+
" if (parent) {",
|
|
1857
|
+
" const siblings = Array.from(parent.children).filter(",
|
|
1858
|
+
" (child) => child.tagName === node.tagName",
|
|
1859
|
+
" );",
|
|
1860
|
+
" if (siblings.length > 1) {",
|
|
1861
|
+
" part += `:nth-of-type(${siblings.indexOf(node) + 1})`;",
|
|
1862
|
+
" }",
|
|
1863
|
+
" }",
|
|
1864
|
+
" parts.unshift(part);",
|
|
1865
|
+
" node = parent;",
|
|
1866
|
+
" }",
|
|
1867
|
+
" return parts.join('>');",
|
|
1868
|
+
" };",
|
|
1869
|
+
" const readStorage = (storage, limit) => {",
|
|
1870
|
+
" try {",
|
|
1871
|
+
" return Object.keys(storage)",
|
|
1872
|
+
" .slice(0, limit)",
|
|
1873
|
+
" .map((key) => ({",
|
|
1874
|
+
" key,",
|
|
1875
|
+
" value: truncate(storage.getItem(key), 500),",
|
|
1876
|
+
" }));",
|
|
1877
|
+
" } catch {",
|
|
1878
|
+
" return [];",
|
|
1879
|
+
" }",
|
|
1880
|
+
" };",
|
|
1881
|
+
" const forms = Array.from(document.querySelectorAll('form')).map((form) => {",
|
|
1882
|
+
" const fields = Array.from(form.elements)",
|
|
1883
|
+
" .filter((element) => element && element.tagName)",
|
|
1884
|
+
" .map((element) => {",
|
|
1885
|
+
" const tag = element.tagName.toLowerCase();",
|
|
1886
|
+
" const type = 'type' in element && element.type ? element.type : tag;",
|
|
1887
|
+
" const name = element.name || element.getAttribute('name') || element.id || '';",
|
|
1888
|
+
" let value = '';",
|
|
1889
|
+
" let options;",
|
|
1890
|
+
" if (tag === 'select') {",
|
|
1891
|
+
" const select = element;",
|
|
1892
|
+
" value = select.value ?? '';",
|
|
1893
|
+
" options = Array.from(select.options).map((option) => option.text);",
|
|
1894
|
+
" } else if (tag === 'input' && element.type === 'password') {",
|
|
1895
|
+
" value = '[redacted]';",
|
|
1896
|
+
" } else if (tag === 'input' || tag === 'textarea') {",
|
|
1897
|
+
" value = element.value ?? '';",
|
|
1898
|
+
" } else if (element.isContentEditable) {",
|
|
1899
|
+
" value = element.textContent ?? '';",
|
|
1900
|
+
" } else if ('value' in element) {",
|
|
1901
|
+
" value = element.value ?? '';",
|
|
1902
|
+
" }",
|
|
1903
|
+
" return {",
|
|
1904
|
+
" name,",
|
|
1905
|
+
" type,",
|
|
1906
|
+
" value: truncate(value, 500),",
|
|
1907
|
+
" ...(options ? { options } : {}),",
|
|
1908
|
+
" };",
|
|
1909
|
+
" });",
|
|
1910
|
+
" return {",
|
|
1911
|
+
" selector: selectorFor(form),",
|
|
1912
|
+
" action: form.getAttribute('action') || undefined,",
|
|
1913
|
+
" method: form.getAttribute('method') || undefined,",
|
|
1914
|
+
" fields,",
|
|
1915
|
+
" };",
|
|
1916
|
+
" });",
|
|
1917
|
+
" const localStorage = readStorage(window.localStorage, 100);",
|
|
1918
|
+
" const sessionStorage = readStorage(window.sessionStorage, 100);",
|
|
1919
|
+
" const cookies = (document.cookie ? document.cookie.split(';') : [])",
|
|
1920
|
+
" .map((entry) => entry.trim())",
|
|
1921
|
+
" .filter((entry) => entry.length > 0)",
|
|
1922
|
+
" .slice(0, 50)",
|
|
1923
|
+
" .map((entry) => {",
|
|
1924
|
+
" const [key, ...rest] = entry.split('=');",
|
|
1925
|
+
" return { key, value: truncate(rest.join('='), 500) };",
|
|
1926
|
+
" });",
|
|
1927
|
+
" return { forms, localStorage, sessionStorage, cookies };",
|
|
1928
|
+
"})()"
|
|
1929
|
+
].join("\n");
|
|
1930
|
+
|
|
1931
|
+
// packages/core/src/target-matching.ts
|
|
1932
|
+
var normalizeText = (value) => (value ?? "").trim().toLowerCase();
|
|
1933
|
+
var normalizeUrl = (value) => {
|
|
1934
|
+
const normalized = normalizeText(value);
|
|
1935
|
+
if (!normalized) {
|
|
1936
|
+
return "";
|
|
1937
|
+
}
|
|
1938
|
+
const hashIndex = normalized.indexOf("#");
|
|
1939
|
+
const withoutHash = hashIndex >= 0 ? normalized.slice(0, hashIndex) : normalized;
|
|
1940
|
+
return withoutHash.endsWith("/") && withoutHash.length > 1 ? withoutHash.slice(0, -1) : withoutHash;
|
|
1941
|
+
};
|
|
1942
|
+
var scoreUrlMatch = (candidateUrl, hintUrl) => {
|
|
1943
|
+
if (!candidateUrl || !hintUrl) {
|
|
1944
|
+
return 0;
|
|
1945
|
+
}
|
|
1946
|
+
if (candidateUrl === hintUrl) {
|
|
1947
|
+
return 100;
|
|
1948
|
+
}
|
|
1949
|
+
if (candidateUrl.includes(hintUrl) || hintUrl.includes(candidateUrl)) {
|
|
1950
|
+
return 60;
|
|
1951
|
+
}
|
|
1952
|
+
return 0;
|
|
1953
|
+
};
|
|
1954
|
+
var scoreTitleMatch = (candidateTitle, hintTitle) => {
|
|
1955
|
+
if (!candidateTitle || !hintTitle) {
|
|
1956
|
+
return 0;
|
|
1957
|
+
}
|
|
1958
|
+
if (candidateTitle === hintTitle) {
|
|
1959
|
+
return 80;
|
|
1960
|
+
}
|
|
1961
|
+
if (candidateTitle.includes(hintTitle) || hintTitle.includes(candidateTitle)) {
|
|
1962
|
+
return 40;
|
|
1963
|
+
}
|
|
1964
|
+
return 0;
|
|
1965
|
+
};
|
|
1966
|
+
var scoreRecency = (lastSeenAt, now) => {
|
|
1967
|
+
if (!lastSeenAt) {
|
|
1968
|
+
return 0;
|
|
1969
|
+
}
|
|
1970
|
+
const ageMinutes = Math.max(0, (now - lastSeenAt) / 6e4);
|
|
1971
|
+
return Math.max(0, 30 - ageMinutes);
|
|
1972
|
+
};
|
|
1973
|
+
var scoreHintRecency = (lastSeenAt, hintLastActiveAt) => {
|
|
1974
|
+
if (!lastSeenAt || !hintLastActiveAt) {
|
|
1975
|
+
return 0;
|
|
1976
|
+
}
|
|
1977
|
+
const deltaMinutes = Math.abs(lastSeenAt - hintLastActiveAt) / 6e4;
|
|
1978
|
+
return Math.max(0, 20 - deltaMinutes);
|
|
1979
|
+
};
|
|
1980
|
+
var isBlankUrl = (url) => url.length === 0 || url === "about:blank";
|
|
1981
|
+
var rankTargetCandidates = (candidates, hint, now = Date.now()) => {
|
|
1982
|
+
const normalizedHintUrl = normalizeUrl(hint?.url);
|
|
1983
|
+
const normalizedHintTitle = normalizeText(hint?.title);
|
|
1984
|
+
const hintLastActiveAt = hint?.lastActiveAt ? Date.parse(hint.lastActiveAt) : void 0;
|
|
1985
|
+
const ranked = candidates.map((candidate) => {
|
|
1986
|
+
const reasons = [];
|
|
1987
|
+
const normalizedUrl = normalizeUrl(candidate.url);
|
|
1988
|
+
const normalizedTitle = normalizeText(candidate.title);
|
|
1989
|
+
let score = 0;
|
|
1990
|
+
const urlScore = scoreUrlMatch(normalizedUrl, normalizedHintUrl);
|
|
1991
|
+
if (urlScore > 0) {
|
|
1992
|
+
score += urlScore;
|
|
1993
|
+
reasons.push(urlScore >= 100 ? "url:exact" : "url:partial");
|
|
1994
|
+
}
|
|
1995
|
+
const titleScore = scoreTitleMatch(normalizedTitle, normalizedHintTitle);
|
|
1996
|
+
if (titleScore > 0) {
|
|
1997
|
+
score += titleScore;
|
|
1998
|
+
reasons.push(titleScore >= 80 ? "title:exact" : "title:partial");
|
|
1999
|
+
}
|
|
2000
|
+
const recencyScore = scoreRecency(candidate.lastSeenAt, now);
|
|
2001
|
+
if (recencyScore > 0) {
|
|
2002
|
+
score += recencyScore;
|
|
2003
|
+
reasons.push("recency");
|
|
2004
|
+
}
|
|
2005
|
+
const hintRecencyScore = scoreHintRecency(
|
|
2006
|
+
candidate.lastSeenAt,
|
|
2007
|
+
Number.isFinite(hintLastActiveAt) ? hintLastActiveAt : void 0
|
|
2008
|
+
);
|
|
2009
|
+
if (hintRecencyScore > 0) {
|
|
2010
|
+
score += hintRecencyScore;
|
|
2011
|
+
reasons.push("hint-recency");
|
|
2012
|
+
}
|
|
2013
|
+
if (isBlankUrl(normalizedUrl)) {
|
|
2014
|
+
score -= 50;
|
|
2015
|
+
reasons.push("blank-url");
|
|
2016
|
+
}
|
|
2017
|
+
return { candidate, score, reasons };
|
|
2018
|
+
});
|
|
2019
|
+
return ranked.sort((a, b) => {
|
|
2020
|
+
if (a.score !== b.score) {
|
|
2021
|
+
return b.score - a.score;
|
|
2022
|
+
}
|
|
2023
|
+
const aSeen = a.candidate.lastSeenAt ?? 0;
|
|
2024
|
+
const bSeen = b.candidate.lastSeenAt ?? 0;
|
|
2025
|
+
if (aSeen !== bSeen) {
|
|
2026
|
+
return bSeen - aSeen;
|
|
2027
|
+
}
|
|
2028
|
+
return a.candidate.url.localeCompare(b.candidate.url);
|
|
2029
|
+
});
|
|
2030
|
+
};
|
|
2031
|
+
var pickBestTarget = (candidates, hint, now = Date.now()) => {
|
|
2032
|
+
const ranked = rankTargetCandidates(candidates, hint, now);
|
|
2033
|
+
return ranked.length > 0 ? ranked[0] : null;
|
|
2034
|
+
};
|
|
2035
|
+
|
|
2036
|
+
// packages/core/src/inspect/service.ts
|
|
2037
|
+
var DEFAULT_MAX_SNAPSHOTS_PER_SESSION = 20;
|
|
2038
|
+
var DEFAULT_MAX_SNAPSHOT_HISTORY = 100;
|
|
2039
|
+
var InspectService = class {
|
|
2040
|
+
constructor(options) {
|
|
2041
|
+
this.registry = options.registry;
|
|
2042
|
+
this.debugger = options.debuggerBridge;
|
|
2043
|
+
this.extensionBridge = options.extensionBridge;
|
|
2044
|
+
const maxSnapshotsPerSession = options.maxSnapshotsPerSession ?? DEFAULT_MAX_SNAPSHOTS_PER_SESSION;
|
|
2045
|
+
const maxSnapshotHistory = options.maxSnapshotHistory ?? DEFAULT_MAX_SNAPSHOT_HISTORY;
|
|
2046
|
+
this.snapshots = new SnapshotHistory({
|
|
2047
|
+
maxSnapshotsPerSession,
|
|
2048
|
+
maxSnapshotHistory
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
isConnected() {
|
|
2052
|
+
return this.debugger?.hasAttachments() ?? false;
|
|
2053
|
+
}
|
|
2054
|
+
getLastError() {
|
|
2055
|
+
if (!this.lastError || !this.lastErrorAt) {
|
|
2056
|
+
const debuggerError = this.debugger?.getLastError();
|
|
2057
|
+
if (!debuggerError) {
|
|
2058
|
+
return void 0;
|
|
1871
2059
|
}
|
|
1872
2060
|
return {
|
|
1873
|
-
|
|
1874
|
-
|
|
2061
|
+
error: new InspectError(
|
|
2062
|
+
"INSPECT_UNAVAILABLE",
|
|
2063
|
+
debuggerError.error.message,
|
|
2064
|
+
{
|
|
2065
|
+
retryable: debuggerError.error.retryable,
|
|
2066
|
+
details: {
|
|
2067
|
+
code: debuggerError.error.code,
|
|
2068
|
+
...debuggerError.error.details ? debuggerError.error.details : {}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
),
|
|
2072
|
+
at: debuggerError.at
|
|
1875
2073
|
};
|
|
1876
2074
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2075
|
+
return { error: this.lastError, at: this.lastErrorAt };
|
|
2076
|
+
}
|
|
2077
|
+
async reconnect(sessionId) {
|
|
2078
|
+
try {
|
|
2079
|
+
this.requireSession(sessionId);
|
|
2080
|
+
const selection = await this.resolveTab();
|
|
2081
|
+
const debuggerBridge = this.ensureDebugger();
|
|
2082
|
+
const result = await debuggerBridge.attach(selection.tabId);
|
|
2083
|
+
if (result.ok) {
|
|
2084
|
+
this.markInspectConnected(sessionId);
|
|
2085
|
+
return true;
|
|
1886
2086
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
)
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
(node) => node && typeof node.nodeId === "string"
|
|
1894
|
-
)?.nodeId;
|
|
1895
|
-
if (first) {
|
|
1896
|
-
roots = [first];
|
|
2087
|
+
const error = this.mapDebuggerError(result.error);
|
|
2088
|
+
this.recordError(error);
|
|
2089
|
+
return false;
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
if (error instanceof InspectError) {
|
|
2092
|
+
this.recordError(error);
|
|
1897
2093
|
}
|
|
2094
|
+
return false;
|
|
1898
2095
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2096
|
+
}
|
|
2097
|
+
async domSnapshot(input) {
|
|
2098
|
+
this.requireSession(input.sessionId);
|
|
2099
|
+
const selection = await this.resolveTab(input.targetHint);
|
|
2100
|
+
const debuggerCommand = this.debuggerCommand.bind(this);
|
|
2101
|
+
const work = async () => {
|
|
2102
|
+
if (input.format === "html") {
|
|
2103
|
+
const html = await captureHtml(selection.tabId, {
|
|
2104
|
+
selector: input.selector,
|
|
2105
|
+
debuggerCommand,
|
|
2106
|
+
onEvaluationFailed: () => {
|
|
2107
|
+
const error = new InspectError(
|
|
2108
|
+
"EVALUATION_FAILED",
|
|
2109
|
+
"Failed to evaluate HTML snapshot.",
|
|
2110
|
+
{ retryable: false }
|
|
2111
|
+
);
|
|
2112
|
+
this.recordError(error);
|
|
2113
|
+
throw error;
|
|
1917
2114
|
}
|
|
2115
|
+
});
|
|
2116
|
+
const warnings = [...selection.warnings ?? []];
|
|
2117
|
+
if (input.interactive) {
|
|
2118
|
+
warnings.push(
|
|
2119
|
+
"Interactive filter is only supported for AX snapshots."
|
|
2120
|
+
);
|
|
1918
2121
|
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
if (kept.size === 0) {
|
|
1922
|
-
const fallback = [];
|
|
1923
|
-
for (const node of nodes) {
|
|
1924
|
-
if (fallback.length >= maxNodes) {
|
|
1925
|
-
break;
|
|
2122
|
+
if (input.compact) {
|
|
2123
|
+
warnings.push("Compact filter is only supported for AX snapshots.");
|
|
1926
2124
|
}
|
|
1927
|
-
if (
|
|
1928
|
-
|
|
2125
|
+
if (input.maxNodes !== void 0) {
|
|
2126
|
+
warnings.push("max_nodes is only supported for AX snapshots.");
|
|
2127
|
+
}
|
|
2128
|
+
if (input.selector && html === "") {
|
|
2129
|
+
warnings.push(`Selector not found: ${input.selector}`);
|
|
1929
2130
|
}
|
|
2131
|
+
return {
|
|
2132
|
+
format: "html",
|
|
2133
|
+
snapshot: html,
|
|
2134
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
2135
|
+
};
|
|
1930
2136
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2137
|
+
try {
|
|
2138
|
+
await this.enableAccessibility(selection.tabId);
|
|
2139
|
+
const selectorWarnings = [];
|
|
2140
|
+
let result2;
|
|
2141
|
+
if (input.selector) {
|
|
2142
|
+
const resolved = await resolveNodeIdForSelector(
|
|
2143
|
+
selection.tabId,
|
|
2144
|
+
input.selector,
|
|
2145
|
+
debuggerCommand
|
|
2146
|
+
);
|
|
2147
|
+
selectorWarnings.push(...resolved.warnings ?? []);
|
|
2148
|
+
if (!resolved.nodeId) {
|
|
2149
|
+
let refWarnings2 = [];
|
|
2150
|
+
try {
|
|
2151
|
+
refWarnings2 = await applySnapshotRefs(
|
|
2152
|
+
selection.tabId,
|
|
2153
|
+
/* @__PURE__ */ new Map(),
|
|
2154
|
+
debuggerCommand
|
|
2155
|
+
);
|
|
2156
|
+
} catch {
|
|
2157
|
+
refWarnings2 = ["Failed to clear prior snapshot refs."];
|
|
2158
|
+
}
|
|
2159
|
+
const warnings2 = [
|
|
2160
|
+
...selection.warnings ?? [],
|
|
2161
|
+
...selectorWarnings,
|
|
2162
|
+
...refWarnings2
|
|
2163
|
+
];
|
|
2164
|
+
return {
|
|
2165
|
+
format: "ax",
|
|
2166
|
+
snapshot: { nodes: [] },
|
|
2167
|
+
...warnings2.length > 0 ? { warnings: warnings2 } : {}
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
result2 = await this.debuggerCommand(
|
|
2171
|
+
selection.tabId,
|
|
2172
|
+
"Accessibility.getPartialAXTree",
|
|
2173
|
+
{ nodeId: resolved.nodeId }
|
|
2174
|
+
);
|
|
2175
|
+
} else {
|
|
2176
|
+
result2 = await this.debuggerCommand(
|
|
2177
|
+
selection.tabId,
|
|
2178
|
+
"Accessibility.getFullAXTree",
|
|
2179
|
+
{}
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
let snapshot = input.interactive || input.compact ? applyAxSnapshotFilters(result2, {
|
|
2183
|
+
interactiveOnly: input.interactive,
|
|
2184
|
+
compact: input.compact
|
|
2185
|
+
}) : result2;
|
|
2186
|
+
let truncated = false;
|
|
2187
|
+
const truncationWarnings = [];
|
|
2188
|
+
if (input.maxNodes !== void 0) {
|
|
2189
|
+
const truncatedResult = truncateAxSnapshot(snapshot, input.maxNodes);
|
|
2190
|
+
snapshot = truncatedResult.snapshot;
|
|
2191
|
+
truncated = truncatedResult.truncated;
|
|
2192
|
+
if (truncated) {
|
|
2193
|
+
truncationWarnings.push(
|
|
2194
|
+
`AX snapshot truncated to ${input.maxNodes} nodes.`
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
const refMap = assignRefsToAxSnapshot(snapshot);
|
|
2199
|
+
const refWarnings = await applySnapshotRefs(
|
|
2200
|
+
selection.tabId,
|
|
2201
|
+
refMap,
|
|
2202
|
+
debuggerCommand
|
|
1940
2203
|
);
|
|
2204
|
+
const warnings = [
|
|
2205
|
+
...selection.warnings ?? [],
|
|
2206
|
+
...selectorWarnings,
|
|
2207
|
+
...truncationWarnings,
|
|
2208
|
+
...refWarnings ?? []
|
|
2209
|
+
];
|
|
2210
|
+
return {
|
|
2211
|
+
format: "ax",
|
|
2212
|
+
snapshot,
|
|
2213
|
+
...truncated ? { truncated: true } : {},
|
|
2214
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
2215
|
+
};
|
|
2216
|
+
} catch (error) {
|
|
2217
|
+
if (error instanceof InspectError) {
|
|
2218
|
+
const fallbackCodes = [
|
|
2219
|
+
"NOT_SUPPORTED",
|
|
2220
|
+
"INSPECT_UNAVAILABLE",
|
|
2221
|
+
"EVALUATION_FAILED"
|
|
2222
|
+
];
|
|
2223
|
+
if (!fallbackCodes.includes(error.code)) {
|
|
2224
|
+
throw error;
|
|
2225
|
+
}
|
|
2226
|
+
const html = await captureHtml(selection.tabId, {
|
|
2227
|
+
selector: input.selector,
|
|
2228
|
+
debuggerCommand,
|
|
2229
|
+
onEvaluationFailed: () => {
|
|
2230
|
+
const error2 = new InspectError(
|
|
2231
|
+
"EVALUATION_FAILED",
|
|
2232
|
+
"Failed to evaluate HTML snapshot.",
|
|
2233
|
+
{ retryable: false }
|
|
2234
|
+
);
|
|
2235
|
+
this.recordError(error2);
|
|
2236
|
+
throw error2;
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
const warnings = [
|
|
2240
|
+
...selection.warnings ?? [],
|
|
2241
|
+
"AX snapshot failed; returned HTML instead.",
|
|
2242
|
+
...input.maxNodes !== void 0 ? ["max_nodes is only supported for AX snapshots."] : [],
|
|
2243
|
+
...input.interactive ? ["Interactive filter is only supported for AX snapshots."] : [],
|
|
2244
|
+
...input.compact ? ["Compact filter is only supported for AX snapshots."] : [],
|
|
2245
|
+
...input.selector && html === "" ? [`Selector not found: ${input.selector}`] : []
|
|
2246
|
+
];
|
|
2247
|
+
return {
|
|
2248
|
+
format: "html",
|
|
2249
|
+
snapshot: html,
|
|
2250
|
+
warnings
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
throw error;
|
|
1941
2254
|
}
|
|
1942
|
-
}
|
|
1943
|
-
return {
|
|
1944
|
-
snapshot: this.replaceAxNodes(snapshot, filtered),
|
|
1945
|
-
truncated: true
|
|
1946
2255
|
};
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
return
|
|
1952
|
-
}
|
|
1953
|
-
const keepIds = /* @__PURE__ */ new Set();
|
|
1954
|
-
const filtered = nodes.filter((node) => {
|
|
1955
|
-
if (!node || typeof node !== "object") {
|
|
1956
|
-
return false;
|
|
1957
|
-
}
|
|
1958
|
-
const keep = predicate(node);
|
|
1959
|
-
if (keep && typeof node.nodeId === "string") {
|
|
1960
|
-
keepIds.add(node.nodeId);
|
|
1961
|
-
}
|
|
1962
|
-
return keep;
|
|
1963
|
-
});
|
|
1964
|
-
for (const node of filtered) {
|
|
1965
|
-
if (Array.isArray(node.childIds)) {
|
|
1966
|
-
node.childIds = node.childIds.filter((id) => keepIds.has(id));
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
return this.replaceAxNodes(snapshot, filtered);
|
|
1970
|
-
}
|
|
1971
|
-
replaceAxNodes(snapshot, nodes) {
|
|
1972
|
-
if (Array.isArray(snapshot)) {
|
|
1973
|
-
return nodes;
|
|
1974
|
-
}
|
|
1975
|
-
if (snapshot && typeof snapshot === "object") {
|
|
1976
|
-
snapshot.nodes = nodes;
|
|
2256
|
+
if (input.consistency === "quiesce") {
|
|
2257
|
+
const result2 = await driveMutex.runExclusive(work);
|
|
2258
|
+
this.snapshots.record(input.sessionId, result2);
|
|
2259
|
+
this.markInspectConnected(input.sessionId);
|
|
2260
|
+
return result2;
|
|
1977
2261
|
}
|
|
1978
|
-
|
|
2262
|
+
const result = await work();
|
|
2263
|
+
this.snapshots.record(input.sessionId, result);
|
|
2264
|
+
this.markInspectConnected(input.sessionId);
|
|
2265
|
+
return result;
|
|
1979
2266
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2267
|
+
domDiff(input) {
|
|
2268
|
+
this.requireSession(input.sessionId);
|
|
2269
|
+
this.markInspectConnected(input.sessionId);
|
|
2270
|
+
return this.snapshots.diff(input.sessionId);
|
|
1983
2271
|
}
|
|
1984
|
-
|
|
1985
|
-
const
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
nodes.forEach((node) => {
|
|
1991
|
-
if (node && typeof node.nodeId === "string") {
|
|
1992
|
-
nodeById.set(node.nodeId, node);
|
|
1993
|
-
}
|
|
2272
|
+
async find(input) {
|
|
2273
|
+
const snapshot = await this.domSnapshot({
|
|
2274
|
+
sessionId: input.sessionId,
|
|
2275
|
+
format: "ax",
|
|
2276
|
+
consistency: "best_effort",
|
|
2277
|
+
targetHint: input.targetHint
|
|
1994
2278
|
});
|
|
1995
|
-
const
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
const filtered = nodes.filter(
|
|
2005
|
-
(node) => node && typeof node.nodeId === "string" && keepIds.has(node.nodeId)
|
|
2006
|
-
);
|
|
2007
|
-
for (const node of filtered) {
|
|
2008
|
-
if (!Array.isArray(node.childIds) || typeof node.nodeId !== "string") {
|
|
2009
|
-
continue;
|
|
2010
|
-
}
|
|
2011
|
-
const nextChildIds = [];
|
|
2012
|
-
for (const childId of node.childIds) {
|
|
2013
|
-
nextChildIds.push(
|
|
2014
|
-
...this.collectKeptDescendants(childId, nodeById, keepIds)
|
|
2015
|
-
);
|
|
2016
|
-
}
|
|
2017
|
-
node.childIds = Array.from(new Set(nextChildIds));
|
|
2018
|
-
}
|
|
2019
|
-
return this.replaceAxNodes(snapshot, filtered);
|
|
2020
|
-
}
|
|
2021
|
-
collectKeptDescendants(nodeId, nodeById, keepIds, visited = /* @__PURE__ */ new Set()) {
|
|
2022
|
-
if (visited.has(nodeId)) {
|
|
2023
|
-
return [];
|
|
2024
|
-
}
|
|
2025
|
-
visited.add(nodeId);
|
|
2026
|
-
if (keepIds.has(nodeId)) {
|
|
2027
|
-
return [nodeId];
|
|
2028
|
-
}
|
|
2029
|
-
const node = nodeById.get(nodeId);
|
|
2030
|
-
if (!node || !Array.isArray(node.childIds)) {
|
|
2031
|
-
return [];
|
|
2032
|
-
}
|
|
2033
|
-
const output = [];
|
|
2034
|
-
for (const childId of node.childIds) {
|
|
2035
|
-
output.push(
|
|
2036
|
-
...this.collectKeptDescendants(childId, nodeById, keepIds, visited)
|
|
2037
|
-
);
|
|
2038
|
-
}
|
|
2039
|
-
return output;
|
|
2040
|
-
}
|
|
2041
|
-
shouldKeepCompactNode(node) {
|
|
2042
|
-
if (node.ignored) {
|
|
2043
|
-
return false;
|
|
2044
|
-
}
|
|
2045
|
-
const role = this.getAxRole(node);
|
|
2046
|
-
if (role && INTERACTIVE_AX_ROLES.has(role)) {
|
|
2047
|
-
return true;
|
|
2048
|
-
}
|
|
2049
|
-
const name = this.getAxName(node);
|
|
2050
|
-
const hasName = name.trim().length > 0;
|
|
2051
|
-
const hasValue = this.hasAxValue(node);
|
|
2052
|
-
if (hasName || hasValue) {
|
|
2053
|
-
return true;
|
|
2054
|
-
}
|
|
2055
|
-
const hasChildren = Array.isArray(node.childIds) && node.childIds.length > 0;
|
|
2056
|
-
if (!role || DECORATIVE_AX_ROLES.has(role)) {
|
|
2057
|
-
return false;
|
|
2058
|
-
}
|
|
2059
|
-
return hasChildren;
|
|
2060
|
-
}
|
|
2061
|
-
getAxRole(node) {
|
|
2062
|
-
const role = typeof node.role === "string" ? node.role : node.role?.value ?? "";
|
|
2063
|
-
return typeof role === "string" ? role.toLowerCase() : "";
|
|
2064
|
-
}
|
|
2065
|
-
getAxName(node) {
|
|
2066
|
-
const name = typeof node.name === "string" ? node.name : node.name?.value ?? "";
|
|
2067
|
-
return typeof name === "string" ? name : "";
|
|
2068
|
-
}
|
|
2069
|
-
hasAxValue(node) {
|
|
2070
|
-
if (!Array.isArray(node.properties)) {
|
|
2071
|
-
return false;
|
|
2279
|
+
const warnings = [...snapshot.warnings ?? []];
|
|
2280
|
+
if (snapshot.format !== "ax") {
|
|
2281
|
+
warnings.push("AX snapshot unavailable; cannot resolve refs.");
|
|
2282
|
+
return {
|
|
2283
|
+
matches: [],
|
|
2284
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
2285
|
+
};
|
|
2072
2286
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2287
|
+
const nodes = getAxNodes(snapshot.snapshot);
|
|
2288
|
+
const matches = [];
|
|
2289
|
+
const nameQuery = typeof input.name === "string" ? normalizeQuery(input.name) : "";
|
|
2290
|
+
const textQuery = typeof input.text === "string" ? normalizeQuery(input.text) : "";
|
|
2291
|
+
const labelQuery = typeof input.label === "string" ? normalizeQuery(input.label) : "";
|
|
2292
|
+
const roleQuery = typeof input.role === "string" ? normalizeQuery(input.role) : "";
|
|
2293
|
+
for (const node of nodes) {
|
|
2294
|
+
if (!node || typeof node !== "object") {
|
|
2079
2295
|
continue;
|
|
2080
2296
|
}
|
|
2081
|
-
if (typeof
|
|
2297
|
+
if (typeof node.ref !== "string" || node.ref.length === 0) {
|
|
2082
2298
|
continue;
|
|
2083
2299
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
return value.toLowerCase().includes(query);
|
|
2096
|
-
}
|
|
2097
|
-
matchesAxText(node, query) {
|
|
2098
|
-
if (!query) {
|
|
2099
|
-
return false;
|
|
2100
|
-
}
|
|
2101
|
-
const candidates = [this.getAxName(node)];
|
|
2102
|
-
if (Array.isArray(node.properties)) {
|
|
2103
|
-
for (const prop of node.properties) {
|
|
2104
|
-
if (!prop || typeof prop !== "object") {
|
|
2300
|
+
const role = getAxRole(node);
|
|
2301
|
+
const name = getAxName(node);
|
|
2302
|
+
if (input.kind === "role") {
|
|
2303
|
+
if (!role || role !== roleQuery) {
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
if (nameQuery && !matchesTextValue(name, nameQuery)) {
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
} else if (input.kind === "text") {
|
|
2310
|
+
if (!textQuery || !matchesAxText(node, textQuery)) {
|
|
2105
2311
|
continue;
|
|
2106
2312
|
}
|
|
2107
|
-
|
|
2108
|
-
if (
|
|
2313
|
+
} else if (input.kind === "label") {
|
|
2314
|
+
if (!labelQuery || !LABEL_AX_ROLES.has(role)) {
|
|
2109
2315
|
continue;
|
|
2110
2316
|
}
|
|
2111
|
-
if (
|
|
2112
|
-
|
|
2113
|
-
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
2114
|
-
candidates.push(String(value));
|
|
2317
|
+
if (!matchesTextValue(name, labelQuery)) {
|
|
2318
|
+
continue;
|
|
2115
2319
|
}
|
|
2116
2320
|
}
|
|
2321
|
+
matches.push({
|
|
2322
|
+
ref: node.ref,
|
|
2323
|
+
...role ? { role } : {},
|
|
2324
|
+
...name ? { name } : {}
|
|
2325
|
+
});
|
|
2117
2326
|
}
|
|
2118
|
-
return
|
|
2327
|
+
return {
|
|
2328
|
+
matches,
|
|
2329
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
2330
|
+
};
|
|
2119
2331
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2332
|
+
async consoleList(input) {
|
|
2333
|
+
this.requireSession(input.sessionId);
|
|
2334
|
+
const selection = await this.resolveTab(input.targetHint);
|
|
2335
|
+
await this.enableConsole(selection.tabId);
|
|
2336
|
+
const events = this.ensureDebugger().getConsoleEvents(selection.tabId);
|
|
2337
|
+
const entries = events.map((event) => toConsoleEntry(event)).filter((entry) => entry !== null);
|
|
2338
|
+
const result = {
|
|
2339
|
+
entries,
|
|
2340
|
+
warnings: selection.warnings
|
|
2341
|
+
};
|
|
2342
|
+
this.markInspectConnected(input.sessionId);
|
|
2343
|
+
return result;
|
|
2344
|
+
}
|
|
2345
|
+
async networkHar(input) {
|
|
2346
|
+
this.requireSession(input.sessionId);
|
|
2347
|
+
const selection = await this.resolveTab(input.targetHint);
|
|
2348
|
+
await this.enableNetwork(selection.tabId);
|
|
2349
|
+
const events = this.ensureDebugger().getNetworkEvents(selection.tabId);
|
|
2350
|
+
const har = buildHar(events, selection.tab.title);
|
|
2351
|
+
try {
|
|
2352
|
+
const rootDir = await ensureArtifactRootDir(input.sessionId);
|
|
2353
|
+
const artifactId = (0, import_crypto3.randomUUID)();
|
|
2354
|
+
const filePath = import_node_path2.default.join(rootDir, `har-${artifactId}.json`);
|
|
2355
|
+
await (0, import_promises2.writeFile)(filePath, JSON.stringify(har, null, 2), "utf-8");
|
|
2356
|
+
const result = {
|
|
2357
|
+
artifact_id: artifactId,
|
|
2358
|
+
path: filePath,
|
|
2359
|
+
mime: "application/json"
|
|
2360
|
+
};
|
|
2361
|
+
this.markInspectConnected(input.sessionId);
|
|
2362
|
+
return result;
|
|
2363
|
+
} catch {
|
|
2364
|
+
const error = new InspectError(
|
|
2365
|
+
"ARTIFACT_IO_ERROR",
|
|
2366
|
+
"Failed to write HAR file."
|
|
2367
|
+
);
|
|
2368
|
+
this.recordError(error);
|
|
2369
|
+
throw error;
|
|
2142
2370
|
}
|
|
2143
|
-
return entries;
|
|
2144
2371
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const
|
|
2148
|
-
|
|
2149
|
-
|
|
2372
|
+
async evaluate(input) {
|
|
2373
|
+
this.requireSession(input.sessionId);
|
|
2374
|
+
const selection = await this.resolveTab(input.targetHint);
|
|
2375
|
+
const expression = input.expression ?? "undefined";
|
|
2376
|
+
await this.debuggerCommand(selection.tabId, "Runtime.enable", {});
|
|
2377
|
+
const result = await this.debuggerCommand(
|
|
2378
|
+
selection.tabId,
|
|
2379
|
+
"Runtime.evaluate",
|
|
2380
|
+
{
|
|
2381
|
+
expression,
|
|
2382
|
+
returnByValue: true,
|
|
2383
|
+
awaitPromise: true
|
|
2384
|
+
}
|
|
2385
|
+
);
|
|
2386
|
+
if (result && typeof result === "object" && "exceptionDetails" in result) {
|
|
2387
|
+
const output2 = {
|
|
2388
|
+
exception: result.exceptionDetails,
|
|
2389
|
+
warnings: selection.warnings
|
|
2390
|
+
};
|
|
2391
|
+
this.markInspectConnected(input.sessionId);
|
|
2392
|
+
return output2;
|
|
2150
2393
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2394
|
+
const output = {
|
|
2395
|
+
value: result?.result?.value,
|
|
2396
|
+
warnings: selection.warnings
|
|
2397
|
+
};
|
|
2398
|
+
this.markInspectConnected(input.sessionId);
|
|
2399
|
+
return output;
|
|
2400
|
+
}
|
|
2401
|
+
async extractContent(input) {
|
|
2402
|
+
this.requireSession(input.sessionId);
|
|
2403
|
+
const selection = await this.resolveTab(input.targetHint);
|
|
2404
|
+
const debuggerCommand = this.debuggerCommand.bind(this);
|
|
2405
|
+
const html = await captureHtml(selection.tabId, {
|
|
2406
|
+
debuggerCommand,
|
|
2407
|
+
onEvaluationFailed: () => {
|
|
2408
|
+
const error = new InspectError(
|
|
2409
|
+
"EVALUATION_FAILED",
|
|
2410
|
+
"Failed to evaluate HTML snapshot.",
|
|
2411
|
+
{ retryable: false }
|
|
2412
|
+
);
|
|
2413
|
+
this.recordError(error);
|
|
2414
|
+
throw error;
|
|
2154
2415
|
}
|
|
2155
|
-
const record = node;
|
|
2156
|
-
const role = typeof record.role === "string" ? record.role : record.role?.value ?? "node";
|
|
2157
|
-
const name = typeof record.name === "string" ? record.name : record.name?.value ?? "";
|
|
2158
|
-
const nodeId = record.nodeId ?? (record.backendDOMNodeId !== void 0 ? String(record.backendDOMNodeId) : void 0);
|
|
2159
|
-
const key = nodeId ? `node-${nodeId}` : `${role}:${name}:${index}`;
|
|
2160
|
-
entries.set(key, `${role}:${name}`);
|
|
2161
2416
|
});
|
|
2162
|
-
|
|
2417
|
+
const url = selection.tab.url ?? "about:blank";
|
|
2418
|
+
let article = null;
|
|
2419
|
+
try {
|
|
2420
|
+
const dom = new import_jsdom.JSDOM(html, { url });
|
|
2421
|
+
const reader = new import_readability.Readability(dom.window.document);
|
|
2422
|
+
article = reader.parse();
|
|
2423
|
+
} catch {
|
|
2424
|
+
const err = new InspectError(
|
|
2425
|
+
"EVALUATION_FAILED",
|
|
2426
|
+
"Failed to parse page content.",
|
|
2427
|
+
{ retryable: false }
|
|
2428
|
+
);
|
|
2429
|
+
this.recordError(err);
|
|
2430
|
+
throw err;
|
|
2431
|
+
}
|
|
2432
|
+
if (!article) {
|
|
2433
|
+
const err = new InspectError(
|
|
2434
|
+
"NOT_SUPPORTED",
|
|
2435
|
+
"Readability could not extract content.",
|
|
2436
|
+
{ retryable: false }
|
|
2437
|
+
);
|
|
2438
|
+
this.recordError(err);
|
|
2439
|
+
throw err;
|
|
2440
|
+
}
|
|
2441
|
+
let content = "";
|
|
2442
|
+
if (input.format === "article_json") {
|
|
2443
|
+
content = JSON.stringify(article, null, 2);
|
|
2444
|
+
} else if (input.format === "text") {
|
|
2445
|
+
content = article.textContent ?? "";
|
|
2446
|
+
} else {
|
|
2447
|
+
const turndown = new import_turndown.default();
|
|
2448
|
+
content = turndown.turndown(article.content ?? "");
|
|
2449
|
+
}
|
|
2450
|
+
const warnings = selection.warnings ?? [];
|
|
2451
|
+
const includeMetadata = input.includeMetadata ?? true;
|
|
2452
|
+
const output = {
|
|
2453
|
+
content,
|
|
2454
|
+
...includeMetadata ? {
|
|
2455
|
+
title: article.title ?? void 0,
|
|
2456
|
+
byline: article.byline ?? void 0,
|
|
2457
|
+
excerpt: article.excerpt ?? void 0,
|
|
2458
|
+
siteName: article.siteName ?? void 0
|
|
2459
|
+
} : {},
|
|
2460
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
2461
|
+
};
|
|
2462
|
+
this.markInspectConnected(input.sessionId);
|
|
2463
|
+
return output;
|
|
2163
2464
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
const
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2465
|
+
async pageState(input) {
|
|
2466
|
+
this.requireSession(input.sessionId);
|
|
2467
|
+
const selection = await this.resolveTab(input.targetHint);
|
|
2468
|
+
await this.debuggerCommand(selection.tabId, "Runtime.enable", {});
|
|
2469
|
+
const expression = PAGE_STATE_SCRIPT;
|
|
2470
|
+
const result = await this.debuggerCommand(
|
|
2471
|
+
selection.tabId,
|
|
2472
|
+
"Runtime.evaluate",
|
|
2473
|
+
{
|
|
2474
|
+
expression,
|
|
2475
|
+
returnByValue: true,
|
|
2476
|
+
awaitPromise: true
|
|
2174
2477
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2478
|
+
);
|
|
2479
|
+
if (result && typeof result === "object" && "exceptionDetails" in result) {
|
|
2480
|
+
const error = new InspectError(
|
|
2481
|
+
"EVALUATION_FAILED",
|
|
2482
|
+
"Failed to capture page state.",
|
|
2483
|
+
{ retryable: false }
|
|
2484
|
+
);
|
|
2485
|
+
this.recordError(error);
|
|
2486
|
+
throw error;
|
|
2487
|
+
}
|
|
2488
|
+
const value = result?.result?.value;
|
|
2489
|
+
const raw = value && typeof value === "object" ? value : {};
|
|
2490
|
+
const warnings = [
|
|
2491
|
+
...Array.isArray(raw.warnings) ? raw.warnings : [],
|
|
2492
|
+
...selection.warnings ?? []
|
|
2493
|
+
];
|
|
2494
|
+
const output = {
|
|
2495
|
+
forms: Array.isArray(raw.forms) ? raw.forms : [],
|
|
2496
|
+
localStorage: Array.isArray(raw.localStorage) ? raw.localStorage : [],
|
|
2497
|
+
sessionStorage: Array.isArray(raw.sessionStorage) ? raw.sessionStorage : [],
|
|
2498
|
+
cookies: Array.isArray(raw.cookies) ? raw.cookies : [],
|
|
2499
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
2500
|
+
};
|
|
2501
|
+
this.markInspectConnected(input.sessionId);
|
|
2502
|
+
return output;
|
|
2503
|
+
}
|
|
2504
|
+
async performanceMetrics(input) {
|
|
2505
|
+
this.requireSession(input.sessionId);
|
|
2506
|
+
const selection = await this.resolveTab(input.targetHint);
|
|
2507
|
+
await this.debuggerCommand(selection.tabId, "Performance.enable", {});
|
|
2508
|
+
const result = await this.debuggerCommand(
|
|
2509
|
+
selection.tabId,
|
|
2510
|
+
"Performance.getMetrics",
|
|
2511
|
+
{}
|
|
2512
|
+
);
|
|
2513
|
+
const metrics = Array.isArray(result?.metrics) ? result.metrics.map((metric) => ({
|
|
2514
|
+
name: metric.name,
|
|
2515
|
+
value: metric.value
|
|
2516
|
+
})) : [];
|
|
2517
|
+
const output = { metrics, warnings: selection.warnings };
|
|
2518
|
+
this.markInspectConnected(input.sessionId);
|
|
2519
|
+
return output;
|
|
2520
|
+
}
|
|
2521
|
+
async screenshot(input) {
|
|
2522
|
+
this.requireSession(input.sessionId);
|
|
2523
|
+
const selection = await this.resolveTab(input.targetHint);
|
|
2524
|
+
await this.debuggerCommand(selection.tabId, "Page.enable", {});
|
|
2525
|
+
const format = input.format ?? "png";
|
|
2526
|
+
let captureParams = {
|
|
2527
|
+
format,
|
|
2528
|
+
fromSurface: true
|
|
2529
|
+
};
|
|
2530
|
+
if (format !== "png" && typeof input.quality === "number") {
|
|
2531
|
+
captureParams = { ...captureParams, quality: input.quality };
|
|
2532
|
+
}
|
|
2533
|
+
if (input.target === "full") {
|
|
2534
|
+
const layout = await this.debuggerCommand(
|
|
2535
|
+
selection.tabId,
|
|
2536
|
+
"Page.getLayoutMetrics",
|
|
2537
|
+
{}
|
|
2538
|
+
);
|
|
2539
|
+
const contentSize = layout?.contentSize;
|
|
2540
|
+
if (contentSize) {
|
|
2541
|
+
captureParams = {
|
|
2542
|
+
...captureParams,
|
|
2543
|
+
clip: {
|
|
2544
|
+
x: 0,
|
|
2545
|
+
y: 0,
|
|
2546
|
+
width: contentSize.width,
|
|
2547
|
+
height: contentSize.height,
|
|
2548
|
+
scale: 1
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
} else {
|
|
2552
|
+
captureParams = { ...captureParams, captureBeyondViewport: true };
|
|
2178
2553
|
}
|
|
2179
|
-
const ref = `@e${index}`;
|
|
2180
|
-
index += 1;
|
|
2181
|
-
node.ref = ref;
|
|
2182
|
-
refs.set(backendId, ref);
|
|
2183
2554
|
}
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2555
|
+
const result = await this.debuggerCommand(
|
|
2556
|
+
selection.tabId,
|
|
2557
|
+
"Page.captureScreenshot",
|
|
2558
|
+
captureParams
|
|
2559
|
+
);
|
|
2560
|
+
const data = result.data;
|
|
2561
|
+
if (!data) {
|
|
2562
|
+
const error = new InspectError(
|
|
2563
|
+
"INSPECT_UNAVAILABLE",
|
|
2564
|
+
"Failed to capture screenshot.",
|
|
2565
|
+
{ retryable: false }
|
|
2566
|
+
);
|
|
2567
|
+
this.recordError(error);
|
|
2568
|
+
throw error;
|
|
2569
|
+
}
|
|
2190
2570
|
try {
|
|
2191
|
-
await
|
|
2571
|
+
const rootDir = await ensureArtifactRootDir(input.sessionId);
|
|
2572
|
+
const artifactId = (0, import_crypto3.randomUUID)();
|
|
2573
|
+
const extension = format === "jpeg" ? "jpg" : format;
|
|
2574
|
+
const filePath = import_node_path2.default.join(
|
|
2575
|
+
rootDir,
|
|
2576
|
+
`screenshot-${artifactId}.${extension}`
|
|
2577
|
+
);
|
|
2578
|
+
await (0, import_promises2.writeFile)(filePath, Buffer.from(data, "base64"));
|
|
2579
|
+
const mime = format === "jpeg" ? "image/jpeg" : `image/${format}`;
|
|
2580
|
+
const output = {
|
|
2581
|
+
artifact_id: artifactId,
|
|
2582
|
+
path: filePath,
|
|
2583
|
+
mime
|
|
2584
|
+
};
|
|
2585
|
+
this.markInspectConnected(input.sessionId);
|
|
2586
|
+
return output;
|
|
2192
2587
|
} catch {
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
for (const [backendNodeId, ref] of refs) {
|
|
2200
|
-
if (applied >= MAX_REF_ASSIGNMENTS) {
|
|
2201
|
-
warnings.push(
|
|
2202
|
-
`Snapshot refs truncated at ${MAX_REF_ASSIGNMENTS} elements.`
|
|
2203
|
-
);
|
|
2204
|
-
break;
|
|
2205
|
-
}
|
|
2206
|
-
try {
|
|
2207
|
-
const described = await this.debuggerCommand(
|
|
2208
|
-
tabId,
|
|
2209
|
-
"DOM.describeNode",
|
|
2210
|
-
{
|
|
2211
|
-
backendNodeId
|
|
2212
|
-
}
|
|
2213
|
-
);
|
|
2214
|
-
const node = described.node;
|
|
2215
|
-
if (!node || node.nodeType !== 1 || typeof node.nodeId !== "number") {
|
|
2216
|
-
if (warnings.length < MAX_REF_WARNINGS) {
|
|
2217
|
-
warnings.push(`Ref ${ref} could not be applied to a DOM element.`);
|
|
2218
|
-
}
|
|
2219
|
-
continue;
|
|
2220
|
-
}
|
|
2221
|
-
await this.debuggerCommand(tabId, "DOM.setAttributeValue", {
|
|
2222
|
-
nodeId: node.nodeId,
|
|
2223
|
-
name: SNAPSHOT_REF_ATTRIBUTE,
|
|
2224
|
-
value: ref
|
|
2225
|
-
});
|
|
2226
|
-
applied += 1;
|
|
2227
|
-
} catch {
|
|
2228
|
-
if (warnings.length < MAX_REF_WARNINGS) {
|
|
2229
|
-
warnings.push(`Ref ${ref} could not be applied.`);
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2588
|
+
const error = new InspectError(
|
|
2589
|
+
"ARTIFACT_IO_ERROR",
|
|
2590
|
+
"Failed to write screenshot file."
|
|
2591
|
+
);
|
|
2592
|
+
this.recordError(error);
|
|
2593
|
+
throw error;
|
|
2232
2594
|
}
|
|
2233
|
-
return warnings;
|
|
2234
2595
|
}
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
}
|
|
2596
|
+
ensureDebugger() {
|
|
2597
|
+
if (!this.debugger) {
|
|
2598
|
+
const error = this.buildUnavailableError();
|
|
2599
|
+
this.recordError(error);
|
|
2600
|
+
throw error;
|
|
2601
|
+
}
|
|
2602
|
+
return this.debugger;
|
|
2241
2603
|
}
|
|
2242
|
-
async
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2604
|
+
async resolveTab(hint) {
|
|
2605
|
+
if (!this.extensionBridge || !this.extensionBridge.isConnected()) {
|
|
2606
|
+
const error = new InspectError(
|
|
2607
|
+
"EXTENSION_DISCONNECTED",
|
|
2608
|
+
"Extension is not connected.",
|
|
2609
|
+
{ retryable: true }
|
|
2610
|
+
);
|
|
2611
|
+
this.recordError(error);
|
|
2612
|
+
throw error;
|
|
2250
2613
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
}
|
|
2260
|
-
return { nodeId };
|
|
2261
|
-
} catch (error) {
|
|
2262
|
-
if (error instanceof InspectError) {
|
|
2263
|
-
return { warnings: [error.message] };
|
|
2264
|
-
}
|
|
2265
|
-
return { warnings: ["Selector query failed."] };
|
|
2614
|
+
const tabs = this.extensionBridge.getStatus().tabs ?? [];
|
|
2615
|
+
if (!Array.isArray(tabs) || tabs.length === 0) {
|
|
2616
|
+
const error = new InspectError(
|
|
2617
|
+
"TAB_NOT_FOUND",
|
|
2618
|
+
"No tabs available to inspect."
|
|
2619
|
+
);
|
|
2620
|
+
this.recordError(error);
|
|
2621
|
+
throw error;
|
|
2266
2622
|
}
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
const
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2623
|
+
const candidates = tabs.map((tab2) => ({
|
|
2624
|
+
id: String(tab2.tab_id),
|
|
2625
|
+
url: tab2.url ?? "",
|
|
2626
|
+
title: tab2.title,
|
|
2627
|
+
lastSeenAt: tab2.last_active_at ? Date.parse(tab2.last_active_at) : void 0
|
|
2628
|
+
}));
|
|
2629
|
+
const ranked = pickBestTarget(candidates, hint);
|
|
2630
|
+
if (!ranked) {
|
|
2631
|
+
const error = new InspectError("TAB_NOT_FOUND", "No matching tab found.");
|
|
2632
|
+
this.recordError(error);
|
|
2633
|
+
throw error;
|
|
2634
|
+
}
|
|
2635
|
+
const tabId = Number(ranked.candidate.id);
|
|
2636
|
+
if (!Number.isFinite(tabId)) {
|
|
2279
2637
|
const error = new InspectError(
|
|
2280
|
-
"
|
|
2281
|
-
"
|
|
2282
|
-
{ retryable: false }
|
|
2638
|
+
"TAB_NOT_FOUND",
|
|
2639
|
+
"Resolved tab id is invalid."
|
|
2283
2640
|
);
|
|
2284
2641
|
this.recordError(error);
|
|
2285
2642
|
throw error;
|
|
2286
2643
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
)
|
|
2644
|
+
const tab = tabs.find((entry) => entry.tab_id === tabId) ?? tabs[0];
|
|
2645
|
+
const warnings = [];
|
|
2646
|
+
if (!hint) {
|
|
2647
|
+
warnings.push("No target hint provided; using the most recent tab.");
|
|
2648
|
+
} else if (ranked.score < 20) {
|
|
2649
|
+
warnings.push("Weak target match; using best available tab.");
|
|
2650
|
+
}
|
|
2651
|
+
return {
|
|
2652
|
+
tabId,
|
|
2653
|
+
tab,
|
|
2654
|
+
warnings: warnings.length > 0 ? warnings : void 0
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
async enableConsole(tabId) {
|
|
2658
|
+
await this.debuggerCommand(tabId, "Runtime.enable", {});
|
|
2659
|
+
await this.debuggerCommand(tabId, "Log.enable", {});
|
|
2660
|
+
}
|
|
2661
|
+
async enableNetwork(tabId) {
|
|
2662
|
+
await this.debuggerCommand(tabId, "Network.enable", {});
|
|
2663
|
+
}
|
|
2664
|
+
async enableAccessibility(tabId) {
|
|
2665
|
+
await this.debuggerCommand(tabId, "Accessibility.enable", {});
|
|
2290
2666
|
}
|
|
2291
2667
|
async debuggerCommand(tabId, method, params, timeoutMs) {
|
|
2292
2668
|
const debuggerBridge = this.ensureDebugger();
|
|
@@ -2323,319 +2699,6 @@ var InspectService = class {
|
|
|
2323
2699
|
details: error.details
|
|
2324
2700
|
});
|
|
2325
2701
|
}
|
|
2326
|
-
toConsoleEntry(event) {
|
|
2327
|
-
const params = event.params ?? {};
|
|
2328
|
-
switch (event.method) {
|
|
2329
|
-
case "Runtime.consoleAPICalled": {
|
|
2330
|
-
const rawArgs = Array.isArray(params.args) ? params.args : [];
|
|
2331
|
-
const text = rawArgs.map((arg) => this.stringifyRemoteObject(arg)).join(" ");
|
|
2332
|
-
const level = String(params.type ?? "log");
|
|
2333
|
-
const stack = this.toStackFrames(
|
|
2334
|
-
params.stackTrace
|
|
2335
|
-
);
|
|
2336
|
-
const args = rawArgs.map((arg) => this.toRemoteObjectSummary(arg)).filter(
|
|
2337
|
-
(entry) => Boolean(entry)
|
|
2338
|
-
);
|
|
2339
|
-
return {
|
|
2340
|
-
level,
|
|
2341
|
-
text,
|
|
2342
|
-
timestamp: event.timestamp,
|
|
2343
|
-
...stack && stack.length > 0 ? { stack } : {},
|
|
2344
|
-
...args.length > 0 ? { args } : {}
|
|
2345
|
-
};
|
|
2346
|
-
}
|
|
2347
|
-
case "Runtime.exceptionThrown": {
|
|
2348
|
-
const details = params.exceptionDetails;
|
|
2349
|
-
const exception = this.toRemoteObjectSummary(details?.exception);
|
|
2350
|
-
const stack = this.toStackFrames(details?.stackTrace);
|
|
2351
|
-
const source = this.toSourceLocation({
|
|
2352
|
-
url: details?.url,
|
|
2353
|
-
lineNumber: details?.lineNumber,
|
|
2354
|
-
columnNumber: details?.columnNumber
|
|
2355
|
-
}) ?? // If the top frame exists, treat it as the source.
|
|
2356
|
-
(stack && stack.length > 0 ? {
|
|
2357
|
-
url: stack[0].url,
|
|
2358
|
-
line: stack[0].line,
|
|
2359
|
-
column: stack[0].column
|
|
2360
|
-
} : void 0);
|
|
2361
|
-
const baseText = typeof details?.text === "string" && details.text.trim().length > 0 ? details.text : "Uncaught exception";
|
|
2362
|
-
const exceptionDesc = typeof exception?.description === "string" && exception.description.trim().length > 0 ? exception.description : void 0;
|
|
2363
|
-
const text = baseText === "Uncaught" && exceptionDesc ? `Uncaught: ${exceptionDesc}` : baseText;
|
|
2364
|
-
return {
|
|
2365
|
-
level: "error",
|
|
2366
|
-
text,
|
|
2367
|
-
timestamp: event.timestamp,
|
|
2368
|
-
...source ? { source } : {},
|
|
2369
|
-
...stack && stack.length > 0 ? { stack } : {},
|
|
2370
|
-
...exception ? { exception } : {}
|
|
2371
|
-
};
|
|
2372
|
-
}
|
|
2373
|
-
case "Log.entryAdded": {
|
|
2374
|
-
const entry = params.entry;
|
|
2375
|
-
if (!entry) {
|
|
2376
|
-
return null;
|
|
2377
|
-
}
|
|
2378
|
-
const stack = this.toStackFrames(entry.stackTrace);
|
|
2379
|
-
const source = this.toSourceLocation({
|
|
2380
|
-
url: entry.url,
|
|
2381
|
-
lineNumber: entry.lineNumber,
|
|
2382
|
-
columnNumber: void 0
|
|
2383
|
-
});
|
|
2384
|
-
return {
|
|
2385
|
-
level: entry.level ?? "log",
|
|
2386
|
-
text: entry.text ?? "",
|
|
2387
|
-
timestamp: event.timestamp,
|
|
2388
|
-
...source ? { source } : {},
|
|
2389
|
-
...stack && stack.length > 0 ? { stack } : {}
|
|
2390
|
-
};
|
|
2391
|
-
}
|
|
2392
|
-
default:
|
|
2393
|
-
return null;
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
toSourceLocation(input) {
|
|
2397
|
-
const url = typeof input.url === "string" && input.url.length > 0 ? input.url : void 0;
|
|
2398
|
-
const line = typeof input.lineNumber === "number" && Number.isFinite(input.lineNumber) ? Math.max(1, Math.floor(input.lineNumber) + 1) : void 0;
|
|
2399
|
-
const column = typeof input.columnNumber === "number" && Number.isFinite(input.columnNumber) ? Math.max(1, Math.floor(input.columnNumber) + 1) : void 0;
|
|
2400
|
-
if (!url && !line && !column) {
|
|
2401
|
-
return void 0;
|
|
2402
|
-
}
|
|
2403
|
-
return {
|
|
2404
|
-
...url ? { url } : {},
|
|
2405
|
-
...line ? { line } : {},
|
|
2406
|
-
...column ? { column } : {}
|
|
2407
|
-
};
|
|
2408
|
-
}
|
|
2409
|
-
toStackFrames(stackTrace) {
|
|
2410
|
-
const frames = [];
|
|
2411
|
-
const collect = (trace) => {
|
|
2412
|
-
if (!trace || typeof trace !== "object") {
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2415
|
-
const callFrames = trace.callFrames;
|
|
2416
|
-
if (Array.isArray(callFrames)) {
|
|
2417
|
-
for (const frame of callFrames) {
|
|
2418
|
-
if (!frame || typeof frame !== "object") {
|
|
2419
|
-
continue;
|
|
2420
|
-
}
|
|
2421
|
-
const functionName = typeof frame.functionName === "string" ? String(frame.functionName) : void 0;
|
|
2422
|
-
const url = typeof frame.url === "string" ? String(frame.url) : void 0;
|
|
2423
|
-
const lineNumber = frame.lineNumber;
|
|
2424
|
-
const columnNumber = frame.columnNumber;
|
|
2425
|
-
const loc = this.toSourceLocation({ url, lineNumber, columnNumber });
|
|
2426
|
-
frames.push({
|
|
2427
|
-
...functionName ? { functionName } : {},
|
|
2428
|
-
...loc?.url ? { url: loc.url } : {},
|
|
2429
|
-
...loc?.line ? { line: loc.line } : {},
|
|
2430
|
-
...loc?.column ? { column: loc.column } : {}
|
|
2431
|
-
});
|
|
2432
|
-
if (frames.length >= 50) {
|
|
2433
|
-
return;
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
const parent = trace.parent;
|
|
2438
|
-
if (frames.length < 50 && parent) {
|
|
2439
|
-
collect(parent);
|
|
2440
|
-
}
|
|
2441
|
-
};
|
|
2442
|
-
collect(stackTrace);
|
|
2443
|
-
return frames.length > 0 ? frames : void 0;
|
|
2444
|
-
}
|
|
2445
|
-
toRemoteObjectSummary(obj) {
|
|
2446
|
-
if (!obj || typeof obj !== "object") {
|
|
2447
|
-
return void 0;
|
|
2448
|
-
}
|
|
2449
|
-
const raw = obj;
|
|
2450
|
-
const type = typeof raw.type === "string" ? raw.type : void 0;
|
|
2451
|
-
const subtype = typeof raw.subtype === "string" ? raw.subtype : void 0;
|
|
2452
|
-
const description = typeof raw.description === "string" ? raw.description : void 0;
|
|
2453
|
-
const unserializableValue = typeof raw.unserializableValue === "string" ? raw.unserializableValue : void 0;
|
|
2454
|
-
const out = {};
|
|
2455
|
-
if (type) out.type = type;
|
|
2456
|
-
if (subtype) out.subtype = subtype;
|
|
2457
|
-
if (description) out.description = description;
|
|
2458
|
-
if (raw.value !== void 0) out.value = raw.value;
|
|
2459
|
-
if (unserializableValue) out.unserializableValue = unserializableValue;
|
|
2460
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
2461
|
-
}
|
|
2462
|
-
stringifyRemoteObject(value) {
|
|
2463
|
-
if (!value || typeof value !== "object") {
|
|
2464
|
-
return String(value ?? "");
|
|
2465
|
-
}
|
|
2466
|
-
const obj = value;
|
|
2467
|
-
if (obj.unserializableValue) {
|
|
2468
|
-
return obj.unserializableValue;
|
|
2469
|
-
}
|
|
2470
|
-
if (obj.value !== void 0) {
|
|
2471
|
-
try {
|
|
2472
|
-
return typeof obj.value === "string" ? obj.value : JSON.stringify(obj.value);
|
|
2473
|
-
} catch {
|
|
2474
|
-
return String(obj.value);
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
if (obj.description) {
|
|
2478
|
-
return obj.description;
|
|
2479
|
-
}
|
|
2480
|
-
return obj.type ?? "";
|
|
2481
|
-
}
|
|
2482
|
-
buildHar(events, title) {
|
|
2483
|
-
const requests = /* @__PURE__ */ new Map();
|
|
2484
|
-
const toTimestamp = (event, fallback) => {
|
|
2485
|
-
const raw = event.params?.wallTime;
|
|
2486
|
-
if (typeof raw === "number") {
|
|
2487
|
-
return raw * 1e3;
|
|
2488
|
-
}
|
|
2489
|
-
const ts = event.params?.timestamp;
|
|
2490
|
-
if (typeof ts === "number") {
|
|
2491
|
-
return ts * 1e3;
|
|
2492
|
-
}
|
|
2493
|
-
const parsed = Date.parse(event.timestamp);
|
|
2494
|
-
if (Number.isFinite(parsed)) {
|
|
2495
|
-
return parsed;
|
|
2496
|
-
}
|
|
2497
|
-
return fallback ?? Date.now();
|
|
2498
|
-
};
|
|
2499
|
-
for (const event of events) {
|
|
2500
|
-
const params = event.params ?? {};
|
|
2501
|
-
switch (event.method) {
|
|
2502
|
-
case "Network.requestWillBeSent": {
|
|
2503
|
-
const requestId = String(
|
|
2504
|
-
params.requestId
|
|
2505
|
-
);
|
|
2506
|
-
if (!requestId) {
|
|
2507
|
-
break;
|
|
2508
|
-
}
|
|
2509
|
-
const request = params.request;
|
|
2510
|
-
const record = {
|
|
2511
|
-
id: requestId,
|
|
2512
|
-
url: request?.url,
|
|
2513
|
-
method: request?.method,
|
|
2514
|
-
requestHeaders: request?.headers ?? {},
|
|
2515
|
-
startTime: toTimestamp(event)
|
|
2516
|
-
};
|
|
2517
|
-
requests.set(requestId, record);
|
|
2518
|
-
break;
|
|
2519
|
-
}
|
|
2520
|
-
case "Network.responseReceived": {
|
|
2521
|
-
const requestId = String(
|
|
2522
|
-
params.requestId
|
|
2523
|
-
);
|
|
2524
|
-
if (!requestId) {
|
|
2525
|
-
break;
|
|
2526
|
-
}
|
|
2527
|
-
const response = params.response;
|
|
2528
|
-
const record = requests.get(requestId) ?? { id: requestId };
|
|
2529
|
-
record.status = response?.status;
|
|
2530
|
-
record.statusText = response?.statusText;
|
|
2531
|
-
record.mimeType = response?.mimeType;
|
|
2532
|
-
record.responseHeaders = response?.headers ?? {};
|
|
2533
|
-
record.protocol = response?.protocol;
|
|
2534
|
-
record.startTime = record.startTime ?? toTimestamp(event);
|
|
2535
|
-
requests.set(requestId, record);
|
|
2536
|
-
break;
|
|
2537
|
-
}
|
|
2538
|
-
case "Network.loadingFinished": {
|
|
2539
|
-
const requestId = String(
|
|
2540
|
-
params.requestId
|
|
2541
|
-
);
|
|
2542
|
-
if (!requestId) {
|
|
2543
|
-
break;
|
|
2544
|
-
}
|
|
2545
|
-
const record = requests.get(requestId) ?? { id: requestId };
|
|
2546
|
-
record.encodedDataLength = params.encodedDataLength;
|
|
2547
|
-
record.endTime = toTimestamp(event, record.startTime);
|
|
2548
|
-
requests.set(requestId, record);
|
|
2549
|
-
break;
|
|
2550
|
-
}
|
|
2551
|
-
case "Network.loadingFailed": {
|
|
2552
|
-
const requestId = String(
|
|
2553
|
-
params.requestId
|
|
2554
|
-
);
|
|
2555
|
-
if (!requestId) {
|
|
2556
|
-
break;
|
|
2557
|
-
}
|
|
2558
|
-
const record = requests.get(requestId) ?? { id: requestId };
|
|
2559
|
-
record.endTime = toTimestamp(event, record.startTime);
|
|
2560
|
-
requests.set(requestId, record);
|
|
2561
|
-
break;
|
|
2562
|
-
}
|
|
2563
|
-
default:
|
|
2564
|
-
break;
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
const entries = Array.from(requests.values()).map((record) => {
|
|
2568
|
-
const started = record.startTime ?? Date.now();
|
|
2569
|
-
const ended = record.endTime ?? started;
|
|
2570
|
-
const time = Math.max(0, ended - started);
|
|
2571
|
-
const url = record.url ?? "";
|
|
2572
|
-
const queryString = [];
|
|
2573
|
-
try {
|
|
2574
|
-
const parsed = new URL(url);
|
|
2575
|
-
parsed.searchParams.forEach((value, name) => {
|
|
2576
|
-
queryString.push({ name, value });
|
|
2577
|
-
});
|
|
2578
|
-
} catch {
|
|
2579
|
-
}
|
|
2580
|
-
return {
|
|
2581
|
-
pageref: "page_0",
|
|
2582
|
-
startedDateTime: new Date(started).toISOString(),
|
|
2583
|
-
time,
|
|
2584
|
-
request: {
|
|
2585
|
-
method: record.method ?? "GET",
|
|
2586
|
-
url,
|
|
2587
|
-
httpVersion: record.protocol ?? "HTTP/1.1",
|
|
2588
|
-
cookies: [],
|
|
2589
|
-
headers: [],
|
|
2590
|
-
queryString,
|
|
2591
|
-
headersSize: -1,
|
|
2592
|
-
bodySize: -1
|
|
2593
|
-
},
|
|
2594
|
-
response: {
|
|
2595
|
-
status: record.status ?? 0,
|
|
2596
|
-
statusText: record.statusText ?? "",
|
|
2597
|
-
httpVersion: record.protocol ?? "HTTP/1.1",
|
|
2598
|
-
cookies: [],
|
|
2599
|
-
headers: [],
|
|
2600
|
-
redirectURL: "",
|
|
2601
|
-
headersSize: -1,
|
|
2602
|
-
bodySize: record.encodedDataLength ?? 0,
|
|
2603
|
-
content: {
|
|
2604
|
-
size: record.encodedDataLength ?? 0,
|
|
2605
|
-
mimeType: record.mimeType ?? ""
|
|
2606
|
-
}
|
|
2607
|
-
},
|
|
2608
|
-
cache: {},
|
|
2609
|
-
timings: {
|
|
2610
|
-
send: 0,
|
|
2611
|
-
wait: time,
|
|
2612
|
-
receive: 0
|
|
2613
|
-
}
|
|
2614
|
-
};
|
|
2615
|
-
});
|
|
2616
|
-
const startedDateTime = entries.length ? entries[0].startedDateTime : (/* @__PURE__ */ new Date()).toISOString();
|
|
2617
|
-
return {
|
|
2618
|
-
log: {
|
|
2619
|
-
version: "1.2",
|
|
2620
|
-
creator: {
|
|
2621
|
-
name: "browser-bridge",
|
|
2622
|
-
version: "0.0.0"
|
|
2623
|
-
},
|
|
2624
|
-
pages: [
|
|
2625
|
-
{
|
|
2626
|
-
id: "page_0",
|
|
2627
|
-
title: title ?? "page",
|
|
2628
|
-
startedDateTime,
|
|
2629
|
-
pageTimings: {
|
|
2630
|
-
onContentLoad: -1,
|
|
2631
|
-
onLoad: -1
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
],
|
|
2635
|
-
entries
|
|
2636
|
-
}
|
|
2637
|
-
};
|
|
2638
|
-
}
|
|
2639
2702
|
markInspectConnected(sessionId) {
|
|
2640
2703
|
try {
|
|
2641
2704
|
const session = this.registry.require(sessionId);
|
|
@@ -2658,11 +2721,6 @@ var InspectService = class {
|
|
|
2658
2721
|
{ retryable: false }
|
|
2659
2722
|
);
|
|
2660
2723
|
}
|
|
2661
|
-
throwUnavailable() {
|
|
2662
|
-
const error = this.buildUnavailableError();
|
|
2663
|
-
this.recordError(error);
|
|
2664
|
-
throw error;
|
|
2665
|
-
}
|
|
2666
2724
|
requireSession(sessionId) {
|
|
2667
2725
|
try {
|
|
2668
2726
|
return this.registry.require(sessionId);
|
|
@@ -2797,6 +2855,11 @@ var DiagnosticReportSchema = import_zod2.z.object({
|
|
|
2797
2855
|
ok: import_zod2.z.boolean(),
|
|
2798
2856
|
session_id: import_zod2.z.string().optional(),
|
|
2799
2857
|
checks: import_zod2.z.array(DiagnosticCheckSchema).optional(),
|
|
2858
|
+
sessions: import_zod2.z.object({
|
|
2859
|
+
count: import_zod2.z.number().finite().optional(),
|
|
2860
|
+
max_age_ms: import_zod2.z.number().finite().optional(),
|
|
2861
|
+
max_idle_ms: import_zod2.z.number().finite().optional()
|
|
2862
|
+
}).optional(),
|
|
2800
2863
|
extension: import_zod2.z.object({
|
|
2801
2864
|
connected: import_zod2.z.boolean().optional(),
|
|
2802
2865
|
version: import_zod2.z.string().optional(),
|
|
@@ -3362,6 +3425,11 @@ var buildDiagnosticReport = (sessionId, context = {}) => {
|
|
|
3362
3425
|
ok: checks.every((check) => check.ok),
|
|
3363
3426
|
session_id: sessionId,
|
|
3364
3427
|
checks,
|
|
3428
|
+
sessions: context.sessions ? {
|
|
3429
|
+
count: context.sessions.count,
|
|
3430
|
+
max_age_ms: context.sessions.maxAgeMs,
|
|
3431
|
+
max_idle_ms: context.sessions.maxIdleMs
|
|
3432
|
+
} : void 0,
|
|
3365
3433
|
extension: {
|
|
3366
3434
|
connected: extensionConnected,
|
|
3367
3435
|
last_seen_at: context.extension?.lastSeenAt
|
|
@@ -3426,6 +3494,26 @@ var registerDiagnosticsRoutes = (router, options = {}) => {
|
|
|
3426
3494
|
} catch {
|
|
3427
3495
|
}
|
|
3428
3496
|
}
|
|
3497
|
+
if (options.registry) {
|
|
3498
|
+
const now = Date.now();
|
|
3499
|
+
const sessions = options.registry.list();
|
|
3500
|
+
let maxAgeMs = 0;
|
|
3501
|
+
let maxIdleMs = 0;
|
|
3502
|
+
for (const session of sessions) {
|
|
3503
|
+
const ageMs = now - session.createdAt.getTime();
|
|
3504
|
+
const idleMs = now - session.lastAccessedAt.getTime();
|
|
3505
|
+
if (ageMs > maxAgeMs) {
|
|
3506
|
+
maxAgeMs = ageMs;
|
|
3507
|
+
}
|
|
3508
|
+
if (idleMs > maxIdleMs) {
|
|
3509
|
+
maxIdleMs = idleMs;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
context.sessions = {
|
|
3513
|
+
count: sessions.length,
|
|
3514
|
+
...sessions.length > 0 ? { maxAgeMs, maxIdleMs } : {}
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3429
3517
|
if (options.extensionBridge) {
|
|
3430
3518
|
const status = options.extensionBridge.getStatus();
|
|
3431
3519
|
context.extension = {
|
|
@@ -4250,6 +4338,29 @@ var resolveCorePort = (portOverride) => {
|
|
|
4250
4338
|
}
|
|
4251
4339
|
return 3210;
|
|
4252
4340
|
};
|
|
4341
|
+
var resolveSessionTtlMs = () => {
|
|
4342
|
+
const env = process.env.BROWSER_BRIDGE_SESSION_TTL_MS || process.env.BROWSER_VISION_SESSION_TTL_MS;
|
|
4343
|
+
if (env) {
|
|
4344
|
+
const parsed = Number(env);
|
|
4345
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
4346
|
+
return parsed;
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
return 60 * 60 * 1e3;
|
|
4350
|
+
};
|
|
4351
|
+
var resolveSessionCleanupIntervalMs = (ttlMs) => {
|
|
4352
|
+
const env = process.env.BROWSER_BRIDGE_SESSION_CLEANUP_INTERVAL_MS || process.env.BROWSER_VISION_SESSION_CLEANUP_INTERVAL_MS;
|
|
4353
|
+
if (env) {
|
|
4354
|
+
const parsed = Number(env);
|
|
4355
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
4356
|
+
return parsed;
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
4360
|
+
return 60 * 1e3;
|
|
4361
|
+
}
|
|
4362
|
+
return Math.min(60 * 1e3, Math.max(1e3, Math.floor(ttlMs / 2)));
|
|
4363
|
+
};
|
|
4253
4364
|
var startCoreServer = (options = {}) => {
|
|
4254
4365
|
const host = options.host ?? "127.0.0.1";
|
|
4255
4366
|
const port = resolveCorePort(options.port);
|
|
@@ -4262,6 +4373,19 @@ var startCoreServer = (options = {}) => {
|
|
|
4262
4373
|
server.listen(port, host, () => {
|
|
4263
4374
|
const address = server.address();
|
|
4264
4375
|
const resolvedPort = typeof address === "object" && address !== null ? address.port : port;
|
|
4376
|
+
const ttlMs = resolveSessionTtlMs();
|
|
4377
|
+
if (ttlMs > 0) {
|
|
4378
|
+
const intervalMs = resolveSessionCleanupIntervalMs(ttlMs);
|
|
4379
|
+
const timer = setInterval(() => {
|
|
4380
|
+
try {
|
|
4381
|
+
registry.cleanupIdleSessions(ttlMs);
|
|
4382
|
+
} catch (error) {
|
|
4383
|
+
console.warn("Session cleanup failed:", error);
|
|
4384
|
+
}
|
|
4385
|
+
}, intervalMs);
|
|
4386
|
+
timer.unref();
|
|
4387
|
+
server.on("close", () => clearInterval(timer));
|
|
4388
|
+
}
|
|
4265
4389
|
resolve({ app, registry, server, host, port: resolvedPort });
|
|
4266
4390
|
});
|
|
4267
4391
|
server.on("error", (error) => {
|