@detiq/crawler 1.1.23 → 1.1.25
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 +173 -28
- package/dist/crawler.js.map +2 -2
- package/dist/extractors/branding-extractor.js +104 -0
- package/dist/extractors/branding-extractor.js.map +7 -0
- package/dist/extractors/summary-extractor.js +36 -0
- package/dist/extractors/summary-extractor.js.map +7 -0
- package/dist/frontier.js +20 -2
- package/dist/frontier.js.map +2 -2
- package/dist/incremental-diff.js +76 -0
- package/dist/incremental-diff.js.map +2 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +2 -2
- package/dist/interactive-probe.js +108 -17
- package/dist/interactive-probe.js.map +2 -2
- package/dist/job-queue.js +408 -61
- package/dist/job-queue.js.map +2 -2
- package/dist/sqs-queue.js +78 -10
- package/dist/sqs-queue.js.map +2 -2
- package/dist/worker.js +7 -7
- package/dist/worker.js.map +2 -2
- package/package.json +1 -1
- package/src/crawler.ts +208 -30
- package/src/extractors/branding-extractor.ts +159 -0
- package/src/extractors/summary-extractor.ts +49 -0
- package/src/frontier.ts +31 -1
- package/src/incremental-diff.ts +138 -0
- package/src/index.ts +1 -1
- package/src/interactive-probe.ts +129 -20
- package/src/job-queue.ts +489 -63
- package/src/sqs-queue.ts +92 -12
- package/src/worker.ts +13 -11
- package/types/crawler.d.ts +20 -0
- package/types/extractors/branding-extractor.d.ts +46 -0
- package/types/extractors/summary-extractor.d.ts +10 -0
- package/types/frontier.d.ts +9 -1
- package/types/incremental-diff.d.ts +57 -0
- package/types/index.d.ts +1 -1
- package/types/interactive-probe.d.ts +5 -1
- package/types/job-queue.d.ts +53 -0
- package/types/sqs-queue.d.ts +12 -1
package/dist/crawler.js
CHANGED
|
@@ -54,7 +54,7 @@ import { enumerateDropdowns } from "./dropdown-enumerator.js";
|
|
|
54
54
|
import { probeCrudFlows } from "./crud-interaction.js";
|
|
55
55
|
import { crawlFiltersAndPagination } from "./filter-pagination-crawler.js";
|
|
56
56
|
import { probeAdvancedUI } from "./advanced-ui-probes.js";
|
|
57
|
-
import { ScreenTransitionGraph } from "./state-graph.js";
|
|
57
|
+
import { ScreenTransitionGraph, simHash32 } from "./state-graph.js";
|
|
58
58
|
import { crawlNestedContexts } from "./nested-window-crawler.js";
|
|
59
59
|
import { crawlShadowDom } from "./shadow-dom-crawler.js";
|
|
60
60
|
import { crawlAccessibilityTree } from "./accessibility-tree-crawler.js";
|
|
@@ -86,6 +86,7 @@ function getGitInfo() {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
const ALLOWED_NUMERIC_PATHS = /* @__PURE__ */ new Set(["/2fa", "/404", "/500", "/403", "/401"]);
|
|
89
|
+
let _activeSeedPatterns = [];
|
|
89
90
|
const UUID_PATH_SEGMENT_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
90
91
|
const CUID_PATH_SEGMENT_RE = /^c[a-z0-9]{20,}$/i;
|
|
91
92
|
const NUMERIC_ID_PATH_SEGMENT_RE = /^\d{4,}$/;
|
|
@@ -110,7 +111,7 @@ function isUsefulNavigationUrl(href, siteOrigin) {
|
|
|
110
111
|
const u = new URL(href);
|
|
111
112
|
if (!sameSite(u.toString(), siteOrigin)) return false;
|
|
112
113
|
if (/\/(logout|logged-out|signout|sign-out)(\/|$|\?)/i.test(u.pathname)) return false;
|
|
113
|
-
if (isGeneratedDynamicPath(u.pathname)) return false;
|
|
114
|
+
if (isGeneratedDynamicPath(u.pathname) && !_activeSeedPatterns.some((re) => re.test(u.pathname))) return false;
|
|
114
115
|
if (DOWNLOAD_EXT_RE.test(u.pathname)) return false;
|
|
115
116
|
if (API_PATH_RE.test(u.pathname)) return false;
|
|
116
117
|
if (EXPORT_PATH_RE.test(u.pathname)) return false;
|
|
@@ -119,6 +120,13 @@ function isUsefulNavigationUrl(href, siteOrigin) {
|
|
|
119
120
|
return false;
|
|
120
121
|
}
|
|
121
122
|
}
|
|
123
|
+
function buildSeedPatternRegexes(patterns) {
|
|
124
|
+
return patterns.map((p) => {
|
|
125
|
+
const parts = p.split("{id}").map((part) => part.replace(/[.+^${}()|[\]\\]/g, "\\$&"));
|
|
126
|
+
const body = parts.join("[^/]+");
|
|
127
|
+
return new RegExp(`^${body}(/.*)?$`, "i");
|
|
128
|
+
});
|
|
129
|
+
}
|
|
122
130
|
const AUTH_DISCOVERY_MAX_SCREENS = parseInt(process.env.CRAWLER_AUTH_DISCOVERY_MAX_SCREENS ?? "") || 36;
|
|
123
131
|
const AUTH_DISCOVERY_MAX_QUEUE = parseInt(process.env.CRAWLER_AUTH_DISCOVERY_MAX_QUEUE ?? "") || 48;
|
|
124
132
|
const AUTH_DISCOVERY_MAX_CLICKS_PER_PAGE = parseInt(process.env.CRAWLER_AUTH_DISCOVERY_MAX_CLICKS_PER_PAGE ?? "") || 6;
|
|
@@ -244,9 +252,10 @@ async function capturePageHtml(page) {
|
|
|
244
252
|
}
|
|
245
253
|
return html;
|
|
246
254
|
}
|
|
247
|
-
async function launchChromeCDP(execPath, extraArgs, proxy) {
|
|
255
|
+
async function launchChromeCDP(execPath, extraArgs, proxy, useStealthPlugin = true) {
|
|
248
256
|
const port = 9200 + Math.floor(Math.random() * 700);
|
|
249
|
-
const
|
|
257
|
+
const launcher = useStealthPlugin ? stealthChromium : chromium;
|
|
258
|
+
const browserServer = await launcher.launchServer({
|
|
250
259
|
headless: true,
|
|
251
260
|
executablePath: execPath,
|
|
252
261
|
// Use Playwright's native proxy option — it handles 407 auth challenges automatically.
|
|
@@ -377,6 +386,7 @@ const TwoFASchema = z.object({
|
|
|
377
386
|
const INLINE_AUTH_WALL_RE = /your login session (was|has been) expired|session (has )?expired[^.]*click|please (log ?in|sign ?in) to continue|you (are|were) logged out|your session has timed out/i;
|
|
378
387
|
async function crawlProject(opts, onProgress) {
|
|
379
388
|
const tuning = await getCrawlTuning();
|
|
389
|
+
_activeSeedPatterns = buildSeedPatternRegexes(opts.seedPathPatterns ?? []);
|
|
380
390
|
if (opts.screenshotDir) await fs.mkdir(opts.screenshotDir, { recursive: true });
|
|
381
391
|
const uploadToCloud = async (localPath, key) => {
|
|
382
392
|
if (!opts.storageConfig) return void 0;
|
|
@@ -643,7 +653,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
643
653
|
logger.warn({ err: foxErr }, "[crawler] Camoufox bootstrap failed \u2014 continuing without it");
|
|
644
654
|
}
|
|
645
655
|
}
|
|
646
|
-
const { wsUrl: cdpUrl, proc: chromeProc } = await launchChromeCDP(executablePath, chromeArgs, playwrightProxy);
|
|
656
|
+
const { wsUrl: cdpUrl, proc: chromeProc } = await launchChromeCDP(executablePath, chromeArgs, playwrightProxy, opts.stealth !== false);
|
|
647
657
|
let stagehandPromptTokens = 0;
|
|
648
658
|
let stagehandCompletionTokens = 0;
|
|
649
659
|
const stagehandLogger = (line) => {
|
|
@@ -1444,7 +1454,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
1444
1454
|
...sitemapUrls.filter((u) => canonicalize(u) !== canonicalize(startUrl))
|
|
1445
1455
|
];
|
|
1446
1456
|
const queued = new Set(seedUrls.map(canonicalize));
|
|
1447
|
-
const queue = new CrawlFrontier(opts.traversalStrategy ?? "BFS", seedUrls, opts.bestFirstKeywords, opts.urlFilter);
|
|
1457
|
+
const queue = new CrawlFrontier(opts.traversalStrategy ?? "BFS", seedUrls, opts.bestFirstKeywords, opts.urlFilter, opts.includePatterns, opts.excludePatterns);
|
|
1448
1458
|
if (selective) {
|
|
1449
1459
|
queue.push = () => {
|
|
1450
1460
|
};
|
|
@@ -1467,7 +1477,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
1467
1477
|
const { pathname } = new URL(u);
|
|
1468
1478
|
const stripWww = (h) => h.replace(/^www\./i, "").toLowerCase();
|
|
1469
1479
|
if (stripWww(new URL(u).hostname) !== stripWww(new URL(startUrl).hostname)) return false;
|
|
1470
|
-
if (isGeneratedDynamicPath(pathname)) return false;
|
|
1480
|
+
if (isGeneratedDynamicPath(pathname) && !_activeSeedPatterns.some((re) => re.test(pathname))) return false;
|
|
1471
1481
|
if (!isUsefulNavigationUrl(u, startUrl)) return false;
|
|
1472
1482
|
const canon = canonicalize(u);
|
|
1473
1483
|
if (seenReseedCanon.has(canon)) return false;
|
|
@@ -1629,7 +1639,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
1629
1639
|
if (visited.has(canonUrl)) continue;
|
|
1630
1640
|
try {
|
|
1631
1641
|
const { pathname } = new URL(url);
|
|
1632
|
-
if (SKIP_PATH.test(pathname) || isGeneratedDynamicPath(pathname)) continue;
|
|
1642
|
+
if (SKIP_PATH.test(pathname) || isGeneratedDynamicPath(pathname) && !_activeSeedPatterns.some((re) => re.test(pathname))) continue;
|
|
1633
1643
|
if (API_PATH_RE.test(pathname) || EXPORT_PATH_RE.test(pathname) || DOWNLOAD_EXT_RE.test(pathname)) continue;
|
|
1634
1644
|
} catch {
|
|
1635
1645
|
continue;
|
|
@@ -1985,7 +1995,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
1985
1995
|
console.log(`[crawler] skip content page: ${url}`);
|
|
1986
1996
|
continue;
|
|
1987
1997
|
}
|
|
1988
|
-
if (isGeneratedDynamicPath(pathname)) {
|
|
1998
|
+
if (isGeneratedDynamicPath(pathname) && !_activeSeedPatterns.some((re) => re.test(pathname))) {
|
|
1989
1999
|
console.log(`[crawler] skip generated dynamic route: ${url}`);
|
|
1990
2000
|
continue;
|
|
1991
2001
|
}
|
|
@@ -2724,10 +2734,24 @@ async function crawlProject(opts, onProgress) {
|
|
|
2724
2734
|
// Always run AI link discovery (even when cache exists) — cache is merged in, not used as a replacement.
|
|
2725
2735
|
// Skipping AI when cache exists was a bug: stale cache from broken crawls permanently blocked discovery.
|
|
2726
2736
|
// Note: preDomCount < aiMinElements guard removed — it was backwards: sparse pages (SPAs) need AI most.
|
|
2727
|
-
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, aiSkipCount > 0, rawPage ?? void 0).then((
|
|
2737
|
+
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, aiSkipCount > 0, rawPage ?? void 0).then((metas) => {
|
|
2738
|
+
if (metas.length > 0) {
|
|
2739
|
+
AppBrain.saveDiscoveredLinks(opts.tenantId, opts.projectId, screen.id, metas.map((m) => ({
|
|
2740
|
+
href: m.url,
|
|
2741
|
+
text: m.text,
|
|
2742
|
+
landmark: m.landmark,
|
|
2743
|
+
linkType: "NAV_LINK",
|
|
2744
|
+
source: m.source,
|
|
2745
|
+
jobId: opts.jobId
|
|
2746
|
+
}))).catch(() => {
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
return metas.map((m) => m.url);
|
|
2750
|
+
}).then((freshLinks) => {
|
|
2728
2751
|
if (aiSkipCount > 0) aiSkipCount--;
|
|
2729
2752
|
aiErrorStreak = 0;
|
|
2730
|
-
const
|
|
2753
|
+
const freshUrls = freshLinks;
|
|
2754
|
+
const merged = (cachedLinks ? Array.from(/* @__PURE__ */ new Set([...cachedLinks, ...freshUrls])) : freshUrls).filter((href) => isUsefulNavigationUrl(href, origin));
|
|
2731
2755
|
logger.info({ url, domLinks: freshLinks.length, cached: cachedLinks?.length ?? 0, merged: merged.length }, "[crawler] link-discovery: result");
|
|
2732
2756
|
if (merged.length > 0) {
|
|
2733
2757
|
AppBrain.saveActionRecipe(opts.tenantId, opts.projectId, {
|
|
@@ -2741,7 +2765,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
2741
2765
|
}
|
|
2742
2766
|
return merged;
|
|
2743
2767
|
}).catch((err) => {
|
|
2744
|
-
const fallback = err?.domLinks ?? [];
|
|
2768
|
+
const fallback = (err?.domLinks ?? []).map((l) => l.url);
|
|
2745
2769
|
if (err?.isRateLimit) {
|
|
2746
2770
|
aiErrorStreak++;
|
|
2747
2771
|
if (aiErrorStreak >= 5) {
|
|
@@ -3269,7 +3293,7 @@ ${snippet}`,
|
|
|
3269
3293
|
if (!queued.has(canonHref) && !visited.has(canonHref) && queued.size < maxScreens * 3) {
|
|
3270
3294
|
try {
|
|
3271
3295
|
const hrefPath = new URL(href).pathname;
|
|
3272
|
-
if (isGeneratedDynamicPath(hrefPath)) continue;
|
|
3296
|
+
if (isGeneratedDynamicPath(hrefPath) && !_activeSeedPatterns.some((re) => re.test(hrefPath))) continue;
|
|
3273
3297
|
} catch {
|
|
3274
3298
|
}
|
|
3275
3299
|
if (robotsCache && !await robotsCache.isAllowed(href)) continue;
|
|
@@ -3473,7 +3497,20 @@ ${snippet}`,
|
|
|
3473
3497
|
}
|
|
3474
3498
|
const [elems, links, linkMappings] = await Promise.all([
|
|
3475
3499
|
extractElements(stagehand, page, elementLLM ?? void 0, rawPage ?? void 0),
|
|
3476
|
-
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, aiSkipCount > 0, rawPage ?? void 0)
|
|
3500
|
+
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, aiSkipCount > 0, rawPage ?? void 0).then((metas) => {
|
|
3501
|
+
if (metas.length > 0) {
|
|
3502
|
+
AppBrain.saveDiscoveredLinks(opts.tenantId, opts.projectId, screen.id, metas.map((m) => ({
|
|
3503
|
+
href: m.url,
|
|
3504
|
+
text: m.text,
|
|
3505
|
+
landmark: m.landmark,
|
|
3506
|
+
linkType: "NAV_LINK",
|
|
3507
|
+
source: m.source,
|
|
3508
|
+
jobId: opts.jobId
|
|
3509
|
+
}))).catch(() => {
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
return metas.map((m) => m.url);
|
|
3513
|
+
}),
|
|
3477
3514
|
extractLinkMappings(page, origin)
|
|
3478
3515
|
]);
|
|
3479
3516
|
for (const el of elems) {
|
|
@@ -3485,7 +3522,8 @@ ${snippet}`,
|
|
|
3485
3522
|
for (const lm of linkMappings) {
|
|
3486
3523
|
pendingTransitions.push({ fromScreenId: screen.id, elementMeaning: lm.label, targetUrl: lm.href });
|
|
3487
3524
|
}
|
|
3488
|
-
for (const
|
|
3525
|
+
for (const link of links) {
|
|
3526
|
+
const href = link;
|
|
3489
3527
|
const c = canonicalize(href);
|
|
3490
3528
|
if (isUsefulNavigationUrl(href, origin) && !queued.has(c) && !visited.has(c)) {
|
|
3491
3529
|
queued.add(c);
|
|
@@ -3629,7 +3667,20 @@ ${snippet}`,
|
|
|
3629
3667
|
});
|
|
3630
3668
|
}
|
|
3631
3669
|
}
|
|
3632
|
-
const revisitLinks = await discoverLinks(stagehand, origin, page, linkLLM ?? void 0, false, rawPage ?? void 0).
|
|
3670
|
+
const revisitLinks = await discoverLinks(stagehand, origin, page, linkLLM ?? void 0, false, rawPage ?? void 0).then((metas) => {
|
|
3671
|
+
if (metas.length > 0) {
|
|
3672
|
+
AppBrain.saveDiscoveredLinks(opts.tenantId, opts.projectId, existingScreenId, metas.map((m) => ({
|
|
3673
|
+
href: m.url,
|
|
3674
|
+
text: m.text,
|
|
3675
|
+
landmark: m.landmark,
|
|
3676
|
+
linkType: "NAV_LINK",
|
|
3677
|
+
source: m.source,
|
|
3678
|
+
jobId: opts.jobId
|
|
3679
|
+
}))).catch(() => {
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
return metas.map((m) => m.url);
|
|
3683
|
+
}).catch(() => []);
|
|
3633
3684
|
for (const href of revisitLinks) {
|
|
3634
3685
|
const canon = canonicalize(href);
|
|
3635
3686
|
if (isUsefulNavigationUrl(href, origin) && !queued.has(canon) && !visited.has(canon) && queued.size < maxScreens * 3) {
|
|
@@ -3680,7 +3731,20 @@ ${snippet}`,
|
|
|
3680
3731
|
}
|
|
3681
3732
|
const [p3bElems, p3bLinks] = await Promise.all([
|
|
3682
3733
|
extractElements(stagehand, page, elementLLM ?? void 0, rawPage ?? void 0).catch(() => []),
|
|
3683
|
-
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, aiSkipCount > 0, rawPage ?? void 0).
|
|
3734
|
+
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, aiSkipCount > 0, rawPage ?? void 0).then((metas) => {
|
|
3735
|
+
if (metas.length > 0) {
|
|
3736
|
+
AppBrain.saveDiscoveredLinks(opts.tenantId, opts.projectId, screen.id, metas.map((m) => ({
|
|
3737
|
+
href: m.url,
|
|
3738
|
+
text: m.text,
|
|
3739
|
+
landmark: m.landmark,
|
|
3740
|
+
linkType: "NAV_LINK",
|
|
3741
|
+
source: m.source,
|
|
3742
|
+
jobId: opts.jobId
|
|
3743
|
+
}))).catch(() => {
|
|
3744
|
+
});
|
|
3745
|
+
}
|
|
3746
|
+
return metas.map((m) => m.url);
|
|
3747
|
+
}).catch(() => [])
|
|
3684
3748
|
]);
|
|
3685
3749
|
if (p3bElems.length > 0) {
|
|
3686
3750
|
await AppBrain.clearScreenElements(opts.tenantId, opts.projectId, screen.id).catch(() => {
|
|
@@ -3760,7 +3824,20 @@ ${snippet}`,
|
|
|
3760
3824
|
}
|
|
3761
3825
|
const [gapElems, gapLinks, gapLinkMaps] = await Promise.all([
|
|
3762
3826
|
extractElements(stagehand, page, elementLLM ?? void 0, rawPage ?? void 0).catch(() => []),
|
|
3763
|
-
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, false, rawPage ?? void 0).
|
|
3827
|
+
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, false, rawPage ?? void 0).then((metas) => {
|
|
3828
|
+
if (metas.length > 0) {
|
|
3829
|
+
AppBrain.saveDiscoveredLinks(opts.tenantId, opts.projectId, gapScreen.id, metas.map((m) => ({
|
|
3830
|
+
href: m.url,
|
|
3831
|
+
text: m.text,
|
|
3832
|
+
landmark: m.landmark,
|
|
3833
|
+
linkType: "NAV_LINK",
|
|
3834
|
+
source: m.source,
|
|
3835
|
+
jobId: opts.jobId
|
|
3836
|
+
}))).catch(() => {
|
|
3837
|
+
});
|
|
3838
|
+
}
|
|
3839
|
+
return metas.map((m) => m.url);
|
|
3840
|
+
}).catch(() => []),
|
|
3764
3841
|
extractLinkMappings(page, origin).catch(() => [])
|
|
3765
3842
|
]);
|
|
3766
3843
|
for (const el of gapElems) {
|
|
@@ -3882,6 +3959,23 @@ ${snippet}`,
|
|
|
3882
3959
|
}
|
|
3883
3960
|
}).length;
|
|
3884
3961
|
logger.info({ transitionCount }, "KAG: wired element\u2192screen transitions");
|
|
3962
|
+
if (opts.jobId) {
|
|
3963
|
+
const hrefToScreenId = /* @__PURE__ */ new Map();
|
|
3964
|
+
for (const [canonUrl, screenId] of visited) {
|
|
3965
|
+
if (screenId && screenId !== "_pending_" && screenId !== "_error_") {
|
|
3966
|
+
hrefToScreenId.set(canonUrl, screenId);
|
|
3967
|
+
try {
|
|
3968
|
+
const u = new URL(canonUrl);
|
|
3969
|
+
hrefToScreenId.set(u.href, screenId);
|
|
3970
|
+
hrefToScreenId.set(u.origin + u.pathname, screenId);
|
|
3971
|
+
hrefToScreenId.set(u.origin + u.pathname.replace(/\/$/, ""), screenId);
|
|
3972
|
+
} catch {
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
AppBrain.resolveDiscoveredLinkTargets(opts.tenantId, opts.projectId, opts.jobId, hrefToScreenId).catch(() => {
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3885
3979
|
AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
3886
3980
|
jobId: opts.jobId,
|
|
3887
3981
|
type: "WORKFLOW_GRAPH_SUMMARY",
|
|
@@ -4052,11 +4146,33 @@ ${snippet}`,
|
|
|
4052
4146
|
if (shouldRunInteractiveProbe) {
|
|
4053
4147
|
await onProgress?.(88, "Running interactive probe (capturing real errors & modals)");
|
|
4054
4148
|
const probeInputs = discovered.filter((d) => d.screenId).map((d) => ({ url: d.url, screenId: d.screenId, elements: d.elementFacts ?? [] }));
|
|
4055
|
-
await probeAllScreens(page, opts.tenantId, opts.projectId, probeInputs, onProgress);
|
|
4149
|
+
await probeAllScreens(page, opts.tenantId, opts.projectId, probeInputs, onProgress, opts.jobId);
|
|
4056
4150
|
}
|
|
4057
4151
|
try {
|
|
4058
4152
|
await onProgress?.(90, "Generating state graph coverage report");
|
|
4059
4153
|
const stateGraph = new ScreenTransitionGraph();
|
|
4154
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4155
|
+
for (const d of discovered) {
|
|
4156
|
+
if (!d.screenId || !d.url) continue;
|
|
4157
|
+
const hashVal = simHash32(d.url);
|
|
4158
|
+
stateGraph.addState({
|
|
4159
|
+
id: d.screenId,
|
|
4160
|
+
url: d.url,
|
|
4161
|
+
normalizedUrl: d.url,
|
|
4162
|
+
title: d.url,
|
|
4163
|
+
interactiveElementCount: d.elements ?? 0,
|
|
4164
|
+
domFingerprint: hashVal.toString(16).padStart(8, "0"),
|
|
4165
|
+
simHashValue: hashVal,
|
|
4166
|
+
visitedAt: now
|
|
4167
|
+
});
|
|
4168
|
+
}
|
|
4169
|
+
for (const edge of edges) {
|
|
4170
|
+
const fromId = visited.get(canonicalize(edge.fromUrl));
|
|
4171
|
+
const toId = visited.get(canonicalize(edge.toUrl));
|
|
4172
|
+
if (fromId && toId && fromId !== "_pending_" && fromId !== "_error_" && toId !== "_pending_" && toId !== "_error_") {
|
|
4173
|
+
stateGraph.addTransition(fromId, toId, "navigate", edge.toUrl, now);
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4060
4176
|
const coveragePayload = {
|
|
4061
4177
|
stateGraph: stateGraph.getCoverageReport(),
|
|
4062
4178
|
totalScreensCrawled: discovered.filter((d) => !!d.screenId).length
|
|
@@ -4313,7 +4429,8 @@ async function extractElements(stagehand, page, llmCfg, rawPage) {
|
|
|
4313
4429
|
async function discoverLinks(stagehand, origin, page, llmCfg, skipAi = false, rawPage) {
|
|
4314
4430
|
const navExpanded = await expandNavMenus(page);
|
|
4315
4431
|
const domLinks = await domFallbackLinks(page, origin, rawPage);
|
|
4316
|
-
const
|
|
4432
|
+
const interactiveRaw = await interactAndCollectLinks(page, origin);
|
|
4433
|
+
const interactiveLinks = interactiveRaw.map((url) => ({ url, source: "INTERACTIVE" }));
|
|
4317
4434
|
if (navExpanded) {
|
|
4318
4435
|
page.keyboard?.press("Escape").catch(() => {
|
|
4319
4436
|
});
|
|
@@ -4327,7 +4444,14 @@ async function discoverLinks(stagehand, origin, page, llmCfg, skipAi = false, ra
|
|
|
4327
4444
|
}).catch(() => {
|
|
4328
4445
|
});
|
|
4329
4446
|
}
|
|
4330
|
-
const
|
|
4447
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
4448
|
+
const allDomLinks = [];
|
|
4449
|
+
for (const link of [...domLinks, ...interactiveLinks]) {
|
|
4450
|
+
if (!seenUrls.has(link.url)) {
|
|
4451
|
+
seenUrls.add(link.url);
|
|
4452
|
+
allDomLinks.push(link);
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4331
4455
|
if (allDomLinks.length >= 50 || skipAi) return allDomLinks;
|
|
4332
4456
|
try {
|
|
4333
4457
|
const extracted = await Promise.race([
|
|
@@ -4351,8 +4475,16 @@ async function discoverLinks(stagehand, origin, page, llmCfg, skipAi = false, ra
|
|
|
4351
4475
|
}).filter((u) => {
|
|
4352
4476
|
if (!u) return false;
|
|
4353
4477
|
return isUsefulNavigationUrl(u, origin);
|
|
4354
|
-
});
|
|
4355
|
-
|
|
4478
|
+
}).map((url) => ({ url, source: "AI" }));
|
|
4479
|
+
const merged = [...allDomLinks];
|
|
4480
|
+
const mergedUrls = new Set(merged.map((l) => l.url));
|
|
4481
|
+
for (const link of aiLinks) {
|
|
4482
|
+
if (!mergedUrls.has(link.url) && isUsefulNavigationUrl(link.url, origin)) {
|
|
4483
|
+
mergedUrls.add(link.url);
|
|
4484
|
+
merged.push(link);
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
return merged.filter((l) => isUsefulNavigationUrl(l.url, origin));
|
|
4356
4488
|
} catch (err) {
|
|
4357
4489
|
const is429 = err?.status === 429 || String(err?.message ?? "").includes("429") || String(err?.message ?? "").includes("rate limit");
|
|
4358
4490
|
if (is429) throw Object.assign(new Error("RATE_LIMITED"), { isRateLimit: true, domLinks: allDomLinks });
|
|
@@ -5442,7 +5574,7 @@ async function domFallbackLinks(page, origin, rawPage) {
|
|
|
5442
5574
|
function _linkEval(originStr) {
|
|
5443
5575
|
const logout = /logout|log out|sign out|delete/i;
|
|
5444
5576
|
const isRouteHash = (hash) => /^#!?\//.test(hash);
|
|
5445
|
-
const seen = /* @__PURE__ */ new
|
|
5577
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5446
5578
|
const add = (rawHref, textContent) => {
|
|
5447
5579
|
if (logout.test(textContent)) return;
|
|
5448
5580
|
if (!rawHref) return;
|
|
@@ -5451,7 +5583,7 @@ async function domFallbackLinks(page, origin, rawPage) {
|
|
|
5451
5583
|
try {
|
|
5452
5584
|
const u = new URL(rawHref, location.href);
|
|
5453
5585
|
if (!isRouteHash(u.hash)) u.hash = "";
|
|
5454
|
-
if (new URL(u).origin === originStr) seen.
|
|
5586
|
+
if (new URL(u).origin === originStr) seen.set(u.toString(), (textContent || "").trim().slice(0, 120));
|
|
5455
5587
|
} catch {
|
|
5456
5588
|
}
|
|
5457
5589
|
};
|
|
@@ -5485,7 +5617,20 @@ async function domFallbackLinks(page, origin, rawPage) {
|
|
|
5485
5617
|
} catch (_) {
|
|
5486
5618
|
}
|
|
5487
5619
|
}
|
|
5488
|
-
return Array.from(seen);
|
|
5620
|
+
return Array.from(seen.entries()).map(([url, text]) => ({ url, text }));
|
|
5621
|
+
}
|
|
5622
|
+
function inferLandmark(url) {
|
|
5623
|
+
const p = url.toLowerCase();
|
|
5624
|
+
if (p.includes("/auth") || p.includes("/login") || p.includes("/register")) return "nav";
|
|
5625
|
+
return void 0;
|
|
5626
|
+
}
|
|
5627
|
+
function toMeta(raw) {
|
|
5628
|
+
return raw.map(({ url, text }) => ({
|
|
5629
|
+
url,
|
|
5630
|
+
text: text || void 0,
|
|
5631
|
+
landmark: inferLandmark(url),
|
|
5632
|
+
source: "DOM"
|
|
5633
|
+
}));
|
|
5489
5634
|
}
|
|
5490
5635
|
const _linkEvalStr = `(function(){const __name=(fn,n)=>{try{Object.defineProperty(fn,'name',{value:n,configurable:true})}catch(e){}return fn};return(${_linkEval.toString()})(${JSON.stringify(origin)})})();`;
|
|
5491
5636
|
if (rawPage && typeof rawPage.context === "function") {
|
|
@@ -5500,7 +5645,7 @@ async function domFallbackLinks(page, origin, rawPage) {
|
|
|
5500
5645
|
awaitPromise: false
|
|
5501
5646
|
});
|
|
5502
5647
|
if (!evalResult?.exceptionDetails && Array.isArray(evalResult?.result?.value)) {
|
|
5503
|
-
return evalResult.result.value;
|
|
5648
|
+
return toMeta(evalResult.result.value);
|
|
5504
5649
|
}
|
|
5505
5650
|
if (evalResult?.exceptionDetails) {
|
|
5506
5651
|
console.warn("[crawler] domFallbackLinks CDP error:", evalResult.exceptionDetails?.exception?.description ?? evalResult.exceptionDetails?.text);
|
|
@@ -5515,7 +5660,7 @@ async function domFallbackLinks(page, origin, rawPage) {
|
|
|
5515
5660
|
}
|
|
5516
5661
|
}
|
|
5517
5662
|
const evalPage = rawPage ?? page;
|
|
5518
|
-
return evalPage.evaluate(_linkEvalStr).catch((err) => {
|
|
5663
|
+
return evalPage.evaluate(_linkEvalStr).then(toMeta).catch((err) => {
|
|
5519
5664
|
console.warn("[crawler] domFallbackLinks page.evaluate threw:", err?.message ?? String(err));
|
|
5520
5665
|
return [];
|
|
5521
5666
|
});
|