@detiq/crawler 0.1.5 → 0.1.7
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 +33 -18
- package/dist/crawler.js.map +2 -2
- package/package.json +1 -1
- package/src/ai-login.ts +14 -1
- package/src/crawler.ts +35 -19
- package/src/skyvern.ts +0 -6
package/package.json
CHANGED
package/src/ai-login.ts
CHANGED
|
@@ -141,7 +141,20 @@ export async function loginWithStagehand(
|
|
|
141
141
|
await logStep('fill_password', async () => {
|
|
142
142
|
const observations = await stagehand.observe('Find the password input field');
|
|
143
143
|
const selector = observations[0]?.selector || 'input[type="password"]';
|
|
144
|
-
|
|
144
|
+
try {
|
|
145
|
+
await pg.locator(selector).fill(creds.password);
|
|
146
|
+
} catch {
|
|
147
|
+
// Stagehand locator failed (e.g. form re-rendered, proxy can't locate element).
|
|
148
|
+
// Fall back to direct DOM fill via evaluate so the password never touches the LLM.
|
|
149
|
+
await pg.evaluate((pwd: string) => {
|
|
150
|
+
const input = document.querySelector('input[type="password"]') as HTMLInputElement | null;
|
|
151
|
+
if (!input) throw new Error('No password field found on page');
|
|
152
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
|
153
|
+
setter?.call(input, pwd);
|
|
154
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
155
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
156
|
+
}, creds.password);
|
|
157
|
+
}
|
|
145
158
|
});
|
|
146
159
|
await logStep('click_submit', () => actWithMouseSim(stagehand, 'Click the sign in, log in, login, or continue button to submit credentials'));
|
|
147
160
|
await pg.waitForLoadState('networkidle').catch(() => pg.waitForTimeout(2000));
|
package/src/crawler.ts
CHANGED
|
@@ -1059,9 +1059,13 @@ export async function crawlProject(opts: {
|
|
|
1059
1059
|
}, state.origins);
|
|
1060
1060
|
}
|
|
1061
1061
|
|
|
1062
|
-
//
|
|
1062
|
+
// Navigate to appUrl with cookies now set.
|
|
1063
|
+
// We MUST navigate to appUrl (not reload current page) because the initial
|
|
1064
|
+
// goto ran without cookies and was redirected to /login. Reloading /login stays
|
|
1065
|
+
// at /login even with a valid cookie (public path, no auth redirect). Navigating
|
|
1066
|
+
// to appUrl lets server middleware see the cookie and route to the authenticated app.
|
|
1063
1067
|
await onProgress?.(12, 'Restoring session');
|
|
1064
|
-
await rawPage.
|
|
1068
|
+
await rawPage.goto(opts.appUrl, { waitUntil: 'domcontentloaded' }).catch(() => { });
|
|
1065
1069
|
await waitForStableDOM(page);
|
|
1066
1070
|
// SPA root pages redirect client-side (useEffect → router.replace). Wait for it.
|
|
1067
1071
|
if (page.url() === opts.appUrl || page.url() === opts.appUrl.replace(/\/$/, '')) {
|
|
@@ -1303,7 +1307,8 @@ export async function crawlProject(opts: {
|
|
|
1303
1307
|
}
|
|
1304
1308
|
let checkpointSuccessCount = 0;
|
|
1305
1309
|
let maxSeenTotal = 0; // track high-water mark so progress % never goes backwards
|
|
1306
|
-
let lastEmittedPct =
|
|
1310
|
+
let lastEmittedPct = 5; // clamp so progress never decreases even when queue grows
|
|
1311
|
+
const crawlLoopStart = Date.now();
|
|
1307
1312
|
const startUrl = page.url();
|
|
1308
1313
|
// Resume from previous segment's pending queue if provided
|
|
1309
1314
|
const resumeUrls = opts.resumeUrls ?? [];
|
|
@@ -2135,7 +2140,13 @@ export async function crawlProject(opts: {
|
|
|
2135
2140
|
|
|
2136
2141
|
// Emit progress now — navigation is done and the page is stable.
|
|
2137
2142
|
// This fires before the 30-90s parallel screenshot+AI phase, preventing a silent gap in the UI.
|
|
2138
|
-
|
|
2143
|
+
// Phase 1 (link discovery): 5–20% over first 60s of wall-clock time so users see movement
|
|
2144
|
+
// before visited.size grows enough to drive the 20–80% phase.
|
|
2145
|
+
const elapsedDiscoverySec = (Date.now() - crawlLoopStart) / 1000;
|
|
2146
|
+
const discoveryPct = Math.round(5 + Math.min(15, (elapsedDiscoverySec / 60) * 15));
|
|
2147
|
+
const preCapturePct = Math.max(lastEmittedPct, discoveryPct);
|
|
2148
|
+
lastEmittedPct = preCapturePct;
|
|
2149
|
+
await onProgress?.(preCapturePct, `Crawling ${visited.size} of ~${visited.size + queue.length} · ${(() => { try { return new URL(url).pathname || '/'; } catch { return '/'; } })()}`);
|
|
2139
2150
|
|
|
2140
2151
|
// P3a: Async content detection — get raw DOM element count before AI extraction.
|
|
2141
2152
|
// The AI extraction takes 30-90s; any async-loaded content will be visible after it completes.
|
|
@@ -2145,6 +2156,7 @@ export async function crawlProject(opts: {
|
|
|
2145
2156
|
).catch(() => 0);
|
|
2146
2157
|
|
|
2147
2158
|
await waitForScreenshotReady(page);
|
|
2159
|
+
await onProgress?.(lastEmittedPct, `Capturing ${(() => { try { return new URL(url).pathname || '/'; } catch { return '/'; } })()}`);
|
|
2148
2160
|
|
|
2149
2161
|
// Parallelize: screenshot + element extraction + link discovery + AT capture (all work on current page state)
|
|
2150
2162
|
const [, elements, nextLinks, atElements] = await Promise.all([
|
|
@@ -2644,9 +2656,10 @@ export async function crawlProject(opts: {
|
|
|
2644
2656
|
|
|
2645
2657
|
discovered.push({ url, screenId: screen.id, elements: elements.length, elementFacts: elements, screenshotPath: shotPath, version: screen.version });
|
|
2646
2658
|
// Progress: 20% baseline + up to 60% across discovered screens.
|
|
2647
|
-
// maxSeenTotal
|
|
2648
|
-
// the
|
|
2649
|
-
|
|
2659
|
+
// Cap maxSeenTotal at maxScreens * 3 so unbounded link discovery does not
|
|
2660
|
+
// collapse the denominator and freeze progress in the 20-22% band.
|
|
2661
|
+
const effectiveMax = isFinite(maxScreens) ? Math.min(visited.size + queue.length, maxScreens * 3) : visited.size + queue.length;
|
|
2662
|
+
maxSeenTotal = Math.max(maxSeenTotal, effectiveMax);
|
|
2650
2663
|
const rawPct = 20 + Math.round(Math.min(60, (visited.size / Math.max(1, maxSeenTotal)) * 60));
|
|
2651
2664
|
const clampedPct = Math.max(lastEmittedPct, rawPct);
|
|
2652
2665
|
lastEmittedPct = clampedPct;
|
|
@@ -4018,24 +4031,27 @@ async function waitForStableDOM(page: any, maxWait = 8_000, stabilityMs = 400):
|
|
|
4018
4031
|
}
|
|
4019
4032
|
|
|
4020
4033
|
async function waitForScreenshotReady(page: any): Promise<void> {
|
|
4034
|
+
const hardDeadline = Date.now() + 20_000;
|
|
4035
|
+
const withHardCap = <T>(p: Promise<T>): Promise<T | void> =>
|
|
4036
|
+
Promise.race([p, new Promise<void>((resolve) => setTimeout(resolve, Math.max(0, hardDeadline - Date.now())))]);
|
|
4021
4037
|
try {
|
|
4022
|
-
await page.waitForLoadState('domcontentloaded', { timeout: 5_000 }).catch(() => { });
|
|
4023
|
-
await page.waitForLoadState('networkidle', { timeout: 12_000 }).catch(() => { });
|
|
4024
|
-
await page.evaluate(() => {
|
|
4038
|
+
await withHardCap(page.waitForLoadState('domcontentloaded', { timeout: 5_000 }).catch(() => { }));
|
|
4039
|
+
await withHardCap(page.waitForLoadState('networkidle', { timeout: 12_000 }).catch(() => { }));
|
|
4040
|
+
await withHardCap(page.evaluate(() => {
|
|
4025
4041
|
if (document.readyState === 'complete') return true;
|
|
4026
4042
|
return new Promise<boolean>((resolve) => {
|
|
4027
4043
|
const done = () => resolve(true);
|
|
4028
4044
|
window.addEventListener('load', done, { once: true });
|
|
4029
4045
|
setTimeout(done, 4_000);
|
|
4030
4046
|
});
|
|
4031
|
-
}).catch(() => { });
|
|
4032
|
-
await page.evaluate(async () => {
|
|
4047
|
+
}).catch(() => { }));
|
|
4048
|
+
await withHardCap(page.evaluate(async () => {
|
|
4033
4049
|
const fonts = (document as any).fonts;
|
|
4034
4050
|
if (fonts?.ready) await fonts.ready;
|
|
4035
|
-
}).catch(() => { });
|
|
4036
|
-
await scrollToTriggerLazyLoad(page);
|
|
4037
|
-
await waitForStableDOM(page,
|
|
4038
|
-
await page.evaluate(async () => {
|
|
4051
|
+
}).catch(() => { }));
|
|
4052
|
+
await withHardCap(scrollToTriggerLazyLoad(page));
|
|
4053
|
+
await withHardCap(waitForStableDOM(page, 4_000, 700));
|
|
4054
|
+
await withHardCap(page.evaluate(async () => {
|
|
4039
4055
|
const pending = Array.from(document.images).filter((img) => !img.complete);
|
|
4040
4056
|
if (pending.length === 0) return;
|
|
4041
4057
|
await Promise.race([
|
|
@@ -4045,9 +4061,9 @@ async function waitForScreenshotReady(page: any): Promise<void> {
|
|
|
4045
4061
|
}))),
|
|
4046
4062
|
new Promise<void>((resolve) => setTimeout(resolve, 4_000)),
|
|
4047
4063
|
]);
|
|
4048
|
-
}).catch(() => { });
|
|
4049
|
-
await waitForStableDOM(page,
|
|
4050
|
-
await page.waitForTimeout(250).catch(() => { });
|
|
4064
|
+
}).catch(() => { }));
|
|
4065
|
+
await withHardCap(waitForStableDOM(page, 2_000, 500));
|
|
4066
|
+
await withHardCap(page.waitForTimeout(250).catch(() => { }));
|
|
4051
4067
|
} catch { /* readiness is best-effort; never fail the crawl for a continuously mutating page */ }
|
|
4052
4068
|
}
|
|
4053
4069
|
|
package/src/skyvern.ts
CHANGED
|
@@ -67,9 +67,6 @@ export async function skyvernNavigate(
|
|
|
67
67
|
return { screenshotUrls: [], visitedUrls: [], status: 'failed' };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
const { assertResolvesToPublicAddress } = await import('@detiq/core');
|
|
71
|
-
await assertResolvesToPublicAddress(apiUrl);
|
|
72
|
-
|
|
73
70
|
const visitedUrls: string[] = [];
|
|
74
71
|
const screenshotUrls: string[] = [];
|
|
75
72
|
const skyvernSteps: SkyvernStep[] = [];
|
|
@@ -164,9 +161,6 @@ export async function skyvernFallback(task: SkyvernTask, tenantConfig?: SkyvernC
|
|
|
164
161
|
return 'failed';
|
|
165
162
|
}
|
|
166
163
|
|
|
167
|
-
const { assertResolvesToPublicAddress } = await import('@detiq/core');
|
|
168
|
-
await assertResolvesToPublicAddress(apiUrl);
|
|
169
|
-
|
|
170
164
|
try {
|
|
171
165
|
const res = await fetch(`${apiUrl}/api/v1/tasks`, {
|
|
172
166
|
method: 'POST',
|