@btraut/browser-bridge 0.3.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +72 -31
- package/dist/api.js +1671 -1384
- package/dist/api.js.map +4 -4
- package/dist/index.js +228 -2
- package/dist/index.js.map +4 -4
- package/extension/dist/background.js +509 -2
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/content.js +71 -0
- package/extension/dist/content.js.map +2 -2
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
- package/skills/browser-bridge/SKILL.md +12 -0
- package/skills/browser-bridge/skill.json +1 -1
|
@@ -1,3 +1,97 @@
|
|
|
1
|
+
// packages/extension/src/error-sanitizer.ts
|
|
2
|
+
var TRAILING_PUNCTUATION_RE = /[.,;:!?]+$/;
|
|
3
|
+
var stripTrailingPunctuation = (value) => {
|
|
4
|
+
const match = value.match(TRAILING_PUNCTUATION_RE);
|
|
5
|
+
if (!match) {
|
|
6
|
+
return { core: value, trailing: "" };
|
|
7
|
+
}
|
|
8
|
+
const trailing = match[0] ?? "";
|
|
9
|
+
return { core: value.slice(0, -trailing.length), trailing };
|
|
10
|
+
};
|
|
11
|
+
var sanitizeUrlToken = (token) => {
|
|
12
|
+
const { core, trailing } = stripTrailingPunctuation(token);
|
|
13
|
+
try {
|
|
14
|
+
const parsed = new URL(core);
|
|
15
|
+
if (parsed.protocol === "file:") {
|
|
16
|
+
return `file://[redacted]${trailing}`;
|
|
17
|
+
}
|
|
18
|
+
if (parsed.origin && parsed.origin !== "null") {
|
|
19
|
+
return `${parsed.origin}${trailing}`;
|
|
20
|
+
}
|
|
21
|
+
if (parsed.protocol && parsed.host) {
|
|
22
|
+
return `${parsed.protocol}//${parsed.host}${trailing}`;
|
|
23
|
+
}
|
|
24
|
+
if (parsed.protocol) {
|
|
25
|
+
return `${parsed.protocol}${trailing}`;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
const fallback = core.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/\s]+)/);
|
|
30
|
+
if (fallback?.[1] && fallback?.[2]) {
|
|
31
|
+
return `${fallback[1]}://${fallback[2]}${trailing}`;
|
|
32
|
+
}
|
|
33
|
+
return `[redacted url]${trailing}`;
|
|
34
|
+
};
|
|
35
|
+
var URL_TOKEN_RE = /\b(?:https?|wss?|chrome-extension|chrome|chrome-devtools|devtools|edge|brave|file):\/\/[^\s"'<>)}\]]+/gi;
|
|
36
|
+
var VIEW_SOURCE_RE = /\bview-source:(https?:\/\/[^\s"'<>)}\]]+)/gi;
|
|
37
|
+
var sanitizeUrls = (message) => {
|
|
38
|
+
let next = message.replace(VIEW_SOURCE_RE, (_match, inner) => {
|
|
39
|
+
return `view-source:${sanitizeUrlToken(inner)}`;
|
|
40
|
+
});
|
|
41
|
+
next = next.replace(URL_TOKEN_RE, (match) => sanitizeUrlToken(match));
|
|
42
|
+
return next;
|
|
43
|
+
};
|
|
44
|
+
var extractBasename = (value) => {
|
|
45
|
+
const trimmed = value.trim();
|
|
46
|
+
const lastSlash = Math.max(
|
|
47
|
+
trimmed.lastIndexOf("/"),
|
|
48
|
+
trimmed.lastIndexOf("\\")
|
|
49
|
+
);
|
|
50
|
+
if (lastSlash < 0) {
|
|
51
|
+
return trimmed;
|
|
52
|
+
}
|
|
53
|
+
return trimmed.slice(lastSlash + 1);
|
|
54
|
+
};
|
|
55
|
+
var sanitizeWindowsPaths = (message) => {
|
|
56
|
+
const windowsPathRe = /\b[A-Za-z]:\\(?:[^\s"')\]}]+\\)+[^\s"')\]}]+(?::\d+(?::\d+)?)?/g;
|
|
57
|
+
return message.replace(windowsPathRe, (match) => extractBasename(match));
|
|
58
|
+
};
|
|
59
|
+
var sanitizeUnixPaths = (message) => {
|
|
60
|
+
const unixPathRe = /\/(?:Users|home|var|private|tmp|opt|etc|Library|Applications|Volumes|System)(?:\/[^\s"')\]}]+)+(?::\d+(?::\d+)?)?/g;
|
|
61
|
+
return message.replace(unixPathRe, (match) => extractBasename(match));
|
|
62
|
+
};
|
|
63
|
+
var sanitizeChromeErrorMessage = (message) => {
|
|
64
|
+
let next = message;
|
|
65
|
+
next = sanitizeUrls(next);
|
|
66
|
+
next = sanitizeWindowsPaths(next);
|
|
67
|
+
next = sanitizeUnixPaths(next);
|
|
68
|
+
return next;
|
|
69
|
+
};
|
|
70
|
+
var sanitizeUnknown = (value) => {
|
|
71
|
+
if (typeof value === "string") {
|
|
72
|
+
return sanitizeChromeErrorMessage(value);
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
return value.map((entry) => sanitizeUnknown(entry));
|
|
76
|
+
}
|
|
77
|
+
if (!value || typeof value !== "object") {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
const record = value;
|
|
81
|
+
const next = {};
|
|
82
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
83
|
+
next[key] = sanitizeUnknown(entry);
|
|
84
|
+
}
|
|
85
|
+
return next;
|
|
86
|
+
};
|
|
87
|
+
var sanitizeDriveErrorInfo = (error) => {
|
|
88
|
+
return {
|
|
89
|
+
...error,
|
|
90
|
+
message: sanitizeChromeErrorMessage(error.message),
|
|
91
|
+
...error.details ? { details: sanitizeUnknown(error.details) } : {}
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
1
95
|
// packages/extension/src/background.ts
|
|
2
96
|
var DEFAULT_CORE_PORT = 3210;
|
|
3
97
|
var CORE_PORT_KEY = "corePort";
|
|
@@ -48,6 +142,59 @@ var wrapChromeVoid = (invoker) => {
|
|
|
48
142
|
});
|
|
49
143
|
});
|
|
50
144
|
};
|
|
145
|
+
var delayMs = async (ms) => {
|
|
146
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
await new Promise((resolve) => {
|
|
150
|
+
self.setTimeout(resolve, ms);
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
var parseDataUrl = (dataUrl) => {
|
|
154
|
+
const match = /^data:([^;]+);base64,(.*)$/s.exec(dataUrl);
|
|
155
|
+
if (!match) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return { mime: match[1] ?? "application/octet-stream", base64: match[2] };
|
|
159
|
+
};
|
|
160
|
+
var arrayBufferToBase64 = (buffer) => {
|
|
161
|
+
const bytes = new Uint8Array(buffer);
|
|
162
|
+
const chunkSize = 32768;
|
|
163
|
+
let binary = "";
|
|
164
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
165
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
166
|
+
binary += String.fromCharCode(...chunk);
|
|
167
|
+
}
|
|
168
|
+
return btoa(binary);
|
|
169
|
+
};
|
|
170
|
+
var renderDataUrlToFormat = async (dataUrl, format, quality) => {
|
|
171
|
+
const parsed = parseDataUrl(dataUrl);
|
|
172
|
+
if (!parsed) {
|
|
173
|
+
throw new Error("Invalid screenshot data URL.");
|
|
174
|
+
}
|
|
175
|
+
const blob = await (await fetch(dataUrl)).blob();
|
|
176
|
+
const bitmap = await createImageBitmap(blob);
|
|
177
|
+
try {
|
|
178
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
179
|
+
const ctx = canvas.getContext("2d");
|
|
180
|
+
if (!ctx) {
|
|
181
|
+
throw new Error("Canvas context unavailable.");
|
|
182
|
+
}
|
|
183
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
184
|
+
const mime = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
|
|
185
|
+
const q = typeof quality === "number" && Number.isFinite(quality) ? Math.max(0, Math.min(1, quality / 100)) : void 0;
|
|
186
|
+
const out = format === "png" ? await canvas.convertToBlob({ type: mime }) : await canvas.convertToBlob({ type: mime, quality: q });
|
|
187
|
+
const base64 = arrayBufferToBase64(await out.arrayBuffer());
|
|
188
|
+
return {
|
|
189
|
+
mime,
|
|
190
|
+
data_base64: base64,
|
|
191
|
+
width_px: bitmap.width,
|
|
192
|
+
height_px: bitmap.height
|
|
193
|
+
};
|
|
194
|
+
} finally {
|
|
195
|
+
bitmap.close();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
51
198
|
var readCorePort = async () => {
|
|
52
199
|
return await new Promise((resolve) => {
|
|
53
200
|
chrome.storage.local.get(
|
|
@@ -446,7 +593,7 @@ var DriveSocket = class {
|
|
|
446
593
|
id: driveMessage.id,
|
|
447
594
|
action: driveMessage.action,
|
|
448
595
|
status: "error",
|
|
449
|
-
error
|
|
596
|
+
error: sanitizeDriveErrorInfo(error)
|
|
450
597
|
};
|
|
451
598
|
this.sendMessage(response);
|
|
452
599
|
};
|
|
@@ -703,6 +850,366 @@ var DriveSocket = class {
|
|
|
703
850
|
}
|
|
704
851
|
return;
|
|
705
852
|
}
|
|
853
|
+
case "drive.screenshot": {
|
|
854
|
+
const params = message.params ?? {};
|
|
855
|
+
let tabId = params.tab_id;
|
|
856
|
+
if (tabId !== void 0 && typeof tabId !== "number") {
|
|
857
|
+
respondError({
|
|
858
|
+
code: "INVALID_ARGUMENT",
|
|
859
|
+
message: "tab_id must be a number when provided.",
|
|
860
|
+
retryable: false
|
|
861
|
+
});
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (tabId === void 0) {
|
|
865
|
+
tabId = await getActiveTabId();
|
|
866
|
+
}
|
|
867
|
+
const mode = params.mode === "full_page" || params.mode === "viewport" || params.mode === "element" ? params.mode : "viewport";
|
|
868
|
+
const format = params.format === "jpeg" || params.format === "webp" ? params.format : "png";
|
|
869
|
+
const quality = typeof params.quality === "number" && Number.isFinite(params.quality) ? Math.max(0, Math.min(100, Math.floor(params.quality))) : void 0;
|
|
870
|
+
const tab = await getTab(tabId);
|
|
871
|
+
const url = tab.url;
|
|
872
|
+
if (typeof url === "string" && isRestrictedUrl(url)) {
|
|
873
|
+
respondError({
|
|
874
|
+
code: "NOT_SUPPORTED",
|
|
875
|
+
message: "Screenshots are not supported for this URL.",
|
|
876
|
+
retryable: false,
|
|
877
|
+
details: { url }
|
|
878
|
+
});
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const windowId = tab.windowId;
|
|
882
|
+
if (typeof windowId !== "number") {
|
|
883
|
+
respondError({
|
|
884
|
+
code: "TAB_NOT_FOUND",
|
|
885
|
+
message: "window_id missing for tab.",
|
|
886
|
+
retryable: false
|
|
887
|
+
});
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (typeof OffscreenCanvas === "undefined") {
|
|
891
|
+
respondError({
|
|
892
|
+
code: "NOT_SUPPORTED",
|
|
893
|
+
message: "OffscreenCanvas is unavailable in this extension host.",
|
|
894
|
+
retryable: false
|
|
895
|
+
});
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const captureVisible = async () => {
|
|
899
|
+
return await wrapChromeCallback(
|
|
900
|
+
(callback) => chrome.tabs.captureVisibleTab(
|
|
901
|
+
windowId,
|
|
902
|
+
{ format: "png" },
|
|
903
|
+
callback
|
|
904
|
+
)
|
|
905
|
+
);
|
|
906
|
+
};
|
|
907
|
+
const scrollTo = async (top, left) => {
|
|
908
|
+
const result = await sendToTab(tabId, "drive.scroll", {
|
|
909
|
+
top,
|
|
910
|
+
left,
|
|
911
|
+
behavior: "auto",
|
|
912
|
+
tab_id: tabId
|
|
913
|
+
});
|
|
914
|
+
if (!result.ok) {
|
|
915
|
+
throw new Error(result.error.message);
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
const getMetaInfo = async () => {
|
|
919
|
+
const meta = await sendToTab(
|
|
920
|
+
tabId,
|
|
921
|
+
"drive.screenshot_meta"
|
|
922
|
+
);
|
|
923
|
+
if (!meta.ok) {
|
|
924
|
+
throw new Error(meta.error.message);
|
|
925
|
+
}
|
|
926
|
+
const payload = meta.result;
|
|
927
|
+
if (!payload || typeof payload !== "object") {
|
|
928
|
+
throw new Error("Invalid screenshot metadata response.");
|
|
929
|
+
}
|
|
930
|
+
const record = payload;
|
|
931
|
+
const viewportHeight = record.viewportHeight;
|
|
932
|
+
const scrollHeight = record.scrollHeight;
|
|
933
|
+
const scrollY = record.scrollY;
|
|
934
|
+
const scrollX = record.scrollX;
|
|
935
|
+
const dpr = record.devicePixelRatio;
|
|
936
|
+
if (typeof viewportHeight !== "number" || !Number.isFinite(viewportHeight) || viewportHeight <= 0) {
|
|
937
|
+
throw new Error(
|
|
938
|
+
"viewportHeight missing from screenshot metadata."
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
if (typeof scrollHeight !== "number" || !Number.isFinite(scrollHeight) || scrollHeight <= 0) {
|
|
942
|
+
throw new Error("scrollHeight missing from screenshot metadata.");
|
|
943
|
+
}
|
|
944
|
+
const devicePixelRatio = typeof dpr === "number" && Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
|
|
945
|
+
const fullHeightPx = Math.round(scrollHeight * devicePixelRatio);
|
|
946
|
+
const maxHeightPx = 5e4;
|
|
947
|
+
if (fullHeightPx > maxHeightPx) {
|
|
948
|
+
throw new Error(
|
|
949
|
+
`Page is too tall to capture (max ${maxHeightPx}px).`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
const maxScrollY = Math.max(0, scrollHeight - viewportHeight);
|
|
953
|
+
const step = viewportHeight;
|
|
954
|
+
const positions = [];
|
|
955
|
+
for (let y = 0; y < maxScrollY; y += step) {
|
|
956
|
+
positions.push(y);
|
|
957
|
+
}
|
|
958
|
+
positions.push(maxScrollY);
|
|
959
|
+
const maxTiles = 200;
|
|
960
|
+
if (positions.length > maxTiles) {
|
|
961
|
+
throw new Error(
|
|
962
|
+
`Page requires too many tiles to capture (${positions.length}).`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
viewportHeight,
|
|
967
|
+
scrollHeight,
|
|
968
|
+
scrollY: typeof scrollY === "number" && Number.isFinite(scrollY) ? scrollY : 0,
|
|
969
|
+
scrollX: typeof scrollX === "number" && Number.isFinite(scrollX) ? scrollX : 0,
|
|
970
|
+
devicePixelRatio,
|
|
971
|
+
fullHeightPx,
|
|
972
|
+
positions
|
|
973
|
+
};
|
|
974
|
+
};
|
|
975
|
+
const canvasToResult = async (canvas) => {
|
|
976
|
+
const mime = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
|
|
977
|
+
const q = typeof quality === "number" && Number.isFinite(quality) ? Math.max(0, Math.min(1, quality / 100)) : void 0;
|
|
978
|
+
const blob = format === "png" ? await canvas.convertToBlob({ type: mime }) : await canvas.convertToBlob({ type: mime, quality: q });
|
|
979
|
+
const base64 = arrayBufferToBase64(await blob.arrayBuffer());
|
|
980
|
+
return {
|
|
981
|
+
mime,
|
|
982
|
+
data_base64: base64,
|
|
983
|
+
width_px: canvas.width,
|
|
984
|
+
height_px: canvas.height
|
|
985
|
+
};
|
|
986
|
+
};
|
|
987
|
+
const captureFullPageCanvas = async (metaInfo) => {
|
|
988
|
+
try {
|
|
989
|
+
await scrollTo(0, 0);
|
|
990
|
+
await delayMs(100);
|
|
991
|
+
const firstDataUrl = await captureVisible();
|
|
992
|
+
const firstBlob = await (await fetch(firstDataUrl)).blob();
|
|
993
|
+
const firstBitmap = await createImageBitmap(firstBlob);
|
|
994
|
+
const canvas = new OffscreenCanvas(
|
|
995
|
+
firstBitmap.width,
|
|
996
|
+
metaInfo.fullHeightPx
|
|
997
|
+
);
|
|
998
|
+
const ctx = canvas.getContext("2d");
|
|
999
|
+
if (!ctx) {
|
|
1000
|
+
firstBitmap.close();
|
|
1001
|
+
throw new Error("Canvas context unavailable.");
|
|
1002
|
+
}
|
|
1003
|
+
const drawTile = (bitmap, yCss) => {
|
|
1004
|
+
const destY = Math.round(yCss * metaInfo.devicePixelRatio);
|
|
1005
|
+
const remaining = metaInfo.fullHeightPx - destY;
|
|
1006
|
+
if (remaining <= 0) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const drawHeight = Math.min(bitmap.height, remaining);
|
|
1010
|
+
ctx.drawImage(
|
|
1011
|
+
bitmap,
|
|
1012
|
+
0,
|
|
1013
|
+
0,
|
|
1014
|
+
bitmap.width,
|
|
1015
|
+
drawHeight,
|
|
1016
|
+
0,
|
|
1017
|
+
destY,
|
|
1018
|
+
bitmap.width,
|
|
1019
|
+
drawHeight
|
|
1020
|
+
);
|
|
1021
|
+
};
|
|
1022
|
+
drawTile(firstBitmap, 0);
|
|
1023
|
+
firstBitmap.close();
|
|
1024
|
+
for (const y of metaInfo.positions.slice(1)) {
|
|
1025
|
+
await scrollTo(y, 0);
|
|
1026
|
+
await delayMs(100);
|
|
1027
|
+
const dataUrl = await captureVisible();
|
|
1028
|
+
const blob = await (await fetch(dataUrl)).blob();
|
|
1029
|
+
const bitmap = await createImageBitmap(blob);
|
|
1030
|
+
drawTile(bitmap, y);
|
|
1031
|
+
bitmap.close();
|
|
1032
|
+
}
|
|
1033
|
+
return canvas;
|
|
1034
|
+
} finally {
|
|
1035
|
+
try {
|
|
1036
|
+
await scrollTo(metaInfo.scrollY, metaInfo.scrollX);
|
|
1037
|
+
} catch {
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
if (mode === "viewport") {
|
|
1042
|
+
try {
|
|
1043
|
+
const dataUrl = await captureVisible();
|
|
1044
|
+
const rendered = await renderDataUrlToFormat(
|
|
1045
|
+
dataUrl,
|
|
1046
|
+
format,
|
|
1047
|
+
quality
|
|
1048
|
+
);
|
|
1049
|
+
respondOk(rendered);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
respondError({
|
|
1052
|
+
code: "ARTIFACT_IO_ERROR",
|
|
1053
|
+
message: error instanceof Error ? error.message : "Failed to capture screenshot.",
|
|
1054
|
+
retryable: false
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (mode === "element") {
|
|
1060
|
+
const selector = params.selector;
|
|
1061
|
+
if (typeof selector !== "string" || selector.trim().length === 0) {
|
|
1062
|
+
respondError({
|
|
1063
|
+
code: "INVALID_ARGUMENT",
|
|
1064
|
+
message: "selector must be a non-empty string.",
|
|
1065
|
+
retryable: false
|
|
1066
|
+
});
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
let metaInfo;
|
|
1070
|
+
try {
|
|
1071
|
+
metaInfo = await getMetaInfo();
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
respondError({
|
|
1074
|
+
code: "EVALUATION_FAILED",
|
|
1075
|
+
message: error instanceof Error ? error.message : "Failed to read screenshot metadata.",
|
|
1076
|
+
retryable: false
|
|
1077
|
+
});
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const element = await sendToTab(
|
|
1081
|
+
tabId,
|
|
1082
|
+
"drive.screenshot_element",
|
|
1083
|
+
{ selector }
|
|
1084
|
+
);
|
|
1085
|
+
if (!element.ok) {
|
|
1086
|
+
respondError(element.error);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const payload = element.result;
|
|
1090
|
+
if (!payload || typeof payload !== "object") {
|
|
1091
|
+
respondError({
|
|
1092
|
+
code: "EVALUATION_FAILED",
|
|
1093
|
+
message: "Invalid element metadata response.",
|
|
1094
|
+
retryable: false
|
|
1095
|
+
});
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const record = payload;
|
|
1099
|
+
const viewportLeft = record.viewportLeft;
|
|
1100
|
+
const viewportTop = record.viewportTop;
|
|
1101
|
+
const viewportWidth = record.viewportWidth;
|
|
1102
|
+
const viewportHeight = record.viewportHeight;
|
|
1103
|
+
const width = record.width;
|
|
1104
|
+
const height = record.height;
|
|
1105
|
+
const pageX = record.pageX;
|
|
1106
|
+
const pageY = record.pageY;
|
|
1107
|
+
const dpr = record.devicePixelRatio;
|
|
1108
|
+
const devicePixelRatio = typeof dpr === "number" && Number.isFinite(dpr) && dpr > 0 ? dpr : metaInfo.devicePixelRatio;
|
|
1109
|
+
if (typeof viewportLeft !== "number" || typeof viewportTop !== "number" || typeof viewportWidth !== "number" || typeof viewportHeight !== "number" || typeof width !== "number" || typeof height !== "number" || typeof pageX !== "number" || typeof pageY !== "number" || !Number.isFinite(viewportLeft) || !Number.isFinite(viewportTop) || !Number.isFinite(viewportWidth) || !Number.isFinite(viewportHeight) || !Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(pageX) || !Number.isFinite(pageY)) {
|
|
1110
|
+
respondError({
|
|
1111
|
+
code: "EVALUATION_FAILED",
|
|
1112
|
+
message: "Invalid element bounding box metadata.",
|
|
1113
|
+
retryable: false
|
|
1114
|
+
});
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const fitsInViewport = viewportLeft >= 0 && viewportTop >= 0 && viewportLeft + width <= viewportWidth && viewportTop + height <= viewportHeight;
|
|
1118
|
+
const cropW = Math.max(1, Math.round(width * devicePixelRatio));
|
|
1119
|
+
const cropH = Math.max(1, Math.round(height * devicePixelRatio));
|
|
1120
|
+
try {
|
|
1121
|
+
if (fitsInViewport) {
|
|
1122
|
+
const dataUrl = await captureVisible();
|
|
1123
|
+
const blob = await (await fetch(dataUrl)).blob();
|
|
1124
|
+
const bitmap = await createImageBitmap(blob);
|
|
1125
|
+
try {
|
|
1126
|
+
const cropX2 = Math.max(
|
|
1127
|
+
0,
|
|
1128
|
+
Math.round(viewportLeft * devicePixelRatio)
|
|
1129
|
+
);
|
|
1130
|
+
const cropY2 = Math.max(
|
|
1131
|
+
0,
|
|
1132
|
+
Math.round(viewportTop * devicePixelRatio)
|
|
1133
|
+
);
|
|
1134
|
+
const srcW2 = Math.min(cropW, bitmap.width - cropX2);
|
|
1135
|
+
const srcH2 = Math.min(cropH, bitmap.height - cropY2);
|
|
1136
|
+
if (srcW2 <= 0 || srcH2 <= 0) {
|
|
1137
|
+
throw new Error("Element is outside screenshot bounds.");
|
|
1138
|
+
}
|
|
1139
|
+
const cropCanvas2 = new OffscreenCanvas(srcW2, srcH2);
|
|
1140
|
+
const ctx2 = cropCanvas2.getContext("2d");
|
|
1141
|
+
if (!ctx2) {
|
|
1142
|
+
throw new Error("Canvas context unavailable.");
|
|
1143
|
+
}
|
|
1144
|
+
ctx2.drawImage(
|
|
1145
|
+
bitmap,
|
|
1146
|
+
cropX2,
|
|
1147
|
+
cropY2,
|
|
1148
|
+
srcW2,
|
|
1149
|
+
srcH2,
|
|
1150
|
+
0,
|
|
1151
|
+
0,
|
|
1152
|
+
srcW2,
|
|
1153
|
+
srcH2
|
|
1154
|
+
);
|
|
1155
|
+
respondOk(await canvasToResult(cropCanvas2));
|
|
1156
|
+
} finally {
|
|
1157
|
+
bitmap.close();
|
|
1158
|
+
}
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const fullCanvas = await captureFullPageCanvas(metaInfo);
|
|
1162
|
+
const cropX = Math.max(0, Math.round(pageX * devicePixelRatio));
|
|
1163
|
+
const cropY = Math.max(0, Math.round(pageY * devicePixelRatio));
|
|
1164
|
+
const srcW = Math.min(cropW, fullCanvas.width - cropX);
|
|
1165
|
+
const srcH = Math.min(cropH, fullCanvas.height - cropY);
|
|
1166
|
+
if (srcW <= 0 || srcH <= 0) {
|
|
1167
|
+
throw new Error("Element is outside screenshot bounds.");
|
|
1168
|
+
}
|
|
1169
|
+
const cropCanvas = new OffscreenCanvas(srcW, srcH);
|
|
1170
|
+
const ctx = cropCanvas.getContext("2d");
|
|
1171
|
+
if (!ctx) {
|
|
1172
|
+
throw new Error("Canvas context unavailable.");
|
|
1173
|
+
}
|
|
1174
|
+
ctx.drawImage(
|
|
1175
|
+
fullCanvas,
|
|
1176
|
+
cropX,
|
|
1177
|
+
cropY,
|
|
1178
|
+
srcW,
|
|
1179
|
+
srcH,
|
|
1180
|
+
0,
|
|
1181
|
+
0,
|
|
1182
|
+
srcW,
|
|
1183
|
+
srcH
|
|
1184
|
+
);
|
|
1185
|
+
respondOk(await canvasToResult(cropCanvas));
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
respondError({
|
|
1188
|
+
code: "ARTIFACT_IO_ERROR",
|
|
1189
|
+
message: error instanceof Error ? error.message : "Failed to capture element screenshot.",
|
|
1190
|
+
retryable: false
|
|
1191
|
+
});
|
|
1192
|
+
} finally {
|
|
1193
|
+
try {
|
|
1194
|
+
await scrollTo(metaInfo.scrollY, metaInfo.scrollX);
|
|
1195
|
+
} catch {
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
try {
|
|
1201
|
+
const metaInfo = await getMetaInfo();
|
|
1202
|
+
const canvas = await captureFullPageCanvas(metaInfo);
|
|
1203
|
+
respondOk(await canvasToResult(canvas));
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
respondError({
|
|
1206
|
+
code: "ARTIFACT_IO_ERROR",
|
|
1207
|
+
message: error instanceof Error ? error.message : "Failed to capture full page screenshot.",
|
|
1208
|
+
retryable: false
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
706
1213
|
default:
|
|
707
1214
|
respondError({
|
|
708
1215
|
code: "NOT_IMPLEMENTED",
|
|
@@ -733,7 +1240,7 @@ var DriveSocket = class {
|
|
|
733
1240
|
id: message.id,
|
|
734
1241
|
action: message.action,
|
|
735
1242
|
status: "error",
|
|
736
|
-
error
|
|
1243
|
+
error: sanitizeDriveErrorInfo(error)
|
|
737
1244
|
});
|
|
738
1245
|
};
|
|
739
1246
|
try {
|