@detiq/crawler 1.1.11 → 1.1.13
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/crawler.js +52 -6
- package/dist/crawler.js.map +2 -2
- package/dist/job-queue.js +499 -10
- package/dist/job-queue.js.map +2 -2
- package/dist/locator-extractor.js +35 -1
- package/dist/locator-extractor.js.map +2 -2
- package/dist/test-executor.js +102 -0
- package/dist/test-executor.js.map +2 -2
- package/package.json +1 -1
- package/src/content-chunker.ts +2 -2
- package/src/crawler.ts +95 -30
- package/src/frontier.ts +5 -3
- package/src/gap-pass.ts +1 -1
- package/src/http-engine.ts +9 -4
- package/src/job-queue.ts +350 -18
- package/src/locator-extractor.ts +41 -3
- package/src/policies/crawl-safety.ts +7 -1
- package/src/profiles/domain-profile.ts +24 -1
- package/src/rate-limiter.ts +2 -2
- package/src/test-executor.ts +122 -0
- package/src/url-pattern-completer.ts +1 -1
- package/src/visual-diff-verdict.ts +4 -2
- package/types/crawler.d.ts +9 -0
- package/types/locator-extractor.d.ts +2 -0
- package/types/test-executor.d.ts +7 -0
package/src/crawler.ts
CHANGED
|
@@ -155,9 +155,9 @@ type AuthDiscoveryOptions = {
|
|
|
155
155
|
storageConfig?: any;
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
-
const AUTH_DISCOVERY_MAX_SCREENS = 36;
|
|
159
|
-
const AUTH_DISCOVERY_MAX_QUEUE = 48;
|
|
160
|
-
const AUTH_DISCOVERY_MAX_CLICKS_PER_PAGE = 6;
|
|
158
|
+
const AUTH_DISCOVERY_MAX_SCREENS = parseInt(process.env.CRAWLER_AUTH_DISCOVERY_MAX_SCREENS ?? '') || 36;
|
|
159
|
+
const AUTH_DISCOVERY_MAX_QUEUE = parseInt(process.env.CRAWLER_AUTH_DISCOVERY_MAX_QUEUE ?? '') || 48;
|
|
160
|
+
const AUTH_DISCOVERY_MAX_CLICKS_PER_PAGE = parseInt(process.env.CRAWLER_AUTH_DISCOVERY_MAX_CLICKS_PER_PAGE ?? '') || 6;
|
|
161
161
|
const AUTH_DISCOVERY_PATHS = [
|
|
162
162
|
'/',
|
|
163
163
|
'/login', '/log-in', '/signin', '/sign-in', '/auth', '/auth/login', '/account/login', '/user/login', '/app/login',
|
|
@@ -576,15 +576,15 @@ export async function crawlProject(opts: {
|
|
|
576
576
|
break;
|
|
577
577
|
case 'openrouter':
|
|
578
578
|
stagehandModel = `openai/${m}`;
|
|
579
|
-
stagehandModelOpts = { ...(apiKey ? { apiKey } : {}), baseURL: baseUrl || 'https://openrouter.ai/api/v1' };
|
|
579
|
+
stagehandModelOpts = { ...(apiKey ? { apiKey } : {}), baseURL: baseUrl || process.env.CRAWLER_OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1' };
|
|
580
580
|
break;
|
|
581
581
|
case 'ollama':
|
|
582
582
|
stagehandModel = `ollama/${m}`;
|
|
583
|
-
stagehandModelOpts = { baseURL: baseUrl || 'http://localhost:11434' };
|
|
583
|
+
stagehandModelOpts = { baseURL: baseUrl || process.env.CRAWLER_OLLAMA_BASE_URL || 'http://localhost:11434' };
|
|
584
584
|
break;
|
|
585
585
|
case 'nvidia':
|
|
586
586
|
stagehandModel = `openai/${m.startsWith('openai/') ? m.slice(7) : m}`;
|
|
587
|
-
stagehandModelOpts = { ...(apiKey ? { apiKey } : {}), baseURL: baseUrl || 'https://integrate.api.nvidia.com/v1' };
|
|
587
|
+
stagehandModelOpts = { ...(apiKey ? { apiKey } : {}), baseURL: baseUrl || process.env.CRAWLER_NVIDIA_BASE_URL || 'https://integrate.api.nvidia.com/v1' };
|
|
588
588
|
break;
|
|
589
589
|
case 'groq':
|
|
590
590
|
stagehandModel = `groq/${m}`;
|
|
@@ -635,15 +635,18 @@ export async function crawlProject(opts: {
|
|
|
635
635
|
const preferAnthropic = envModel.includes('claude') || (!envModel && !process.env.OPENAI_API_KEY);
|
|
636
636
|
if (preferAnthropic && process.env.ANTHROPIC_API_KEY) {
|
|
637
637
|
stagehandModelOpts = { apiKey: process.env.ANTHROPIC_API_KEY };
|
|
638
|
-
|
|
638
|
+
const defaultAnthropicModel = process.env.CRAWLER_DEFAULT_MODEL ?? 'claude-sonnet-4-6';
|
|
639
|
+
stagehandModel = envModel.includes('/') ? envModel : `anthropic/${envModel || defaultAnthropicModel}`;
|
|
639
640
|
stagehandConfigured = true;
|
|
640
641
|
} else if (process.env.OPENAI_API_KEY) {
|
|
641
642
|
stagehandModelOpts = { apiKey: process.env.OPENAI_API_KEY };
|
|
642
|
-
|
|
643
|
+
const defaultOpenaiModel = process.env.CRAWLER_DEFAULT_MODEL ?? 'gpt-4o';
|
|
644
|
+
stagehandModel = envModel.includes('/') ? envModel : `openai/${envModel || defaultOpenaiModel}`;
|
|
643
645
|
stagehandConfigured = true;
|
|
644
646
|
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
645
647
|
stagehandModelOpts = { apiKey: process.env.ANTHROPIC_API_KEY };
|
|
646
|
-
|
|
648
|
+
const defaultAnthropicModel2 = process.env.CRAWLER_DEFAULT_MODEL ?? 'claude-sonnet-4-6';
|
|
649
|
+
stagehandModel = envModel.includes('/') ? envModel : `anthropic/${envModel || defaultAnthropicModel2}`;
|
|
647
650
|
stagehandConfigured = true;
|
|
648
651
|
}
|
|
649
652
|
}
|
|
@@ -848,7 +851,7 @@ export async function crawlProject(opts: {
|
|
|
848
851
|
verbose: 1,
|
|
849
852
|
logger: stagehandLogger,
|
|
850
853
|
localBrowserLaunchOptions: { cdpUrl, headless: true },
|
|
851
|
-
domSettleTimeout: 2000,
|
|
854
|
+
domSettleTimeout: parseInt(process.env.CRAWLER_DOM_SETTLE_TIMEOUT_MS ?? '') || 2000,
|
|
852
855
|
});
|
|
853
856
|
|
|
854
857
|
await stagehand.init();
|
|
@@ -857,8 +860,8 @@ export async function crawlProject(opts: {
|
|
|
857
860
|
// Use resolvePage() to get the active V3Page (which is a full Playwright proxy).
|
|
858
861
|
const page = await (stagehand as any).resolvePage();
|
|
859
862
|
// Cap all Playwright operations so one stuck page can't freeze the crawl.
|
|
860
|
-
try { page.setDefaultTimeout(45_000); } catch { /* proxy may not expose this */ }
|
|
861
|
-
try { page.setDefaultNavigationTimeout(30_000); } catch { /* proxy may not expose this */ }
|
|
863
|
+
try { page.setDefaultTimeout(parseInt(process.env.CRAWLER_PLAYWRIGHT_DEFAULT_TIMEOUT_MS ?? '') || 45_000); } catch { /* proxy may not expose this */ }
|
|
864
|
+
try { page.setDefaultNavigationTimeout(parseInt(process.env.CRAWLER_PLAYWRIGHT_NAV_TIMEOUT_MS ?? '') || 30_000); } catch { /* proxy may not expose this */ }
|
|
862
865
|
// Stagehand V3Page proxy only supports the 'console' event (page.on throws for others).
|
|
863
866
|
// rawPage will be wired to a real Playwright Page via the CDP connection established below.
|
|
864
867
|
// Declared as `let` so the network-observer block can populate it.
|
|
@@ -1472,7 +1475,7 @@ export async function crawlProject(opts: {
|
|
|
1472
1475
|
];
|
|
1473
1476
|
const queued = new Set<string>(seedUrls.map(canonicalize));
|
|
1474
1477
|
const queue = new CrawlFrontier(opts.traversalStrategy ?? 'BFS', seedUrls, opts.bestFirstKeywords, opts.urlFilter);
|
|
1475
|
-
const deduplicator = new ContentDeduplicator(0.88);
|
|
1478
|
+
const deduplicator = new ContentDeduplicator(parseFloat(process.env.CRAWLER_DEDUP_THRESHOLD ?? '') || 0.88);
|
|
1476
1479
|
const deadLinkTracker = new DeadLinkTracker();
|
|
1477
1480
|
const redirectRegistry = new RedirectRegistry();
|
|
1478
1481
|
// Re-seed from previously-discovered screens so deep routes found by AI link
|
|
@@ -1626,7 +1629,12 @@ export async function crawlProject(opts: {
|
|
|
1626
1629
|
// Legal/policy-only pages — no interactive elements worth testing; skip to prevent noise
|
|
1627
1630
|
// NOTE: /about, /contact, /careers, /help are intentionally NOT skipped — they have
|
|
1628
1631
|
// real test targets (contact forms, job apply forms, help search, team content)
|
|
1629
|
-
const
|
|
1632
|
+
const skipPathExtra = process.env.CRAWL_SKIP_PATH_EXTRA
|
|
1633
|
+
? process.env.CRAWL_SKIP_PATH_EXTRA.split(',').map((p) => p.trim()).filter(Boolean)
|
|
1634
|
+
: [];
|
|
1635
|
+
const SKIP_PATH = skipPathExtra.length > 0
|
|
1636
|
+
? new RegExp(`/(terms|privacy|dpa|legal|cookie-policy|changelog|blog|imprint|gdpr|compliance|acceptable-use|security-policy|press|sitemap|${skipPathExtra.join('|')})([-/]|$)`, 'i')
|
|
1637
|
+
: /\/(terms|privacy|dpa|legal|cookie-policy|changelog|blog|imprint|gdpr|compliance|acceptable-use|security-policy|press|sitemap)([-/]|$)/i;
|
|
1630
1638
|
const segIdx = opts.segmentIndex ?? 1;
|
|
1631
1639
|
|
|
1632
1640
|
const robotsCache = opts.respectRobotsTxt ? new RobotsCache() : null;
|
|
@@ -1637,6 +1645,7 @@ export async function crawlProject(opts: {
|
|
|
1637
1645
|
// (was: 3 errors → skip 10 screens — too aggressive for Nvidia NIM which rate-limits quickly)
|
|
1638
1646
|
let aiErrorStreak = 0;
|
|
1639
1647
|
let aiSkipCount = 0;
|
|
1648
|
+
const aiMinElements = parseInt(process.env.CRAWL_AI_LINK_MIN_ELEMENTS ?? '') || 5;
|
|
1640
1649
|
let aiRateLimitWarned = false;
|
|
1641
1650
|
// Hard guard: total URLs attempted (including skips) capped at maxScreens * 4
|
|
1642
1651
|
// to prevent infinite-URL-explosion from WooCommerce/Shopify facet filter combos
|
|
@@ -1746,7 +1755,7 @@ export async function crawlProject(opts: {
|
|
|
1746
1755
|
for (let i = 1; i < CRAWL_PAGE_PARALLELISM; i++) {
|
|
1747
1756
|
const wp = await pwCtx.newPage();
|
|
1748
1757
|
await wp.addInitScript(() => { Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); }).catch(() => {});
|
|
1749
|
-
wp.setDefaultTimeout(45_000);
|
|
1758
|
+
wp.setDefaultTimeout(parseInt(process.env.CRAWLER_PLAYWRIGHT_DEFAULT_TIMEOUT_MS ?? '') || 45_000);
|
|
1750
1759
|
liteWorkerPromises.push(runLiteWorker(wp).finally(() => wp.close().catch(() => {})));
|
|
1751
1760
|
}
|
|
1752
1761
|
}
|
|
@@ -2469,22 +2478,68 @@ export async function crawlProject(opts: {
|
|
|
2469
2478
|
// FM-4: capture device pixel ratio for coordinate scaling in element highlighting
|
|
2470
2479
|
const pageDpr: number = await page.evaluate(() => window.devicePixelRatio ?? 1).catch(() => 1);
|
|
2471
2480
|
|
|
2472
|
-
//
|
|
2473
|
-
const
|
|
2481
|
+
// Extract elements first so we can highlight them in the screenshot
|
|
2482
|
+
const elements = await extractElements(stagehand, page, elementLLM ?? undefined).catch((err: any) => {
|
|
2483
|
+
console.error('[crawler] extractElements failed:', err);
|
|
2484
|
+
return [] as Awaited<ReturnType<typeof extractElements>>;
|
|
2485
|
+
});
|
|
2486
|
+
|
|
2487
|
+
// Inject bounding boxes for the screenshot
|
|
2488
|
+
await page.evaluate((elems: any[]) => {
|
|
2489
|
+
const container = document.createElement('div');
|
|
2490
|
+
container.id = '__zeta_element_highlights';
|
|
2491
|
+
container.style.position = 'absolute';
|
|
2492
|
+
container.style.top = '0';
|
|
2493
|
+
container.style.left = '0';
|
|
2494
|
+
container.style.width = '100%';
|
|
2495
|
+
container.style.height = '100%';
|
|
2496
|
+
container.style.pointerEvents = 'none';
|
|
2497
|
+
container.style.zIndex = '2147483647';
|
|
2498
|
+
|
|
2499
|
+
elems.forEach((el, index) => {
|
|
2500
|
+
if (!el.boundingRect) return;
|
|
2501
|
+
const rect = el.boundingRect;
|
|
2502
|
+
const box = document.createElement('div');
|
|
2503
|
+
box.style.position = 'absolute';
|
|
2504
|
+
box.style.left = (rect.x + window.scrollX) + 'px';
|
|
2505
|
+
box.style.top = (rect.y + window.scrollY) + 'px';
|
|
2506
|
+
box.style.width = rect.width + 'px';
|
|
2507
|
+
box.style.height = rect.height + 'px';
|
|
2508
|
+
box.style.border = '2px solid rgba(255, 0, 0, 0.8)';
|
|
2509
|
+
box.style.backgroundColor = 'rgba(255, 0, 0, 0.1)';
|
|
2510
|
+
box.style.boxSizing = 'border-box';
|
|
2511
|
+
|
|
2512
|
+
const label = document.createElement('div');
|
|
2513
|
+
label.textContent = el.role || 'element';
|
|
2514
|
+
label.style.position = 'absolute';
|
|
2515
|
+
label.style.top = '-16px';
|
|
2516
|
+
label.style.left = '-2px';
|
|
2517
|
+
label.style.backgroundColor = 'rgba(255, 0, 0, 0.9)';
|
|
2518
|
+
label.style.color = '#FFF';
|
|
2519
|
+
label.style.fontSize = '10px';
|
|
2520
|
+
label.style.padding = '2px 4px';
|
|
2521
|
+
label.style.borderRadius = '2px';
|
|
2522
|
+
label.style.whiteSpace = 'nowrap';
|
|
2523
|
+
label.style.fontFamily = 'monospace';
|
|
2524
|
+
box.appendChild(label);
|
|
2525
|
+
|
|
2526
|
+
container.appendChild(box);
|
|
2527
|
+
});
|
|
2528
|
+
document.body.appendChild(container);
|
|
2529
|
+
}, elements).catch(() => {});
|
|
2530
|
+
|
|
2531
|
+
// Parallelize: screenshot + link discovery + AT capture
|
|
2532
|
+
const [, nextLinks, atElements] = await Promise.all([
|
|
2474
2533
|
page.screenshot({ path: shotPath, fullPage: true })
|
|
2475
2534
|
.then(async () => {
|
|
2476
2535
|
const cloudUrl = await uploadToCloud(shotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(shotPath)}`);
|
|
2477
2536
|
return (AppBrain.saveScreenshot as any)(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, 'DESKTOP', cloudUrl, pageDpr);
|
|
2478
2537
|
})
|
|
2479
2538
|
.catch((err: any) => console.error('[crawler] page.screenshot failed:', err)),
|
|
2480
|
-
extractElements(stagehand, page, elementLLM ?? undefined).catch((err: any) => {
|
|
2481
|
-
console.error('[crawler] extractElements failed:', err);
|
|
2482
|
-
return [] as Awaited<ReturnType<typeof extractElements>>;
|
|
2483
|
-
}),
|
|
2484
2539
|
// Always run AI link discovery (even when cache exists) — cache is merged in, not used as a replacement.
|
|
2485
2540
|
// Skipping AI when cache exists was a bug: stale cache from broken crawls permanently blocked discovery.
|
|
2486
2541
|
// Skip AI on simple pages (< 5 interactive elements) — DOM-only is sufficient and saves ~12s per page.
|
|
2487
|
-
discoverLinks(stagehand, origin, page, linkLLM ?? undefined, aiSkipCount > 0 || preDomCount <
|
|
2542
|
+
discoverLinks(stagehand, origin, page, linkLLM ?? undefined, aiSkipCount > 0 || preDomCount < aiMinElements)
|
|
2488
2543
|
.then((freshLinks) => {
|
|
2489
2544
|
if (aiSkipCount > 0) aiSkipCount--;
|
|
2490
2545
|
aiErrorStreak = 0;
|
|
@@ -2517,6 +2572,12 @@ export async function crawlProject(opts: {
|
|
|
2517
2572
|
}),
|
|
2518
2573
|
readScreen(page).catch(() => [] as Awaited<ReturnType<typeof readScreen>>),
|
|
2519
2574
|
]);
|
|
2575
|
+
|
|
2576
|
+
// Cleanup highlights
|
|
2577
|
+
await page.evaluate(() => {
|
|
2578
|
+
const container = document.getElementById('__zeta_element_highlights');
|
|
2579
|
+
if (container) container.remove();
|
|
2580
|
+
}).catch(() => {});
|
|
2520
2581
|
logger.info({ url, screenId: screen.id, version: screen.version, elements: elements.length, links: nextLinks.length, atSample: atElements.slice(0, 3).map((a: any) => a.label) }, '[crawler] screen captured');
|
|
2521
2582
|
console.log('[crawler] AT:', JSON.stringify(atElements.slice(0, 5)));
|
|
2522
2583
|
// Fire-and-forget keyboard-nav probe — records Tab sequence and focus-indicator issues per screen
|
|
@@ -2912,12 +2973,15 @@ export async function crawlProject(opts: {
|
|
|
2912
2973
|
// P3a: Post-extraction DOM count — detect async-loaded content.
|
|
2913
2974
|
// Only check sparse pages (preDomCount < 25) where async widgets are likely.
|
|
2914
2975
|
// If DOM grew significantly during AI extraction, re-visit later with networkidle.
|
|
2915
|
-
|
|
2976
|
+
const asyncContentCheckMinDom = parseInt(process.env.CRAWLER_ASYNC_CONTENT_MIN_DOM ?? '') || 25;
|
|
2977
|
+
if (preDomCount < asyncContentCheckMinDom && !revisitQueue.includes(url)) {
|
|
2916
2978
|
try {
|
|
2917
2979
|
const postDomCount = await page.evaluate(() =>
|
|
2918
2980
|
document.querySelectorAll('button,a,input,select,textarea,[role="button"],[onclick],[aria-label]').length
|
|
2919
2981
|
).catch(() => 0);
|
|
2920
|
-
|
|
2982
|
+
const asyncGrowthRatio = parseFloat(process.env.CRAWLER_ASYNC_GROWTH_RATIO ?? '') || 1.3;
|
|
2983
|
+
const asyncGrowthFloor = parseInt(process.env.CRAWLER_ASYNC_GROWTH_FLOOR ?? '') || 4;
|
|
2984
|
+
if (postDomCount > preDomCount * asyncGrowthRatio + asyncGrowthFloor) {
|
|
2921
2985
|
revisitQueue.push(url);
|
|
2922
2986
|
logger.info({ url, preDomCount, postDomCount }, '[P3a] Async content detected — scheduled for re-visit');
|
|
2923
2987
|
}
|
|
@@ -2926,7 +2990,7 @@ export async function crawlProject(opts: {
|
|
|
2926
2990
|
|
|
2927
2991
|
// P2: Static JS router detection — scan bundle for declared client-side routes.
|
|
2928
2992
|
// Only run on first 5 pages (bundle is the same across pages; scanning every page is wasteful).
|
|
2929
|
-
if (visited.size <= 5) {
|
|
2993
|
+
if (visited.size <= (parseInt(process.env.CRAWLER_SPA_ROUTER_DETECT_LIMIT ?? '') || 5)) {
|
|
2930
2994
|
try {
|
|
2931
2995
|
const jsDetectedRoutes = await detectJsRoutes(page, origin);
|
|
2932
2996
|
for (const route of jsDetectedRoutes) {
|
|
@@ -2959,7 +3023,7 @@ export async function crawlProject(opts: {
|
|
|
2959
3023
|
// Triggered per-page when: zero DOM links found + fewer than 5 elements (dead-end / canvas page).
|
|
2960
3024
|
// Skyvern returns URLs it discovered → added to queue for ZeTa's own BFS crawl (not saved directly).
|
|
2961
3025
|
// ZeTa does all actual crawling — Skyvern is only a URL-discovery override layer.
|
|
2962
|
-
if (nextLinks.length === 0 && elements.length < 5 && opts.skyvernConfig) {
|
|
3026
|
+
if (nextLinks.length === 0 && elements.length < (parseInt(process.env.CRAWL_SKYVERN_PAGE_MIN_ELEMENTS ?? '') || 5) && opts.skyvernConfig) {
|
|
2963
3027
|
try {
|
|
2964
3028
|
logger.info({ url }, 'Gap 5: thin page detected — asking Skyvern to discover navigation URLs');
|
|
2965
3029
|
const skyvernNav = await skyvernNavigate(
|
|
@@ -3073,7 +3137,8 @@ export async function crawlProject(opts: {
|
|
|
3073
3137
|
// Skyvern is ONLY a URL-discovery layer here — it does NOT save screens directly.
|
|
3074
3138
|
// Use only successfully-saved screens (screenId present), not total discovered including errors.
|
|
3075
3139
|
const successfulScreenCount = discovered.filter(d => d.screenId).length;
|
|
3076
|
-
|
|
3140
|
+
const skyvernMinScreens = parseInt(process.env.CRAWL_SKYVERN_MIN_SCREENS ?? '') || 10;
|
|
3141
|
+
if (successfulScreenCount < skyvernMinScreens && opts.skyvernConfig) {
|
|
3077
3142
|
await onProgress?.(78, 'Few screens found — using Skyvern to discover additional navigation URLs');
|
|
3078
3143
|
const currentUrl = page.url();
|
|
3079
3144
|
const skyvernNav = await skyvernNavigate(
|
|
@@ -3231,7 +3296,7 @@ export async function crawlProject(opts: {
|
|
|
3231
3296
|
// P6: Gap Pass — LLM gap analysis on discovered screens to find structurally missing pages.
|
|
3232
3297
|
// Runs after P3a re-visits so the gap analysis has the most complete screen list possible.
|
|
3233
3298
|
// Confirmed gap URLs (HEAD=200) are visited directly with the existing browser.
|
|
3234
|
-
if (visited.size >= 3) {
|
|
3299
|
+
if (visited.size >= (parseInt(process.env.CRAWLER_GAP_PASS_MIN_SCREENS ?? '') || 3)) {
|
|
3235
3300
|
try {
|
|
3236
3301
|
await onProgress?.(80, 'Running gap analysis…');
|
|
3237
3302
|
const llmCfgForGap = await getCachedCrawlLLM(opts.tenantId);
|
|
@@ -3257,7 +3322,7 @@ export async function crawlProject(opts: {
|
|
|
3257
3322
|
|
|
3258
3323
|
// Visit each confirmed gap URL with the existing browser (depth=1, no link traversal)
|
|
3259
3324
|
const gapLoginPattern = /\/(login|signin|sign-in|auth|log-in)(\/|$|\?)/i;
|
|
3260
|
-
for (const gap of confirmedGaps.slice(0, 8)) {
|
|
3325
|
+
for (const gap of confirmedGaps.slice(0, parseInt(process.env.CRAWLER_GAP_PASS_MAX_VISITS ?? '') || 8)) {
|
|
3261
3326
|
const gapUrl = gap.confirmedUrl!;
|
|
3262
3327
|
if (visited.size >= maxScreens) break;
|
|
3263
3328
|
try {
|
|
@@ -3852,7 +3917,7 @@ async function discoverLinks(stagehand: Stagehand, origin: string, page: any, ll
|
|
|
3852
3917
|
}
|
|
3853
3918
|
}
|
|
3854
3919
|
|
|
3855
|
-
async function domFallbackElements(page: any): Promise<Array<{ meaning: string; role: string; expectedData?: string; notes?: string; boundingRect?: Record<string, number>; ariaState?: Record<string, string>; parentLandmark?: string }>> {
|
|
3920
|
+
export async function domFallbackElements(page: any): Promise<Array<{ meaning: string; role: string; expectedData?: string; notes?: string; boundingRect?: Record<string, number>; ariaState?: Record<string, string>; parentLandmark?: string }>> {
|
|
3856
3921
|
return page.evaluate(() => {
|
|
3857
3922
|
const out: { meaning: string; role: string; expectedData?: string; notes?: string; boundingRect?: Record<string, number>; ariaState?: Record<string, string>; parentLandmark?: string }[] = [];
|
|
3858
3923
|
const seen = new Set<string>();
|
package/src/frontier.ts
CHANGED
|
@@ -27,11 +27,13 @@ function scoreUrl(url: string, keywords: string[]): number {
|
|
|
27
27
|
try {
|
|
28
28
|
const u = new URL(url);
|
|
29
29
|
const segments = u.pathname.split('/').filter(Boolean);
|
|
30
|
-
|
|
30
|
+
const depthPenaltyWeight = parseFloat(process.env.CRAWLER_FRONTIER_DEPTH_PENALTY ?? '') || 1;
|
|
31
|
+
const keywordBoostWeight = parseFloat(process.env.CRAWLER_FRONTIER_KEYWORD_BOOST ?? '') || 10;
|
|
32
|
+
let score = -segments.length * depthPenaltyWeight;
|
|
31
33
|
if (keywords.length > 0) {
|
|
32
34
|
const lower = url.toLowerCase();
|
|
33
35
|
for (const kw of keywords) {
|
|
34
|
-
if (kw && lower.includes(kw)) score +=
|
|
36
|
+
if (kw && lower.includes(kw)) score += keywordBoostWeight;
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
return score;
|
|
@@ -69,7 +71,7 @@ export class CrawlFrontier {
|
|
|
69
71
|
const sparseBoost =
|
|
70
72
|
this.strategy === 'BEST_FIRST' &&
|
|
71
73
|
pushOpts?.parentElements !== undefined &&
|
|
72
|
-
pushOpts.parentElements < 5
|
|
74
|
+
pushOpts.parentElements < (parseInt(process.env.CRAWLER_FRONTIER_SPARSE_PARENT_THRESHOLD ?? '') || 5)
|
|
73
75
|
? 1
|
|
74
76
|
: 0;
|
|
75
77
|
this.items.push({ url, score: this.strategy === 'BEST_FIRST' ? scoreUrl(url, this.keywords) + sparseBoost : 0 });
|
package/src/gap-pass.ts
CHANGED
|
@@ -73,7 +73,7 @@ Respond with ONLY valid JSON, no markdown:
|
|
|
73
73
|
const Anthropic = (await import('@anthropic-ai/sdk')).default;
|
|
74
74
|
const client = new Anthropic({ apiKey: llmCfg.apiKey });
|
|
75
75
|
const response = await client.messages.create({
|
|
76
|
-
model: llmCfg.model ?? 'claude-haiku-4-5-20251001',
|
|
76
|
+
model: llmCfg.model ?? process.env.CRAWLER_DEFAULT_MODEL ?? 'claude-haiku-4-5-20251001',
|
|
77
77
|
max_tokens: 1024,
|
|
78
78
|
temperature: 0,
|
|
79
79
|
// @ts-ignore
|
package/src/http-engine.ts
CHANGED
|
@@ -63,7 +63,12 @@ async function getChromeFetchDispatcher() {
|
|
|
63
63
|
const turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
|
|
64
64
|
|
|
65
65
|
// Pages unlikely to have testable UI — skip from HTTP crawl queue
|
|
66
|
-
const
|
|
66
|
+
const _skipPathExtra = process.env.CRAWL_SKIP_PATH_EXTRA
|
|
67
|
+
? process.env.CRAWL_SKIP_PATH_EXTRA.split(',').map((p) => p.trim()).filter(Boolean)
|
|
68
|
+
: [];
|
|
69
|
+
const SKIP_PATH = _skipPathExtra.length > 0
|
|
70
|
+
? new RegExp(`/(terms|privacy|dpa|legal|cookie|changelog|blog|help|docs|about|contact|careers|press|imprint|gdpr|compliance|acceptable-use|security-policy|accessibility|${_skipPathExtra.join('|')})([-/]|$)`, 'i')
|
|
71
|
+
: /\/(terms|privacy|dpa|legal|cookie|changelog|blog|help|docs|about|contact|careers|press|imprint|gdpr|compliance|acceptable-use|security-policy|accessibility)([-/]|$)/i;
|
|
67
72
|
|
|
68
73
|
export type HttpOutputFormat = 'json' | 'jsonlines' | 'csv' | 'markdown' | 'html';
|
|
69
74
|
|
|
@@ -212,7 +217,7 @@ async function fetchWithBackoff(
|
|
|
212
217
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
213
218
|
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
|
214
219
|
// Chrome-like security headers reduce bot-detection fingerprint
|
|
215
|
-
'sec-ch-ua':
|
|
220
|
+
'sec-ch-ua': `"Google Chrome";v="${process.env.CRAWLER_CHROME_VERSION ?? '120'}", "Chromium";v="${process.env.CRAWLER_CHROME_VERSION ?? '120'}", "Not-A.Brand";v="99"`,
|
|
216
221
|
'sec-ch-ua-mobile': '?0',
|
|
217
222
|
'sec-ch-ua-platform': '"Windows"',
|
|
218
223
|
'Sec-Fetch-Dest': 'document',
|
|
@@ -287,10 +292,10 @@ async function fetchWithBackoff(
|
|
|
287
292
|
|
|
288
293
|
export async function httpCrawl(opts: HttpCrawlOpts): Promise<HttpCrawlResult> {
|
|
289
294
|
const startTime = performance.now();
|
|
290
|
-
const maxScreens = opts.maxScreens ?? 100;
|
|
295
|
+
const maxScreens = opts.maxScreens ?? (parseInt(process.env.CRAWLER_HTTP_ENGINE_MAX_SCREENS ?? '') || 100);
|
|
291
296
|
const cacheDir = opts.cacheDir ?? process.env.CRAWLER_CACHE_DIR ?? '/tmp/zeta-cache';
|
|
292
297
|
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
293
|
-
const CRAWLER_USER_AGENT = process.env.CRAWLER_USER_AGENT ?? 'ZeTa-Crawler/1.0 (+https://
|
|
298
|
+
const CRAWLER_USER_AGENT = process.env.CRAWLER_USER_AGENT ?? 'ZeTa-Crawler/1.0 (+https://zeta.taodigitalsolutions.com/crawler)';
|
|
294
299
|
const userAgent = opts.userAgent ?? CRAWLER_USER_AGENT;
|
|
295
300
|
const cache = new ResponseCache(cacheDir);
|
|
296
301
|
const robots = opts.respectRobotsTxt !== false ? new RobotsCache() : null;
|