@agent-native/creative-context 0.5.12 → 0.6.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/dist/actions/list-context-connections.js +1 -1
- package/dist/client/CreativeContextPanel.js +1 -1
- package/dist/connectors/index.d.ts +1 -0
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/index.js +1 -0
- package/dist/connectors/index.js.map +1 -1
- package/dist/connectors/rendered-design.d.ts +68 -0
- package/dist/connectors/rendered-design.d.ts.map +1 -0
- package/dist/connectors/rendered-design.js +362 -0
- package/dist/connectors/rendered-design.js.map +1 -0
- package/dist/connectors/rendered-page.d.ts +15 -1
- package/dist/connectors/rendered-page.d.ts.map +1 -1
- package/dist/connectors/rendered-page.js +602 -85
- package/dist/connectors/rendered-page.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/package.json +3 -2
- package/src/actions/list-context-connections.ts +1 -1
- package/src/client/CreativeContextPanel.tsx +1 -1
- package/src/connectors/index.ts +9 -0
- package/src/connectors/rendered-design.spec.ts +171 -0
- package/src/connectors/rendered-design.ts +498 -0
- package/src/connectors/rendered-page.spec.ts +380 -0
- package/src/connectors/rendered-page.ts +775 -108
- package/src/connectors/website.spec.ts +27 -71
- package/src/server/index.ts +9 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
isBlockedExtensionUrlWithDns,
|
|
@@ -29,6 +30,36 @@ const MAX_TYPOGRAPHY = 100;
|
|
|
29
30
|
const MAX_SPACING = 100;
|
|
30
31
|
const MAX_RADII = 64;
|
|
31
32
|
const MAX_CSS_VARIABLES = 128;
|
|
33
|
+
const MAX_COMPONENT_STYLES = 24;
|
|
34
|
+
const FONT_READY_TIMEOUT_MS = 4_000;
|
|
35
|
+
const MAX_BROWSER_RESOURCE_BYTES = 12 * 1024 * 1024;
|
|
36
|
+
const MAX_BROWSER_RESOURCE_COUNT = 400;
|
|
37
|
+
const MAX_BROWSER_RESOURCE_BYTES_TOTAL = 64 * 1024 * 1024;
|
|
38
|
+
const BROWSER_RESOURCE_TIMEOUT_MS = 15_000;
|
|
39
|
+
const BROWSER_REQUEST_HEADERS = new Set([
|
|
40
|
+
"accept",
|
|
41
|
+
"accept-language",
|
|
42
|
+
"if-modified-since",
|
|
43
|
+
"if-none-match",
|
|
44
|
+
"origin",
|
|
45
|
+
"range",
|
|
46
|
+
"referer",
|
|
47
|
+
"user-agent",
|
|
48
|
+
]);
|
|
49
|
+
const BROWSER_RESPONSE_HEADERS = new Set([
|
|
50
|
+
"connection",
|
|
51
|
+
"content-encoding",
|
|
52
|
+
"content-length",
|
|
53
|
+
"keep-alive",
|
|
54
|
+
"proxy-authenticate",
|
|
55
|
+
"proxy-authorization",
|
|
56
|
+
"set-cookie",
|
|
57
|
+
"set-cookie2",
|
|
58
|
+
"te",
|
|
59
|
+
"trailer",
|
|
60
|
+
"transfer-encoding",
|
|
61
|
+
"upgrade",
|
|
62
|
+
]);
|
|
32
63
|
|
|
33
64
|
export interface RenderedPageRequest {
|
|
34
65
|
url: string;
|
|
@@ -71,12 +102,19 @@ interface PlaywrightRequestLike {
|
|
|
71
102
|
url(): string;
|
|
72
103
|
isNavigationRequest(): boolean;
|
|
73
104
|
resourceType(): string;
|
|
105
|
+
method?(): string;
|
|
106
|
+
headers?(): Record<string, string>;
|
|
74
107
|
}
|
|
75
108
|
|
|
76
109
|
interface PlaywrightRouteLike {
|
|
77
110
|
request(): PlaywrightRequestLike;
|
|
78
111
|
continue(): Promise<void>;
|
|
79
112
|
abort(errorCode?: string): Promise<void>;
|
|
113
|
+
fulfill(options: {
|
|
114
|
+
status: number;
|
|
115
|
+
headers: Record<string, string>;
|
|
116
|
+
body: Uint8Array;
|
|
117
|
+
}): Promise<void>;
|
|
80
118
|
}
|
|
81
119
|
|
|
82
120
|
interface PlaywrightPageLike {
|
|
@@ -88,6 +126,10 @@ interface PlaywrightPageLike {
|
|
|
88
126
|
url: string,
|
|
89
127
|
options: { timeout: number; waitUntil: string },
|
|
90
128
|
): Promise<unknown>;
|
|
129
|
+
waitForLoadState?(
|
|
130
|
+
state: "load" | "domcontentloaded" | "networkidle",
|
|
131
|
+
options?: { timeout: number },
|
|
132
|
+
): Promise<void>;
|
|
91
133
|
title(): Promise<string>;
|
|
92
134
|
url(): string;
|
|
93
135
|
locator(selector: string): { innerText(): Promise<string> };
|
|
@@ -99,18 +141,23 @@ interface PlaywrightPageLike {
|
|
|
99
141
|
interface PlaywrightContextLike {
|
|
100
142
|
pages(): PlaywrightPageLike[];
|
|
101
143
|
newPage(): Promise<PlaywrightPageLike>;
|
|
144
|
+
close?(): Promise<void>;
|
|
102
145
|
}
|
|
103
146
|
|
|
104
147
|
interface PlaywrightBrowserLike {
|
|
105
148
|
contexts(): PlaywrightContextLike[];
|
|
106
|
-
newContext(): Promise<PlaywrightContextLike>;
|
|
149
|
+
newContext?(): Promise<PlaywrightContextLike>;
|
|
107
150
|
close(): Promise<void>;
|
|
108
151
|
}
|
|
109
152
|
|
|
110
153
|
interface PlaywrightLike {
|
|
111
154
|
chromium: {
|
|
112
155
|
connectOverCDP(endpoint: string): Promise<PlaywrightBrowserLike>;
|
|
113
|
-
launch(options: {
|
|
156
|
+
launch(options: {
|
|
157
|
+
headless: boolean;
|
|
158
|
+
args?: string[];
|
|
159
|
+
executablePath?: string;
|
|
160
|
+
}): Promise<PlaywrightBrowserLike>;
|
|
114
161
|
};
|
|
115
162
|
}
|
|
116
163
|
|
|
@@ -158,7 +205,8 @@ export class LayeredRenderedPageProvider implements RenderedPageProvider {
|
|
|
158
205
|
const connection = await this.#requestBuilderBrowserConnection({
|
|
159
206
|
sessionId: `creative-context-${randomUUID()}`,
|
|
160
207
|
});
|
|
161
|
-
const wsUrl =
|
|
208
|
+
const wsUrl =
|
|
209
|
+
typeof connection.wsUrl === "string" ? connection.wsUrl.trim() : "";
|
|
162
210
|
if (!wsUrl) throw new Error("Builder Browser did not return wsUrl.");
|
|
163
211
|
return await renderWithPlaywright(
|
|
164
212
|
playwright,
|
|
@@ -214,7 +262,7 @@ export class LayeredRenderedPageProvider implements RenderedPageProvider {
|
|
|
214
262
|
}
|
|
215
263
|
}
|
|
216
264
|
|
|
217
|
-
async function renderWithPlaywright(
|
|
265
|
+
export async function renderWithPlaywright(
|
|
218
266
|
playwright: PlaywrightLike,
|
|
219
267
|
request: RenderedPageRequest,
|
|
220
268
|
warnings: string[],
|
|
@@ -223,37 +271,98 @@ async function renderWithPlaywright(
|
|
|
223
271
|
): Promise<RenderedPageResult> {
|
|
224
272
|
const browser = wsUrl
|
|
225
273
|
? await playwright.chromium.connectOverCDP(wsUrl)
|
|
226
|
-
: await playwright.chromium
|
|
274
|
+
: await launchChromium(playwright.chromium);
|
|
275
|
+
let isolatedContext: PlaywrightContextLike | undefined;
|
|
227
276
|
try {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
277
|
+
// Never reuse a connected browser's ambient context: it can carry cookies,
|
|
278
|
+
// extensions, or tabs from another workflow. The safe proxy below is only
|
|
279
|
+
// useful when the page itself is isolated from that state.
|
|
280
|
+
if (!browser.newContext) {
|
|
281
|
+
throw new Error("Browser did not provide isolated context support.");
|
|
282
|
+
}
|
|
283
|
+
isolatedContext = await browser.newContext();
|
|
284
|
+
const page = await isolatedContext.newPage();
|
|
285
|
+
const getFinalNavigationUrl = await installNavigationGuard(
|
|
286
|
+
page,
|
|
287
|
+
request,
|
|
288
|
+
warnings,
|
|
289
|
+
);
|
|
231
290
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
232
291
|
await page.goto(request.url, {
|
|
233
292
|
timeout: boundedTimeout(request.timeoutMs),
|
|
234
293
|
waitUntil: request.waitUntil ?? "domcontentloaded",
|
|
235
294
|
});
|
|
236
|
-
|
|
295
|
+
await page
|
|
296
|
+
.waitForLoadState?.("load", {
|
|
297
|
+
timeout: Math.min(8_000, boundedTimeout(request.timeoutMs)),
|
|
298
|
+
})
|
|
299
|
+
.catch((error) => {
|
|
300
|
+
warnings.push(
|
|
301
|
+
`Browser load stabilization unavailable: ${errorMessage(error)}`,
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
// React hydration, CSS-in-JS insertion, and web fonts commonly finish just
|
|
305
|
+
// after `load`. Give those layers a bounded chance to settle, then capture
|
|
306
|
+
// the computed cascade rather than the server HTML.
|
|
307
|
+
await page
|
|
308
|
+
.waitForLoadState?.("networkidle", { timeout: 4_000 })
|
|
309
|
+
.catch((error) => {
|
|
310
|
+
warnings.push(
|
|
311
|
+
`Browser network-idle stabilization unavailable: ${errorMessage(error)}`,
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
await waitForFontReadiness(
|
|
315
|
+
page,
|
|
316
|
+
Math.min(FONT_READY_TIMEOUT_MS, boundedTimeout(request.timeoutMs)),
|
|
317
|
+
).catch((error) => {
|
|
318
|
+
warnings.push(
|
|
319
|
+
`Browser font readiness unavailable: ${errorMessage(error)}`,
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
323
|
+
await page.evaluate(dismissConsentOverlays).catch(() => undefined);
|
|
324
|
+
const finalUrl = getFinalNavigationUrl() ?? page.url();
|
|
237
325
|
await assertPublicBrowserUrl(finalUrl);
|
|
238
326
|
const [title, text, desktopScreenshot, extraction] = await Promise.all([
|
|
239
|
-
page.title().catch(() =>
|
|
327
|
+
page.title().catch((error) => {
|
|
328
|
+
warnings.push(
|
|
329
|
+
`Browser title extraction unavailable: ${errorMessage(error)}`,
|
|
330
|
+
);
|
|
331
|
+
return "";
|
|
332
|
+
}),
|
|
240
333
|
page
|
|
241
334
|
.locator("body")
|
|
242
335
|
.innerText()
|
|
243
|
-
.catch(() =>
|
|
336
|
+
.catch((error) => {
|
|
337
|
+
warnings.push(
|
|
338
|
+
`Browser text extraction unavailable: ${errorMessage(error)}`,
|
|
339
|
+
);
|
|
340
|
+
return "";
|
|
341
|
+
}),
|
|
244
342
|
page
|
|
245
343
|
.screenshot({ type: "png", fullPage: false })
|
|
246
344
|
.then(boundedScreenshot)
|
|
247
|
-
.catch(() =>
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
345
|
+
.catch((error) => {
|
|
346
|
+
warnings.push(
|
|
347
|
+
`Desktop screenshot unavailable: ${errorMessage(error)}`,
|
|
348
|
+
);
|
|
349
|
+
return undefined;
|
|
350
|
+
}),
|
|
351
|
+
page.evaluate(captureRenderedWebsiteContext).catch((error) => {
|
|
352
|
+
warnings.push(
|
|
353
|
+
`Browser style extraction unavailable: ${errorMessage(error)}`,
|
|
354
|
+
);
|
|
355
|
+
return emptyExtraction();
|
|
356
|
+
}),
|
|
251
357
|
]);
|
|
252
358
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
253
359
|
const mobileScreenshot = await page
|
|
254
360
|
.screenshot({ type: "png", fullPage: false })
|
|
255
361
|
.then(boundedScreenshot)
|
|
256
|
-
.catch(() =>
|
|
362
|
+
.catch((error) => {
|
|
363
|
+
warnings.push(`Mobile screenshot unavailable: ${errorMessage(error)}`);
|
|
364
|
+
return undefined;
|
|
365
|
+
});
|
|
257
366
|
const unboundedText = normalizeWhitespace(text);
|
|
258
367
|
const textTruncated = unboundedText.length > MAX_RENDERED_TEXT_CHARS;
|
|
259
368
|
const normalizedText = unboundedText.slice(0, MAX_RENDERED_TEXT_CHARS);
|
|
@@ -317,21 +426,88 @@ async function renderWithPlaywright(
|
|
|
317
426
|
},
|
|
318
427
|
};
|
|
319
428
|
} finally {
|
|
429
|
+
await isolatedContext?.close?.().catch((error) => {
|
|
430
|
+
warnings.push(
|
|
431
|
+
`Browser context cleanup unavailable: ${errorMessage(error)}`,
|
|
432
|
+
);
|
|
433
|
+
});
|
|
320
434
|
await browser.close().catch(() => undefined);
|
|
321
435
|
}
|
|
322
436
|
}
|
|
323
437
|
|
|
324
|
-
async function
|
|
438
|
+
async function waitForFontReadiness(
|
|
439
|
+
page: PlaywrightPageLike,
|
|
440
|
+
timeoutMs: number,
|
|
441
|
+
): Promise<void> {
|
|
442
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
443
|
+
try {
|
|
444
|
+
await Promise.race([
|
|
445
|
+
page.evaluate(async () => {
|
|
446
|
+
if (document.fonts?.ready) await document.fonts.ready;
|
|
447
|
+
}),
|
|
448
|
+
new Promise<never>((_, reject) => {
|
|
449
|
+
timeout = setTimeout(() => {
|
|
450
|
+
reject(new Error(`font readiness timed out after ${timeoutMs}ms`));
|
|
451
|
+
}, timeoutMs);
|
|
452
|
+
}),
|
|
453
|
+
]);
|
|
454
|
+
} finally {
|
|
455
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function installNavigationGuard(
|
|
460
|
+
page: PlaywrightPageLike,
|
|
461
|
+
renderRequest: RenderedPageRequest,
|
|
462
|
+
warnings: string[],
|
|
463
|
+
): Promise<() => string | undefined> {
|
|
464
|
+
let finalNavigationUrl: string | undefined;
|
|
465
|
+
let resourceCount = 0;
|
|
466
|
+
let resourceBytes = 0;
|
|
467
|
+
let reservedResourceBytes = 0;
|
|
468
|
+
const bodyBudgetWaiters: Array<() => void> = [];
|
|
469
|
+
let resourceLimitWarningAdded = false;
|
|
470
|
+
let blockedResourceWarningAdded = false;
|
|
471
|
+
let failedResourceWarningAdded = false;
|
|
472
|
+
|
|
473
|
+
const reserveBodyBudget = async (): Promise<() => void> => {
|
|
474
|
+
while (
|
|
475
|
+
reservedResourceBytes + MAX_BROWSER_RESOURCE_BYTES >
|
|
476
|
+
MAX_BROWSER_RESOURCE_BYTES_TOTAL
|
|
477
|
+
) {
|
|
478
|
+
await new Promise<void>((resolve) => {
|
|
479
|
+
bodyBudgetWaiters.push(resolve);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
reservedResourceBytes += MAX_BROWSER_RESOURCE_BYTES;
|
|
483
|
+
let released = false;
|
|
484
|
+
return () => {
|
|
485
|
+
if (released) return;
|
|
486
|
+
released = true;
|
|
487
|
+
reservedResourceBytes -= MAX_BROWSER_RESOURCE_BYTES;
|
|
488
|
+
bodyBudgetWaiters.shift()?.();
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const addWarning = (value: string): void => {
|
|
493
|
+
if (!warnings.includes(value)) warnings.push(value);
|
|
494
|
+
};
|
|
495
|
+
|
|
325
496
|
await page.route("**/*", async (route) => {
|
|
326
|
-
const
|
|
497
|
+
const browserRequest = route.request();
|
|
327
498
|
let parsed: URL;
|
|
328
499
|
try {
|
|
329
|
-
parsed = new URL(
|
|
500
|
+
parsed = new URL(browserRequest.url());
|
|
330
501
|
} catch {
|
|
502
|
+
// coercion-ok: an absent optional package means the next compatible probe should run.
|
|
331
503
|
await route.abort("blockedbyclient");
|
|
332
504
|
return;
|
|
333
505
|
}
|
|
334
|
-
if (
|
|
506
|
+
if (
|
|
507
|
+
parsed.protocol === "about:" ||
|
|
508
|
+
parsed.protocol === "data:" ||
|
|
509
|
+
parsed.protocol === "blob:"
|
|
510
|
+
) {
|
|
335
511
|
await route.continue();
|
|
336
512
|
return;
|
|
337
513
|
}
|
|
@@ -339,14 +515,294 @@ async function installNavigationGuard(page: PlaywrightPageLike): Promise<void> {
|
|
|
339
515
|
await route.abort("blockedbyclient");
|
|
340
516
|
return;
|
|
341
517
|
}
|
|
518
|
+
|
|
519
|
+
const method = (browserRequest.method?.() ?? "GET").toUpperCase();
|
|
520
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
521
|
+
await route.abort("blockedbyclient");
|
|
522
|
+
if (!resourceLimitWarningAdded) {
|
|
523
|
+
resourceLimitWarningAdded = true;
|
|
524
|
+
addWarning(
|
|
525
|
+
"Browser blocked a non-read-only resource request during extraction.",
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (resourceCount >= MAX_BROWSER_RESOURCE_COUNT) {
|
|
531
|
+
await route.abort("blockedbyclient");
|
|
532
|
+
if (!resourceLimitWarningAdded) {
|
|
533
|
+
resourceLimitWarningAdded = true;
|
|
534
|
+
addWarning(
|
|
535
|
+
`Browser resource budget reached (${MAX_BROWSER_RESOURCE_COUNT} requests).`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Reserve the request slot before the first await. Browser route handlers
|
|
542
|
+
// overlap, so incrementing only after the proxy response arrives lets a
|
|
543
|
+
// burst of requests all pass the limit check.
|
|
544
|
+
resourceCount += 1;
|
|
545
|
+
let bodyBudgetRelease: (() => void) | undefined;
|
|
546
|
+
let committedBytes = 0;
|
|
547
|
+
let fulfilled = false;
|
|
548
|
+
|
|
342
549
|
try {
|
|
343
550
|
await assertPublicBrowserUrl(parsed.href);
|
|
344
|
-
|
|
551
|
+
const response = await ssrfSafeFetch(
|
|
552
|
+
parsed.href,
|
|
553
|
+
{
|
|
554
|
+
method,
|
|
555
|
+
headers: browserRequestHeaders(browserRequest),
|
|
556
|
+
signal: AbortSignal.timeout(
|
|
557
|
+
Math.min(
|
|
558
|
+
BROWSER_RESOURCE_TIMEOUT_MS,
|
|
559
|
+
boundedTimeout(renderRequest.timeoutMs),
|
|
560
|
+
),
|
|
561
|
+
),
|
|
562
|
+
},
|
|
563
|
+
{ maxRedirects: 5 },
|
|
564
|
+
);
|
|
565
|
+
bodyBudgetRelease = await reserveBodyBudget();
|
|
566
|
+
const body = await readBoundedResponseBytes(
|
|
567
|
+
response,
|
|
568
|
+
MAX_BROWSER_RESOURCE_BYTES,
|
|
569
|
+
);
|
|
570
|
+
bodyBudgetRelease();
|
|
571
|
+
bodyBudgetRelease = undefined;
|
|
572
|
+
if (resourceBytes + body.byteLength > MAX_BROWSER_RESOURCE_BYTES_TOTAL) {
|
|
573
|
+
await route.abort("blockedbyclient");
|
|
574
|
+
if (!resourceLimitWarningAdded) {
|
|
575
|
+
resourceLimitWarningAdded = true;
|
|
576
|
+
addWarning(
|
|
577
|
+
`Browser resource budget reached (${MAX_BROWSER_RESOURCE_BYTES_TOTAL} bytes).`,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
resourceBytes += body.byteLength;
|
|
583
|
+
committedBytes = body.byteLength;
|
|
584
|
+
if (browserRequest.isNavigationRequest()) {
|
|
585
|
+
finalNavigationUrl = response.url || parsed.href;
|
|
586
|
+
}
|
|
587
|
+
await route.fulfill({
|
|
588
|
+
status: response.status,
|
|
589
|
+
headers: browserResponseHeaders(response),
|
|
590
|
+
body: Buffer.from(body),
|
|
591
|
+
});
|
|
592
|
+
fulfilled = true;
|
|
593
|
+
} catch (error) {
|
|
594
|
+
if (committedBytes > 0) {
|
|
595
|
+
resourceBytes -= committedBytes;
|
|
596
|
+
committedBytes = 0;
|
|
597
|
+
}
|
|
345
598
|
await route.abort("blockedbyclient");
|
|
346
|
-
|
|
599
|
+
if (isSsrfError(error)) {
|
|
600
|
+
if (!blockedResourceWarningAdded) {
|
|
601
|
+
blockedResourceWarningAdded = true;
|
|
602
|
+
addWarning(
|
|
603
|
+
"Some browser resources were blocked by the SSRF safety policy.",
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
} else if (!failedResourceWarningAdded) {
|
|
607
|
+
failedResourceWarningAdded = true;
|
|
608
|
+
addWarning(
|
|
609
|
+
"Some browser resources could not be fetched through the safe network proxy.",
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
} finally {
|
|
613
|
+
bodyBudgetRelease?.();
|
|
614
|
+
if (!fulfilled) resourceCount -= 1;
|
|
347
615
|
}
|
|
348
|
-
await route.continue();
|
|
349
616
|
});
|
|
617
|
+
return () => finalNavigationUrl;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function browserRequestHeaders(
|
|
621
|
+
request: PlaywrightRequestLike,
|
|
622
|
+
): Record<string, string> {
|
|
623
|
+
const source = request.headers?.() ?? {};
|
|
624
|
+
return Object.fromEntries(
|
|
625
|
+
Object.entries(source).filter(([name, value]) => {
|
|
626
|
+
return BROWSER_REQUEST_HEADERS.has(name.toLowerCase()) && Boolean(value);
|
|
627
|
+
}),
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function browserResponseHeaders(response: Response): Record<string, string> {
|
|
632
|
+
const headers: Record<string, string> = {};
|
|
633
|
+
response.headers.forEach((value, name) => {
|
|
634
|
+
if (!BROWSER_RESPONSE_HEADERS.has(name.toLowerCase())) {
|
|
635
|
+
headers[name] = value;
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
return headers;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function isSsrfError(error: unknown): boolean {
|
|
642
|
+
return /ssrf blocked|private\/internal|connect blocked/i.test(
|
|
643
|
+
errorMessage(error),
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const SYSTEM_CHROME_EXECUTABLES = [
|
|
648
|
+
"/usr/bin/google-chrome-stable",
|
|
649
|
+
"/usr/bin/google-chrome",
|
|
650
|
+
"/usr/bin/chromium",
|
|
651
|
+
"/usr/bin/chromium-browser",
|
|
652
|
+
];
|
|
653
|
+
|
|
654
|
+
async function launchChromium(
|
|
655
|
+
chromium: PlaywrightLike["chromium"],
|
|
656
|
+
): Promise<PlaywrightBrowserLike> {
|
|
657
|
+
const launchOptions = {
|
|
658
|
+
headless: true,
|
|
659
|
+
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
|
660
|
+
};
|
|
661
|
+
let missingBrowserError: unknown;
|
|
662
|
+
try {
|
|
663
|
+
return await chromium.launch(launchOptions);
|
|
664
|
+
} catch (error) {
|
|
665
|
+
if (!isMissingBrowserError(error)) throw error;
|
|
666
|
+
missingBrowserError = error;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const serverlessChromium = await loadOptionalServerlessChromium();
|
|
670
|
+
if (serverlessChromium) {
|
|
671
|
+
try {
|
|
672
|
+
const executablePath = await serverlessChromium.executablePath();
|
|
673
|
+
if (executablePath) {
|
|
674
|
+
return await chromium.launch({
|
|
675
|
+
...launchOptions,
|
|
676
|
+
args: [...launchOptions.args, ...(serverlessChromium.args ?? [])],
|
|
677
|
+
executablePath,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
} catch (error) {
|
|
681
|
+
if (!isMissingBrowserError(error)) throw error;
|
|
682
|
+
missingBrowserError = error;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
for (const executablePath of SYSTEM_CHROME_EXECUTABLES) {
|
|
687
|
+
if (!existsSync(executablePath)) continue;
|
|
688
|
+
try {
|
|
689
|
+
return await chromium.launch({ ...launchOptions, executablePath });
|
|
690
|
+
} catch {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (missingBrowserError) {
|
|
695
|
+
throw missingBrowserError;
|
|
696
|
+
}
|
|
697
|
+
throw new Error(
|
|
698
|
+
"No Chromium executable is available for browser extraction.",
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
interface ServerlessChromiumLike {
|
|
703
|
+
args?: string[];
|
|
704
|
+
executablePath(): Promise<string>;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function loadOptionalServerlessChromium(): Promise<ServerlessChromiumLike | null> {
|
|
708
|
+
const specifier = "@sparticuz/chromium";
|
|
709
|
+
try {
|
|
710
|
+
const module = (await import(/* @vite-ignore */ specifier)) as unknown as {
|
|
711
|
+
default?: Partial<ServerlessChromiumLike>;
|
|
712
|
+
} & Partial<ServerlessChromiumLike>;
|
|
713
|
+
const chromium = module.default ?? module;
|
|
714
|
+
return typeof chromium.executablePath === "function"
|
|
715
|
+
? (chromium as ServerlessChromiumLike)
|
|
716
|
+
: null;
|
|
717
|
+
} catch {
|
|
718
|
+
// coercion-ok: this optional capability is absent in non-serverless installs.
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/*
|
|
724
|
+
* Kept separate from Playwright loading so a deployment can omit the large
|
|
725
|
+
* serverless Chromium package and still use Builder Browser or system Chrome.
|
|
726
|
+
*/
|
|
727
|
+
async function loadOptionalPlaywright(): Promise<PlaywrightLike | null> {
|
|
728
|
+
for (const specifier of [
|
|
729
|
+
"playwright",
|
|
730
|
+
"@playwright/test",
|
|
731
|
+
"playwright-core",
|
|
732
|
+
]) {
|
|
733
|
+
try {
|
|
734
|
+
const module = (await import(
|
|
735
|
+
/* @vite-ignore */ specifier
|
|
736
|
+
)) as unknown as {
|
|
737
|
+
default?: Partial<PlaywrightLike>;
|
|
738
|
+
} & Partial<PlaywrightLike>;
|
|
739
|
+
for (const candidate of [module.default, module]) {
|
|
740
|
+
if (typeof candidate?.chromium?.launch === "function") {
|
|
741
|
+
return candidate as PlaywrightLike;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// coercion-ok: an absent optional package means the next compatible probe should run.
|
|
745
|
+
} catch {
|
|
746
|
+
// Try the next compatible browser package before falling back to HTML.
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// coercion-ok: browser packages are optional in non-Node runtimes.
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function defaultBuilderBrowserRequest(input: {
|
|
755
|
+
sessionId: string;
|
|
756
|
+
}): Promise<Record<string, unknown>> {
|
|
757
|
+
const server = (await import("@agent-native/core/server")) as unknown as {
|
|
758
|
+
requestBuilderBrowserConnection?: (value: {
|
|
759
|
+
sessionId: string;
|
|
760
|
+
}) => Promise<Record<string, unknown>>;
|
|
761
|
+
};
|
|
762
|
+
if (!server.requestBuilderBrowserConnection) {
|
|
763
|
+
throw new Error(
|
|
764
|
+
"@agent-native/core/server does not export requestBuilderBrowserConnection.",
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return server.requestBuilderBrowserConnection(input);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function isMissingBrowserError(error: unknown): boolean {
|
|
771
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
772
|
+
return /Executable doesn't exist|playwright install|browser.*not found|chromium.*not found/i.test(
|
|
773
|
+
message,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** Close common consent banners without accepting tracking or changing page data. */
|
|
778
|
+
function dismissConsentOverlays(): void {
|
|
779
|
+
const selectors = [
|
|
780
|
+
'[aria-label*="reject" i]',
|
|
781
|
+
'[aria-label*="decline" i]',
|
|
782
|
+
'[aria-label*="close" i]',
|
|
783
|
+
'[data-testid*="reject" i]',
|
|
784
|
+
'[data-testid*="decline" i]',
|
|
785
|
+
'[data-testid*="close" i]',
|
|
786
|
+
'button[id*="reject" i]',
|
|
787
|
+
'button[id*="decline" i]',
|
|
788
|
+
'button[class*="reject" i]',
|
|
789
|
+
'button[class*="decline" i]',
|
|
790
|
+
];
|
|
791
|
+
for (const selector of selectors) {
|
|
792
|
+
const element = document.querySelector<HTMLElement>(selector);
|
|
793
|
+
if (!element) continue;
|
|
794
|
+
const rect = element.getBoundingClientRect();
|
|
795
|
+
const style = getComputedStyle(element);
|
|
796
|
+
if (
|
|
797
|
+
rect.width > 0 &&
|
|
798
|
+
rect.height > 0 &&
|
|
799
|
+
style.visibility !== "hidden" &&
|
|
800
|
+
style.display !== "none"
|
|
801
|
+
) {
|
|
802
|
+
element.click();
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
350
806
|
}
|
|
351
807
|
|
|
352
808
|
async function renderStatic(
|
|
@@ -423,31 +879,6 @@ async function assertPublicBrowserUrl(value: string): Promise<void> {
|
|
|
423
879
|
}
|
|
424
880
|
}
|
|
425
881
|
|
|
426
|
-
async function defaultBuilderBrowserRequest(input: {
|
|
427
|
-
sessionId: string;
|
|
428
|
-
}): Promise<Record<string, unknown>> {
|
|
429
|
-
const server = (await import("@agent-native/core/server")) as unknown as {
|
|
430
|
-
requestBuilderBrowserConnection?: (value: {
|
|
431
|
-
sessionId: string;
|
|
432
|
-
}) => Promise<Record<string, unknown>>;
|
|
433
|
-
};
|
|
434
|
-
if (!server.requestBuilderBrowserConnection) {
|
|
435
|
-
throw new Error(
|
|
436
|
-
"@agent-native/core/server does not export requestBuilderBrowserConnection.",
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
return server.requestBuilderBrowserConnection(input);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
async function loadOptionalPlaywright(): Promise<PlaywrightLike | null> {
|
|
443
|
-
const specifier = "playwright";
|
|
444
|
-
try {
|
|
445
|
-
return (await import(specifier)) as PlaywrightLike;
|
|
446
|
-
} catch {
|
|
447
|
-
return null;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
882
|
function captureRenderedWebsiteContext(): WebsiteExtraction {
|
|
452
883
|
const MAX_ASSETS = 500;
|
|
453
884
|
const MAX_LINKS = 500;
|
|
@@ -455,8 +886,11 @@ function captureRenderedWebsiteContext(): WebsiteExtraction {
|
|
|
455
886
|
const MAX_TYPE_STYLES = 100;
|
|
456
887
|
const MAX_SPACING_VALUES = 100;
|
|
457
888
|
const MAX_RADIUS_VALUES = 64;
|
|
889
|
+
const MAX_SHADOWS = 32;
|
|
890
|
+
const MAX_BACKGROUNDS = 32;
|
|
458
891
|
const MAX_VARIABLES = 128;
|
|
459
892
|
const MAX_TEXT = 2_000_000;
|
|
893
|
+
const MAX_COMPONENT_STYLES = 24;
|
|
460
894
|
const assets = new Map<string, WebsiteExtraction["assets"][number]>();
|
|
461
895
|
const links = new Set<string>();
|
|
462
896
|
const colors: string[] = [];
|
|
@@ -466,7 +900,161 @@ function captureRenderedWebsiteContext(): WebsiteExtraction {
|
|
|
466
900
|
>();
|
|
467
901
|
const spacing = new Set<string>();
|
|
468
902
|
const radii = new Set<string>();
|
|
903
|
+
const shadows = new Set<string>();
|
|
904
|
+
const backgrounds = new Set<string>();
|
|
905
|
+
const components: NonNullable<
|
|
906
|
+
WebsiteExtraction["designTokens"]["components"]
|
|
907
|
+
> = [];
|
|
469
908
|
const cssVariables: Record<string, string> = {};
|
|
909
|
+
const semanticColors: NonNullable<
|
|
910
|
+
WebsiteExtraction["designTokens"]["semanticColors"]
|
|
911
|
+
> = {};
|
|
912
|
+
|
|
913
|
+
const isOpaque = (value: string): boolean => {
|
|
914
|
+
const normalized = value.trim().toLowerCase();
|
|
915
|
+
if (!normalized || normalized === "transparent") return false;
|
|
916
|
+
const functionBody = normalized.match(/^[a-z-]+\((.*)\)$/)?.[1];
|
|
917
|
+
if (!functionBody) return true;
|
|
918
|
+
const alphaValue = functionBody.includes("/")
|
|
919
|
+
? functionBody.split("/").at(-1)?.trim()
|
|
920
|
+
: functionBody.split(",").length === 4
|
|
921
|
+
? functionBody.split(",").at(-1)?.trim()
|
|
922
|
+
: undefined;
|
|
923
|
+
if (!alphaValue) return true;
|
|
924
|
+
const alpha = alphaValue.endsWith("%")
|
|
925
|
+
? Number.parseFloat(alphaValue) / 100
|
|
926
|
+
: Number.parseFloat(alphaValue);
|
|
927
|
+
return Number.isNaN(alpha) || alpha > 0.02;
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const addColor = (value: string): void => {
|
|
931
|
+
if (colors.length >= MAX_COLOR_VALUES || !isOpaque(value)) return;
|
|
932
|
+
const normalized = value.trim();
|
|
933
|
+
if (!colors.includes(normalized)) colors.push(normalized);
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
const visible = (element: Element): boolean => {
|
|
937
|
+
const style = getComputedStyle(element);
|
|
938
|
+
const rect = element.getBoundingClientRect();
|
|
939
|
+
return (
|
|
940
|
+
rect.width > 0 &&
|
|
941
|
+
rect.height > 0 &&
|
|
942
|
+
style.display !== "none" &&
|
|
943
|
+
style.visibility !== "hidden" &&
|
|
944
|
+
Number(style.opacity || 1) > 0.02
|
|
945
|
+
);
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
const firstVisible = (selector: string): Element | undefined =>
|
|
949
|
+
Array.from(document.querySelectorAll(selector)).find(visible);
|
|
950
|
+
|
|
951
|
+
const opaqueValue = (value: string): string | undefined =>
|
|
952
|
+
isOpaque(value) ? value.trim() : undefined;
|
|
953
|
+
|
|
954
|
+
const recordComputedStyle = (
|
|
955
|
+
element: Element,
|
|
956
|
+
role?: NonNullable<
|
|
957
|
+
WebsiteExtraction["designTokens"]["components"]
|
|
958
|
+
>[number]["role"],
|
|
959
|
+
) => {
|
|
960
|
+
const style = getComputedStyle(element);
|
|
961
|
+
const values = [
|
|
962
|
+
style.color,
|
|
963
|
+
style.backgroundColor,
|
|
964
|
+
style.borderTopColor,
|
|
965
|
+
style.borderRightColor,
|
|
966
|
+
style.borderBottomColor,
|
|
967
|
+
style.borderLeftColor,
|
|
968
|
+
];
|
|
969
|
+
values.forEach(addColor);
|
|
970
|
+
if (style.boxShadow && style.boxShadow !== "none") {
|
|
971
|
+
if (shadows.size < MAX_SHADOWS) shadows.add(style.boxShadow);
|
|
972
|
+
}
|
|
973
|
+
if (style.backgroundImage && style.backgroundImage !== "none") {
|
|
974
|
+
if (backgrounds.size < MAX_BACKGROUNDS) {
|
|
975
|
+
backgrounds.add(style.backgroundImage);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
for (const value of [
|
|
979
|
+
style.marginTop,
|
|
980
|
+
style.marginRight,
|
|
981
|
+
style.marginBottom,
|
|
982
|
+
style.marginLeft,
|
|
983
|
+
style.paddingTop,
|
|
984
|
+
style.paddingRight,
|
|
985
|
+
style.paddingBottom,
|
|
986
|
+
style.paddingLeft,
|
|
987
|
+
style.gap,
|
|
988
|
+
style.rowGap,
|
|
989
|
+
style.columnGap,
|
|
990
|
+
]) {
|
|
991
|
+
if (
|
|
992
|
+
spacing.size < MAX_SPACING_VALUES &&
|
|
993
|
+
value &&
|
|
994
|
+
value !== "0px" &&
|
|
995
|
+
value !== "normal"
|
|
996
|
+
) {
|
|
997
|
+
spacing.add(value);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
for (const value of [
|
|
1001
|
+
style.borderTopLeftRadius,
|
|
1002
|
+
style.borderTopRightRadius,
|
|
1003
|
+
style.borderBottomRightRadius,
|
|
1004
|
+
style.borderBottomLeftRadius,
|
|
1005
|
+
]) {
|
|
1006
|
+
if (radii.size < MAX_RADIUS_VALUES && value && value !== "0px") {
|
|
1007
|
+
radii.add(value);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const type = {
|
|
1011
|
+
family: style.fontFamily,
|
|
1012
|
+
size: style.fontSize,
|
|
1013
|
+
weight: style.fontWeight,
|
|
1014
|
+
lineHeight: style.lineHeight,
|
|
1015
|
+
letterSpacing: style.letterSpacing,
|
|
1016
|
+
};
|
|
1017
|
+
if (typography.size < MAX_TYPE_STYLES && type.family) {
|
|
1018
|
+
typography.set(JSON.stringify(type), type);
|
|
1019
|
+
}
|
|
1020
|
+
if (!role || components.length >= MAX_COMPONENT_STYLES) return style;
|
|
1021
|
+
components.push({
|
|
1022
|
+
role,
|
|
1023
|
+
fontFamily: style.fontFamily,
|
|
1024
|
+
fontSize: style.fontSize,
|
|
1025
|
+
fontWeight: style.fontWeight,
|
|
1026
|
+
lineHeight: style.lineHeight,
|
|
1027
|
+
letterSpacing: style.letterSpacing,
|
|
1028
|
+
color: opaqueValue(style.color),
|
|
1029
|
+
backgroundColor: opaqueValue(style.backgroundColor),
|
|
1030
|
+
backgroundImage:
|
|
1031
|
+
style.backgroundImage !== "none" ? style.backgroundImage : undefined,
|
|
1032
|
+
border:
|
|
1033
|
+
style.borderStyle !== "none" && style.borderWidth !== "0px"
|
|
1034
|
+
? style.border
|
|
1035
|
+
: undefined,
|
|
1036
|
+
borderRadius:
|
|
1037
|
+
style.borderTopLeftRadius !== "0px" ? style.borderRadius : undefined,
|
|
1038
|
+
boxShadow: style.boxShadow !== "none" ? style.boxShadow : undefined,
|
|
1039
|
+
padding: style.padding !== "0px" ? style.padding : undefined,
|
|
1040
|
+
gap: style.gap !== "normal" ? style.gap : undefined,
|
|
1041
|
+
textTransform:
|
|
1042
|
+
style.textTransform !== "none" ? style.textTransform : undefined,
|
|
1043
|
+
});
|
|
1044
|
+
return style;
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
const styleFor = (
|
|
1048
|
+
selector: string,
|
|
1049
|
+
role?: NonNullable<
|
|
1050
|
+
WebsiteExtraction["designTokens"]["components"]
|
|
1051
|
+
>[number]["role"],
|
|
1052
|
+
): CSSStyleDeclaration | undefined => {
|
|
1053
|
+
const element = firstVisible(selector);
|
|
1054
|
+
if (!element) return undefined;
|
|
1055
|
+
return recordComputedStyle(element, role);
|
|
1056
|
+
};
|
|
1057
|
+
|
|
470
1058
|
const addAsset = (
|
|
471
1059
|
raw: string | null | undefined,
|
|
472
1060
|
kind: WebsiteExtraction["assets"][number]["kind"],
|
|
@@ -548,64 +1136,99 @@ function captureRenderedWebsiteContext(): WebsiteExtraction {
|
|
|
548
1136
|
}
|
|
549
1137
|
}
|
|
550
1138
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1139
|
+
|
|
1140
|
+
const bodyStyle = document.body
|
|
1141
|
+
? recordComputedStyle(document.body)
|
|
1142
|
+
: undefined;
|
|
1143
|
+
const rootBackground = opaqueValue(rootStyle.backgroundColor);
|
|
1144
|
+
const bodyBackground = bodyStyle
|
|
1145
|
+
? opaqueValue(bodyStyle.backgroundColor)
|
|
1146
|
+
: undefined;
|
|
1147
|
+
const headingStyle = styleFor("h1, h2, h3", "heading");
|
|
1148
|
+
const textStyle = styleFor("p, li, label, body", "body");
|
|
1149
|
+
const buttonStyle = styleFor(
|
|
1150
|
+
'button, [role="button"], input[type="submit"], a[class*="button" i], a[class*="cta" i]',
|
|
1151
|
+
"button",
|
|
1152
|
+
);
|
|
1153
|
+
const linkStyle = styleFor("a[href]", "link");
|
|
1154
|
+
const cardStyle = styleFor(
|
|
1155
|
+
'article, [class*="card" i], [class*="panel" i], [class*="surface" i], section',
|
|
1156
|
+
"card",
|
|
1157
|
+
);
|
|
1158
|
+
styleFor("input, textarea, select", "input");
|
|
1159
|
+
styleFor("nav, header", "nav");
|
|
1160
|
+
styleFor('main, [class*="hero" i]', "hero");
|
|
1161
|
+
|
|
1162
|
+
const setSemantic = (
|
|
1163
|
+
role: keyof NonNullable<
|
|
1164
|
+
WebsiteExtraction["designTokens"]["semanticColors"]
|
|
1165
|
+
>,
|
|
1166
|
+
value: string | undefined,
|
|
1167
|
+
): void => {
|
|
1168
|
+
if (value) semanticColors[role] = value;
|
|
1169
|
+
};
|
|
1170
|
+
setSemantic("background", bodyBackground ?? rootBackground);
|
|
1171
|
+
setSemantic(
|
|
1172
|
+
"surface",
|
|
1173
|
+
cardStyle ? opaqueValue(cardStyle.backgroundColor) : undefined,
|
|
1174
|
+
);
|
|
1175
|
+
setSemantic("text", textStyle ? opaqueValue(textStyle.color) : undefined);
|
|
1176
|
+
const mutedElement = firstVisible("small, figcaption, [class*='muted' i]");
|
|
1177
|
+
setSemantic(
|
|
1178
|
+
"textMuted",
|
|
1179
|
+
mutedElement
|
|
1180
|
+
? opaqueValue(getComputedStyle(mutedElement).color)
|
|
1181
|
+
: undefined,
|
|
1182
|
+
);
|
|
1183
|
+
setSemantic(
|
|
1184
|
+
"accent",
|
|
1185
|
+
linkStyle
|
|
1186
|
+
? opaqueValue(linkStyle.color)
|
|
1187
|
+
: buttonStyle
|
|
1188
|
+
? opaqueValue(buttonStyle.backgroundColor)
|
|
1189
|
+
: undefined,
|
|
1190
|
+
);
|
|
1191
|
+
setSemantic(
|
|
1192
|
+
"primary",
|
|
1193
|
+
buttonStyle
|
|
1194
|
+
? (opaqueValue(buttonStyle.backgroundColor) ??
|
|
1195
|
+
opaqueValue(buttonStyle.color))
|
|
1196
|
+
: colors[0],
|
|
554
1197
|
);
|
|
1198
|
+
setSemantic(
|
|
1199
|
+
"secondary",
|
|
1200
|
+
semanticColors.surface ?? colors[1] ?? semanticColors.background,
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
const layoutElement = firstVisible("main, [role='main'], body > div");
|
|
1204
|
+
const layoutStyle = layoutElement
|
|
1205
|
+
? getComputedStyle(layoutElement)
|
|
1206
|
+
: undefined;
|
|
1207
|
+
const layoutRect = layoutElement?.getBoundingClientRect();
|
|
1208
|
+
const layout = {
|
|
1209
|
+
contentWidth:
|
|
1210
|
+
layoutStyle?.maxWidth && layoutStyle.maxWidth !== "none"
|
|
1211
|
+
? layoutStyle.maxWidth
|
|
1212
|
+
: layoutRect && layoutRect.width > 0
|
|
1213
|
+
? `${Math.round(layoutRect.width)}px`
|
|
1214
|
+
: undefined,
|
|
1215
|
+
pagePadding:
|
|
1216
|
+
bodyStyle?.padding && bodyStyle.padding !== "0px"
|
|
1217
|
+
? bodyStyle.padding
|
|
1218
|
+
: undefined,
|
|
1219
|
+
sectionGap:
|
|
1220
|
+
firstVisible("section, article") &&
|
|
1221
|
+
getComputedStyle(firstVisible("section, article")!).gap !== "normal" &&
|
|
1222
|
+
getComputedStyle(firstVisible("section, article")!).gap !== "0px"
|
|
1223
|
+
? getComputedStyle(firstVisible("section, article")!).gap
|
|
1224
|
+
: undefined,
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
const elements = Array.from(document.querySelectorAll("body *"))
|
|
1228
|
+
.filter(visible)
|
|
1229
|
+
.slice(0, 700);
|
|
555
1230
|
for (const element of elements) {
|
|
556
|
-
|
|
557
|
-
if (colors.length < MAX_COLOR_VALUES) {
|
|
558
|
-
colors.push(
|
|
559
|
-
style.color,
|
|
560
|
-
style.backgroundColor,
|
|
561
|
-
style.borderTopColor,
|
|
562
|
-
style.borderRightColor,
|
|
563
|
-
style.borderBottomColor,
|
|
564
|
-
style.borderLeftColor,
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
const type = {
|
|
568
|
-
family: style.fontFamily,
|
|
569
|
-
size: style.fontSize,
|
|
570
|
-
weight: style.fontWeight,
|
|
571
|
-
lineHeight: style.lineHeight,
|
|
572
|
-
letterSpacing: style.letterSpacing,
|
|
573
|
-
};
|
|
574
|
-
if (typography.size < MAX_TYPE_STYLES) {
|
|
575
|
-
typography.set(JSON.stringify(type), type);
|
|
576
|
-
}
|
|
577
|
-
for (const value of [
|
|
578
|
-
style.marginTop,
|
|
579
|
-
style.marginRight,
|
|
580
|
-
style.marginBottom,
|
|
581
|
-
style.marginLeft,
|
|
582
|
-
style.paddingTop,
|
|
583
|
-
style.paddingRight,
|
|
584
|
-
style.paddingBottom,
|
|
585
|
-
style.paddingLeft,
|
|
586
|
-
style.gap,
|
|
587
|
-
style.rowGap,
|
|
588
|
-
style.columnGap,
|
|
589
|
-
]) {
|
|
590
|
-
if (
|
|
591
|
-
spacing.size < MAX_SPACING_VALUES &&
|
|
592
|
-
value &&
|
|
593
|
-
value !== "0px" &&
|
|
594
|
-
value !== "normal"
|
|
595
|
-
) {
|
|
596
|
-
spacing.add(value);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
for (const value of [
|
|
600
|
-
style.borderTopLeftRadius,
|
|
601
|
-
style.borderTopRightRadius,
|
|
602
|
-
style.borderBottomRightRadius,
|
|
603
|
-
style.borderBottomLeftRadius,
|
|
604
|
-
]) {
|
|
605
|
-
if (radii.size < MAX_RADIUS_VALUES && value && value !== "0px") {
|
|
606
|
-
radii.add(value);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
1231
|
+
recordComputedStyle(element);
|
|
609
1232
|
}
|
|
610
1233
|
return {
|
|
611
1234
|
title: document.title,
|
|
@@ -618,6 +1241,11 @@ function captureRenderedWebsiteContext(): WebsiteExtraction {
|
|
|
618
1241
|
spacing: [...spacing],
|
|
619
1242
|
radii: [...radii],
|
|
620
1243
|
cssVariables,
|
|
1244
|
+
semanticColors,
|
|
1245
|
+
shadows: [...shadows],
|
|
1246
|
+
backgrounds: [...backgrounds],
|
|
1247
|
+
components,
|
|
1248
|
+
layout,
|
|
621
1249
|
},
|
|
622
1250
|
};
|
|
623
1251
|
}
|
|
@@ -625,6 +1253,16 @@ function captureRenderedWebsiteContext(): WebsiteExtraction {
|
|
|
625
1253
|
export function boundWebsiteExtraction(
|
|
626
1254
|
extraction: WebsiteExtraction,
|
|
627
1255
|
): WebsiteExtraction {
|
|
1256
|
+
const boundedComponents = extraction.designTokens.components
|
|
1257
|
+
?.slice(0, MAX_COMPONENT_STYLES)
|
|
1258
|
+
.map((component) =>
|
|
1259
|
+
Object.fromEntries(
|
|
1260
|
+
Object.entries(component).map(([key, value]) => [
|
|
1261
|
+
key,
|
|
1262
|
+
typeof value === "string" ? value.slice(0, 500) : value,
|
|
1263
|
+
]),
|
|
1264
|
+
),
|
|
1265
|
+
) as WebsiteExtraction["designTokens"]["components"];
|
|
628
1266
|
return {
|
|
629
1267
|
title: normalizeWhitespace(extraction.title).slice(0, 500),
|
|
630
1268
|
text: normalizeWhitespace(extraction.text).slice(
|
|
@@ -648,6 +1286,39 @@ export function boundWebsiteExtraction(
|
|
|
648
1286
|
.slice(0, MAX_CSS_VARIABLES)
|
|
649
1287
|
.map(([name, value]) => [name.slice(0, 500), value.slice(0, 4_096)]),
|
|
650
1288
|
),
|
|
1289
|
+
...(extraction.designTokens.semanticColors
|
|
1290
|
+
? {
|
|
1291
|
+
semanticColors: Object.fromEntries(
|
|
1292
|
+
Object.entries(extraction.designTokens.semanticColors)
|
|
1293
|
+
.filter(([, value]) => typeof value === "string" && value)
|
|
1294
|
+
.map(([name, value]) => [name, value.slice(0, 200)]),
|
|
1295
|
+
),
|
|
1296
|
+
}
|
|
1297
|
+
: {}),
|
|
1298
|
+
...(extraction.designTokens.shadows
|
|
1299
|
+
? {
|
|
1300
|
+
shadows: extraction.designTokens.shadows
|
|
1301
|
+
.slice(0, 32)
|
|
1302
|
+
.map((value) => value.slice(0, 500)),
|
|
1303
|
+
}
|
|
1304
|
+
: {}),
|
|
1305
|
+
...(extraction.designTokens.backgrounds
|
|
1306
|
+
? {
|
|
1307
|
+
backgrounds: extraction.designTokens.backgrounds
|
|
1308
|
+
.slice(0, 32)
|
|
1309
|
+
.map((value) => value.slice(0, 500)),
|
|
1310
|
+
}
|
|
1311
|
+
: {}),
|
|
1312
|
+
...(boundedComponents ? { components: boundedComponents } : {}),
|
|
1313
|
+
...(extraction.designTokens.layout
|
|
1314
|
+
? {
|
|
1315
|
+
layout: Object.fromEntries(
|
|
1316
|
+
Object.entries(extraction.designTokens.layout)
|
|
1317
|
+
.filter(([, value]) => typeof value === "string" && value)
|
|
1318
|
+
.map(([name, value]) => [name, value.slice(0, 200)]),
|
|
1319
|
+
),
|
|
1320
|
+
}
|
|
1321
|
+
: {}),
|
|
651
1322
|
},
|
|
652
1323
|
};
|
|
653
1324
|
}
|
|
@@ -696,10 +1367,6 @@ function boundedScreenshot(value: Uint8Array): Uint8Array | undefined {
|
|
|
696
1367
|
return value.byteLength <= MAX_SCREENSHOT_BYTES ? value : undefined;
|
|
697
1368
|
}
|
|
698
1369
|
|
|
699
|
-
function stringValue(value: unknown): string | undefined {
|
|
700
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
1370
|
function errorMessage(error: unknown): string {
|
|
704
1371
|
return error instanceof Error ? error.message : String(error);
|
|
705
1372
|
}
|