autokap 1.3.28 → 1.3.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.ts +6 -4
- package/dist/browser.js +199 -36
- package/package.json +1 -1
package/dist/browser.d.ts
CHANGED
|
@@ -99,6 +99,8 @@ export declare class Browser {
|
|
|
99
99
|
private elementMap;
|
|
100
100
|
private akNodeIndex;
|
|
101
101
|
private poolContext;
|
|
102
|
+
private persistentContext;
|
|
103
|
+
private ownedChromiumProfileDir;
|
|
102
104
|
/**
|
|
103
105
|
* Xvfb instance backing the headed Chromium used by clip capture on Cloud
|
|
104
106
|
* Run with NVIDIA L4. Set when forClipCapture spawns Xvfb; null otherwise
|
|
@@ -134,10 +136,10 @@ export declare class Browser {
|
|
|
134
136
|
*/
|
|
135
137
|
static forClipCapture(options: BrowserOptions, cursorScript: string): Promise<Browser>;
|
|
136
138
|
/**
|
|
137
|
-
* Close
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* Call browser.close() afterwards to
|
|
139
|
+
* Close the active capture context promptly after recording stops. Regular
|
|
140
|
+
* clip capture leaves the browser process alive for teardown; persistent
|
|
141
|
+
* Cloud Run contexts close their owned browser process with the context.
|
|
142
|
+
* Call browser.close() afterwards to stop Xvfb and remove owned temp state.
|
|
141
143
|
*/
|
|
142
144
|
closeContext(): Promise<void>;
|
|
143
145
|
launch(): Promise<void>;
|
package/dist/browser.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { chromium } from 'playwright';
|
|
2
2
|
import sharp from 'sharp';
|
|
3
3
|
import { createHash } from 'crypto';
|
|
4
|
+
import { cp, mkdir, readFile, rm, writeFile } from 'fs/promises';
|
|
5
|
+
import { join } from 'path';
|
|
4
6
|
import { buildAKNodeRuntimeIndex, deriveInteractiveElementsFromAKTree, disambiguateFingerprint, focusAKTree, fingerprintAKNode, serializeAKTree, } from './ak-tree.js';
|
|
5
7
|
/**
|
|
6
8
|
* Set-of-Marks (SoM) annotation: overlays colored [N] badges on each visible
|
|
@@ -100,6 +102,110 @@ import { CAPTURE_HIDE_STYLE_ID, dismissCookiesAndWidgets, ensureCaptureHideStyle
|
|
|
100
102
|
import { CHROMIUM_ARGS, browserPool } from './browser-pool.js';
|
|
101
103
|
import { isDebugEnabled, logger } from './logger.js';
|
|
102
104
|
import { XvfbProcess } from './xvfb-process.js';
|
|
105
|
+
const CLOUD_CHROMIUM_PROFILE_TEMPLATE_DIR = '/opt/chromium-profile';
|
|
106
|
+
const CLOUD_CHROMIUM_PROFILE_ROOT = '/tmp/autokap-chromium-profiles';
|
|
107
|
+
const CLOUD_CHROMIUM_TRANSLATE_BLOCKED_LANGUAGES = [
|
|
108
|
+
'fr',
|
|
109
|
+
'en',
|
|
110
|
+
'de',
|
|
111
|
+
'es',
|
|
112
|
+
'it',
|
|
113
|
+
'pt',
|
|
114
|
+
'nl',
|
|
115
|
+
'ja',
|
|
116
|
+
'zh',
|
|
117
|
+
'ko',
|
|
118
|
+
'ar',
|
|
119
|
+
'ru',
|
|
120
|
+
];
|
|
121
|
+
const CLOUD_CHROMIUM_ACCEPT_LANGUAGES = [
|
|
122
|
+
'fr-FR',
|
|
123
|
+
'fr',
|
|
124
|
+
'en-US',
|
|
125
|
+
'en',
|
|
126
|
+
'en-GB',
|
|
127
|
+
'es-ES',
|
|
128
|
+
'es',
|
|
129
|
+
'de-DE',
|
|
130
|
+
'de',
|
|
131
|
+
'it-IT',
|
|
132
|
+
'it',
|
|
133
|
+
'pt-PT',
|
|
134
|
+
'pt-BR',
|
|
135
|
+
'pt',
|
|
136
|
+
'nl-NL',
|
|
137
|
+
'nl',
|
|
138
|
+
'ja-JP',
|
|
139
|
+
'ja',
|
|
140
|
+
'zh-CN',
|
|
141
|
+
'zh-TW',
|
|
142
|
+
'zh',
|
|
143
|
+
'ko-KR',
|
|
144
|
+
'ko',
|
|
145
|
+
'ar',
|
|
146
|
+
'ru-RU',
|
|
147
|
+
'ru',
|
|
148
|
+
'he',
|
|
149
|
+
'th',
|
|
150
|
+
'tr',
|
|
151
|
+
'vi',
|
|
152
|
+
'sv',
|
|
153
|
+
'no',
|
|
154
|
+
'da',
|
|
155
|
+
'fi',
|
|
156
|
+
'pl',
|
|
157
|
+
'cs',
|
|
158
|
+
];
|
|
159
|
+
function isPlainRecord(value) {
|
|
160
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
161
|
+
}
|
|
162
|
+
async function prepareCloudChromiumProfile() {
|
|
163
|
+
const rootDir = process.env.AUTOKAP_CHROMIUM_PROFILE_ROOT || CLOUD_CHROMIUM_PROFILE_ROOT;
|
|
164
|
+
const templateDir = process.env.AUTOKAP_CHROMIUM_PROFILE_TEMPLATE_DIR || CLOUD_CHROMIUM_PROFILE_TEMPLATE_DIR;
|
|
165
|
+
const userDataDir = join(rootDir, `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
166
|
+
await mkdir(rootDir, { recursive: true });
|
|
167
|
+
try {
|
|
168
|
+
await cp(templateDir, userDataDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
if (err.code !== 'ENOENT') {
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
await mkdir(userDataDir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
await seedCloudChromiumPreferences(userDataDir);
|
|
177
|
+
return userDataDir;
|
|
178
|
+
}
|
|
179
|
+
async function seedCloudChromiumPreferences(userDataDir) {
|
|
180
|
+
const defaultDir = join(userDataDir, 'Default');
|
|
181
|
+
const preferencesPath = join(defaultDir, 'Preferences');
|
|
182
|
+
await mkdir(defaultDir, { recursive: true });
|
|
183
|
+
let preferences = {};
|
|
184
|
+
try {
|
|
185
|
+
const raw = await readFile(preferencesPath, 'utf8');
|
|
186
|
+
const parsed = JSON.parse(raw);
|
|
187
|
+
if (isPlainRecord(parsed)) {
|
|
188
|
+
preferences = parsed;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (err.code !== 'ENOENT') {
|
|
193
|
+
logger.warn(`[capture] Cloud clip capture: replacing unreadable Chromium Preferences: ${err.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const translate = isPlainRecord(preferences.translate) ? preferences.translate : {};
|
|
197
|
+
preferences.translate = {
|
|
198
|
+
...translate,
|
|
199
|
+
enabled: false,
|
|
200
|
+
};
|
|
201
|
+
preferences.translate_blocked_languages = CLOUD_CHROMIUM_TRANSLATE_BLOCKED_LANGUAGES;
|
|
202
|
+
const intl = isPlainRecord(preferences.intl) ? preferences.intl : {};
|
|
203
|
+
preferences.intl = {
|
|
204
|
+
...intl,
|
|
205
|
+
accept_languages: CLOUD_CHROMIUM_ACCEPT_LANGUAGES.join(','),
|
|
206
|
+
};
|
|
207
|
+
await writeFile(preferencesPath, `${JSON.stringify(preferences)}\n`, 'utf8');
|
|
208
|
+
}
|
|
103
209
|
async function withHelperTimeout(label, timeoutMs, work) {
|
|
104
210
|
if (!timeoutMs || timeoutMs <= 0) {
|
|
105
211
|
return work();
|
|
@@ -776,6 +882,8 @@ export class Browser {
|
|
|
776
882
|
elementMap = new Map();
|
|
777
883
|
akNodeIndex = new Map();
|
|
778
884
|
poolContext = false;
|
|
885
|
+
persistentContext = false;
|
|
886
|
+
ownedChromiumProfileDir = null;
|
|
779
887
|
/**
|
|
780
888
|
* Xvfb instance backing the headed Chromium used by clip capture on Cloud
|
|
781
889
|
* Run with NVIDIA L4. Set when forClipCapture spawns Xvfb; null otherwise
|
|
@@ -883,37 +991,27 @@ export class Browser {
|
|
|
883
991
|
process.env.DISPLAY = instance.xvfb.display;
|
|
884
992
|
logger.info(`[capture] Cloud clip capture: Chromium → Xvfb ${instance.xvfb.display} → ffmpeg x11grab + h264_nvenc path enabled`);
|
|
885
993
|
}
|
|
994
|
+
const cloudChromiumProfileDir = isLinuxWithGpu ? await prepareCloudChromiumProfile() : null;
|
|
995
|
+
instance.ownedChromiumProfileDir = cloudChromiumProfileDir;
|
|
886
996
|
// Kiosk + zero-position anchor for Xvfb: Chromium normally renders its
|
|
887
997
|
// own toolbar/tabbar in headed mode, which would appear at the top of
|
|
888
998
|
// every clip. `--kiosk` removes the address bar + tab strip;
|
|
889
999
|
// `--window-position=0,0` and `--window-size` make the page fill the
|
|
890
|
-
// Xvfb screen exactly.
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
// CDP screenshot capture (Mac/Win/local Linux) hits the page surface
|
|
896
|
-
// directly so it never sees these; only ffmpeg x11grab does.
|
|
1000
|
+
// Xvfb screen exactly. Chrome UI surfaces render OUTSIDE kiosk's chrome
|
|
1001
|
+
// and would otherwise show up in clips captured via x11grab. Translate is
|
|
1002
|
+
// controlled by the seeded Chromium preferences below; avoid passing our
|
|
1003
|
+
// own --disable-features list here because it replaces Playwright's
|
|
1004
|
+
// default disabled-feature map instead of merging with it.
|
|
897
1005
|
const xvfbWindowArgs = isLinuxWithGpu ? [
|
|
898
1006
|
'--kiosk',
|
|
899
1007
|
'--window-position=0,0',
|
|
900
|
-
'--disable-features=Translate,TranslateUI,AutofillServerCommunication,InfoBars',
|
|
901
|
-
'--disable-infobars',
|
|
902
1008
|
'--disable-blink-features=AutomationControlled',
|
|
903
|
-
'--disable-translate',
|
|
904
1009
|
'--no-default-browser-check',
|
|
905
1010
|
'--no-first-run',
|
|
906
1011
|
'--noerrdialogs',
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
// ja, zh, ko, ar, ru, plus regional variants), Chrome treats every page
|
|
911
|
-
// as "user already speaks this" and never offers translation. Belt-and-
|
|
912
|
-
// suspenders alongside the Dockerfile policy file (which Playwright's
|
|
913
|
-
// bundled Chromium might not honor per issue #32324) and the
|
|
914
|
-
// notranslate meta init script. accept-lang affects HTTP requests AND
|
|
915
|
-
// Chrome's translate decision; it's the only Chrome-side knob that
|
|
916
|
-
// works regardless of policy path detection or feature flag drift.
|
|
1012
|
+
// Belt-and-suspenders with the seeded translate prefs: keep the browser's
|
|
1013
|
+
// preferred-language list broad enough that Chrome has no reason to offer
|
|
1014
|
+
// translation on the marketing/demo languages we capture most often.
|
|
917
1015
|
'--accept-lang=fr-FR,fr,en-US,en,en-GB,es-ES,es,de-DE,de,it-IT,it,pt-PT,pt-BR,pt,nl-NL,nl,ja-JP,ja,zh-CN,zh-TW,zh,ko-KR,ko,ar,ru-RU,ru,he,th,tr,vi,sv,no,da,fi,pl,cs',
|
|
918
1016
|
] : [];
|
|
919
1017
|
const clipArgs = [
|
|
@@ -924,14 +1022,6 @@ export class Browser {
|
|
|
924
1022
|
...cloudGpuArgs,
|
|
925
1023
|
...xvfbWindowArgs,
|
|
926
1024
|
];
|
|
927
|
-
// Dedicated browser process for clip capture. Not pooled because clip
|
|
928
|
-
// capture installs context-level init scripts (cursor overlay).
|
|
929
|
-
// Cloud Run with Xvfb: launch headed (headless: false) so Chromium
|
|
930
|
-
// renders to the Xvfb framebuffer that ffmpeg captures.
|
|
931
|
-
instance.browser = await chromium.launch({
|
|
932
|
-
headless: isLinuxWithGpu ? false : !options.headed,
|
|
933
|
-
args: clipArgs,
|
|
934
|
-
});
|
|
935
1025
|
const contextOptions = {
|
|
936
1026
|
viewport: options.viewport,
|
|
937
1027
|
deviceScaleFactor,
|
|
@@ -939,7 +1029,43 @@ export class Browser {
|
|
|
939
1029
|
colorScheme: options.colorScheme ?? 'light',
|
|
940
1030
|
storageState: options.storageState,
|
|
941
1031
|
};
|
|
942
|
-
|
|
1032
|
+
// Dedicated browser process for clip capture. Not pooled because clip
|
|
1033
|
+
// capture installs context-level init scripts (cursor overlay). Cloud Run
|
|
1034
|
+
// uses a seeded persistent profile so Chromium reads translate.enabled=false
|
|
1035
|
+
// from Default/Preferences. Policy files and flags are not reliable against
|
|
1036
|
+
// the Chromium 127+ TranslateUI2024 bubble; the user-data-dir pref is.
|
|
1037
|
+
if (isLinuxWithGpu && cloudChromiumProfileDir) {
|
|
1038
|
+
instance.context = await chromium.launchPersistentContext(cloudChromiumProfileDir, {
|
|
1039
|
+
...contextOptions,
|
|
1040
|
+
headless: false,
|
|
1041
|
+
args: clipArgs,
|
|
1042
|
+
});
|
|
1043
|
+
instance.browser = instance.context.browser();
|
|
1044
|
+
instance.persistentContext = true;
|
|
1045
|
+
// launchPersistentContext opens an initial about:blank page automatically.
|
|
1046
|
+
// Reuse it instead of closing it + opening a new one: Chromium's --kiosk
|
|
1047
|
+
// mode blocks `Target.createTarget` (one-window-only enforcement), so
|
|
1048
|
+
// `context.newPage()` would fail with "Failed to open a new tab" — which
|
|
1049
|
+
// is exactly what bricked 1.3.29. The init scripts registered below run
|
|
1050
|
+
// on the next navigation regardless of when the page was created.
|
|
1051
|
+
const existingPages = instance.context.pages();
|
|
1052
|
+
instance.page = existingPages[0] ?? await instance.context.newPage();
|
|
1053
|
+
// Defensive: close any extra pages Chromium might have opened (unlikely
|
|
1054
|
+
// under --kiosk but cheap insurance). Keep index 0.
|
|
1055
|
+
for (let i = 1; i < existingPages.length; i++) {
|
|
1056
|
+
await existingPages[i].close().catch(() => undefined);
|
|
1057
|
+
}
|
|
1058
|
+
logger.info(`[capture] Cloud clip capture: persistent Chromium profile seeded at ${cloudChromiumProfileDir} (reusing initial page, kiosk blocks Target.createTarget)`);
|
|
1059
|
+
}
|
|
1060
|
+
else {
|
|
1061
|
+
// Non-cloud clip capture keeps the regular browser + incognito context
|
|
1062
|
+
// model used by local/macOS/Windows frame capture.
|
|
1063
|
+
instance.browser = await chromium.launch({
|
|
1064
|
+
headless: !options.headed,
|
|
1065
|
+
args: clipArgs,
|
|
1066
|
+
});
|
|
1067
|
+
instance.context = await instance.browser.newContext(contextOptions);
|
|
1068
|
+
}
|
|
943
1069
|
// Cloud Run only: inject the notranslate meta on every navigation so
|
|
944
1070
|
// Chromium's translate UI never prompts. The --disable-features=Translate*
|
|
945
1071
|
// launch flags are unreliable across Chromium versions (some translate
|
|
@@ -982,7 +1108,12 @@ export class Browser {
|
|
|
982
1108
|
document.addEventListener('DOMContentLoaded', install, { once: true });
|
|
983
1109
|
}
|
|
984
1110
|
}, { styleId: CAPTURE_HIDE_STYLE_ID, css: getCaptureHideCSS() });
|
|
985
|
-
instance.page
|
|
1111
|
+
// Persistent context (cloud) already set instance.page by reusing the
|
|
1112
|
+
// initial page launchPersistentContext opened. Only call newPage() for
|
|
1113
|
+
// the regular browser+context path (Mac/Win/local Linux).
|
|
1114
|
+
if (!instance.page) {
|
|
1115
|
+
instance.page = await instance.context.newPage();
|
|
1116
|
+
}
|
|
986
1117
|
// Cloud Run only: force the page window to fullscreen via CDP. The
|
|
987
1118
|
// --kiosk launch flag is silently ignored by Chromium for windows
|
|
988
1119
|
// created by Playwright via Target.createTarget (CDP) — kiosk only
|
|
@@ -1041,10 +1172,10 @@ export class Browser {
|
|
|
1041
1172
|
return instance;
|
|
1042
1173
|
}
|
|
1043
1174
|
/**
|
|
1044
|
-
* Close
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1047
|
-
* Call browser.close() afterwards to
|
|
1175
|
+
* Close the active capture context promptly after recording stops. Regular
|
|
1176
|
+
* clip capture leaves the browser process alive for teardown; persistent
|
|
1177
|
+
* Cloud Run contexts close their owned browser process with the context.
|
|
1178
|
+
* Call browser.close() afterwards to stop Xvfb and remove owned temp state.
|
|
1048
1179
|
*/
|
|
1049
1180
|
async closeContext() {
|
|
1050
1181
|
if (this.context) {
|
|
@@ -1053,6 +1184,10 @@ export class Browser {
|
|
|
1053
1184
|
}
|
|
1054
1185
|
catch { /* ignore */ }
|
|
1055
1186
|
this.context = null;
|
|
1187
|
+
this.page = null;
|
|
1188
|
+
if (this.persistentContext) {
|
|
1189
|
+
this.browser = null;
|
|
1190
|
+
}
|
|
1056
1191
|
}
|
|
1057
1192
|
}
|
|
1058
1193
|
async launch() {
|
|
@@ -1201,13 +1336,32 @@ export class Browser {
|
|
|
1201
1336
|
this.poolContext = false;
|
|
1202
1337
|
return;
|
|
1203
1338
|
}
|
|
1204
|
-
// Standalone mode (CLI): close the entire browser process
|
|
1205
|
-
|
|
1339
|
+
// Standalone mode (CLI): close the entire browser process. Persistent
|
|
1340
|
+
// contexts own their browser process, so closing the context is enough.
|
|
1341
|
+
if (this.persistentContext && this.context) {
|
|
1342
|
+
try {
|
|
1343
|
+
await this.context.close();
|
|
1344
|
+
}
|
|
1345
|
+
catch { /* ignore */ }
|
|
1346
|
+
this.context = null;
|
|
1347
|
+
this.browser = null;
|
|
1348
|
+
this.page = null;
|
|
1349
|
+
}
|
|
1350
|
+
else if (this.browser) {
|
|
1206
1351
|
await this.browser.close();
|
|
1207
1352
|
this.browser = null;
|
|
1208
1353
|
this.context = null;
|
|
1209
1354
|
this.page = null;
|
|
1210
1355
|
}
|
|
1356
|
+
else if (this.context) {
|
|
1357
|
+
try {
|
|
1358
|
+
await this.context.close();
|
|
1359
|
+
}
|
|
1360
|
+
catch { /* ignore */ }
|
|
1361
|
+
this.context = null;
|
|
1362
|
+
this.page = null;
|
|
1363
|
+
}
|
|
1364
|
+
this.persistentContext = false;
|
|
1211
1365
|
// Tear down Xvfb only after Chromium is fully gone — Chromium needs the
|
|
1212
1366
|
// X display for its own teardown (releasing GL contexts, X resources).
|
|
1213
1367
|
if (this.xvfb) {
|
|
@@ -1219,6 +1373,15 @@ export class Browser {
|
|
|
1219
1373
|
}
|
|
1220
1374
|
this.xvfb = null;
|
|
1221
1375
|
}
|
|
1376
|
+
if (this.ownedChromiumProfileDir) {
|
|
1377
|
+
try {
|
|
1378
|
+
await rm(this.ownedChromiumProfileDir, { recursive: true, force: true });
|
|
1379
|
+
}
|
|
1380
|
+
catch (err) {
|
|
1381
|
+
logger.warn(`[capture] Cloud clip capture: failed to remove Chromium profile ${this.ownedChromiumProfileDir}: ${err.message}`);
|
|
1382
|
+
}
|
|
1383
|
+
this.ownedChromiumProfileDir = null;
|
|
1384
|
+
}
|
|
1222
1385
|
}
|
|
1223
1386
|
async navigateTo(url) {
|
|
1224
1387
|
const page = this.ensurePage();
|