@btraut/browser-bridge 0.4.0 → 0.4.3
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 +18 -2
- package/README.md +57 -32
- package/dist/api.js +189 -26
- package/dist/api.js.map +3 -3
- package/dist/index.js +222 -1
- package/dist/index.js.map +4 -4
- package/extension/dist/background.js +413 -0
- package/extension/dist/background.js.map +3 -3
- package/extension/dist/content.js +71 -0
- package/extension/dist/content.js.map +2 -2
- package/extension/manifest.json +2 -2
- package/package.json +1 -1
- package/skills/browser-bridge/skill.json +1 -1
|
@@ -142,6 +142,59 @@ var wrapChromeVoid = (invoker) => {
|
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
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
|
+
};
|
|
145
198
|
var readCorePort = async () => {
|
|
146
199
|
return await new Promise((resolve) => {
|
|
147
200
|
chrome.storage.local.get(
|
|
@@ -797,6 +850,366 @@ var DriveSocket = class {
|
|
|
797
850
|
}
|
|
798
851
|
return;
|
|
799
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
|
+
}
|
|
800
1213
|
default:
|
|
801
1214
|
respondError({
|
|
802
1215
|
code: "NOT_IMPLEMENTED",
|