@detiq/crawler 0.2.6 → 0.2.9
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 +61 -43
- package/dist/crawler.js.map +2 -2
- package/package.json +5 -2
- package/src/ai-login.ts +62 -5
- package/src/crawler.ts +2 -2
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@detiq/crawler",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"private": false,
|
|
5
|
-
"publishConfig": {
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public",
|
|
7
|
+
"registry": "https://registry.npmjs.org/"
|
|
8
|
+
},
|
|
6
9
|
"type": "module",
|
|
7
10
|
"main": "dist/index.js",
|
|
8
11
|
"types": "types/index.d.ts",
|
package/src/ai-login.ts
CHANGED
|
@@ -9,6 +9,14 @@ import { simulateMouseMovement } from './stealth/human-behavior.js';
|
|
|
9
9
|
|
|
10
10
|
const CLICK_INTENT = /\b(click|tap|press|submit|select|choose|toggle)\b/i;
|
|
11
11
|
|
|
12
|
+
/** Stagehand V3 enforces elementId as /^\d+-\d+$/ — LLM sometimes returns bare integers.
|
|
13
|
+
* Catch the resulting AI_NoObjectGeneratedError so callers can fall back to DOM. */
|
|
14
|
+
function isNoObjectGeneratedError(err: any): boolean {
|
|
15
|
+
const name = String(err?.name ?? '');
|
|
16
|
+
const msg = String(err?.message ?? '');
|
|
17
|
+
return name.includes('NoObjectGenerated') || msg.includes('NoObjectGenerated') || msg.includes('No object generated');
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
/** Simulate human mouse movement before click-type actions, then call stagehand.act(). */
|
|
13
21
|
async function actWithMouseSim(stagehand: Stagehand, instruction: string): Promise<any> {
|
|
14
22
|
if (CLICK_INTENT.test(instruction)) {
|
|
@@ -131,11 +139,29 @@ export async function loginWithStagehand(
|
|
|
131
139
|
throw new Error(`loginInstructions exceeds ${MAX_LOGIN_INSTRUCTIONS} character limit`);
|
|
132
140
|
}
|
|
133
141
|
if (loginInstructions) {
|
|
134
|
-
await logStep('custom_recipe', () =>
|
|
142
|
+
await logStep('custom_recipe', async () => {
|
|
143
|
+
try {
|
|
144
|
+
return await actWithMouseSim(stagehand, loginInstructions);
|
|
145
|
+
} catch (err: any) {
|
|
146
|
+
if (!isNoObjectGeneratedError(err)) throw err;
|
|
147
|
+
logger.warn('[crawler] custom_recipe act() failed — Stagehand V3 elementId schema error, skipping to standard fill');
|
|
148
|
+
return { description: 'custom_recipe skipped (schema error)' };
|
|
149
|
+
}
|
|
150
|
+
});
|
|
135
151
|
await pg.waitForLoadState('networkidle').catch(() => pg.waitForTimeout(1500));
|
|
136
152
|
}
|
|
137
153
|
|
|
138
|
-
await logStep('fill_username', () =>
|
|
154
|
+
await logStep('fill_username', async () => {
|
|
155
|
+
try {
|
|
156
|
+
return await actWithMouseSim(stagehand, `Type "${creds.username}" into the username or email input field`);
|
|
157
|
+
} catch (err: any) {
|
|
158
|
+
if (!isNoObjectGeneratedError(err)) throw err;
|
|
159
|
+
logger.warn('[crawler] fill_username act() failed — falling back to DOM');
|
|
160
|
+
const sel = 'input[type="email"], input[autocomplete="email"], input[autocomplete="username"], input[name*="email" i], input[name*="user" i], input[placeholder*="email" i], input[placeholder*="user" i], input[id*="email" i], input[id*="user" i]';
|
|
161
|
+
await pg.locator(sel).first().fill(creds.username);
|
|
162
|
+
return { description: 'fill_username via DOM fallback' };
|
|
163
|
+
}
|
|
164
|
+
});
|
|
139
165
|
// Don't send password to LLM — use direct Playwright fill to avoid
|
|
140
166
|
// leaking credentials into the Stagehand/LLM provider's request logs.
|
|
141
167
|
await logStep('fill_password', async () => {
|
|
@@ -156,7 +182,18 @@ export async function loginWithStagehand(
|
|
|
156
182
|
}, creds.password);
|
|
157
183
|
}
|
|
158
184
|
});
|
|
159
|
-
await logStep('click_submit', () =>
|
|
185
|
+
await logStep('click_submit', async () => {
|
|
186
|
+
try {
|
|
187
|
+
return await actWithMouseSim(stagehand, 'Click the sign in, log in, login, or continue button to submit credentials');
|
|
188
|
+
} catch (err: any) {
|
|
189
|
+
if (!isNoObjectGeneratedError(err)) throw err;
|
|
190
|
+
logger.warn('[crawler] click_submit act() failed — falling back to DOM');
|
|
191
|
+
const sel = 'button[type="submit"], input[type="submit"], button:has-text("Sign in"), button:has-text("Log in"), button:has-text("Login"), button:has-text("Continue"), button:has-text("Next"), button:has-text("Sign In"), [role="button"]:has-text("Sign in"), [role="button"]:has-text("Login")';
|
|
192
|
+
await pg.locator(sel).first().click();
|
|
193
|
+
await pg.waitForTimeout(500);
|
|
194
|
+
return { description: 'click_submit via DOM fallback' };
|
|
195
|
+
}
|
|
196
|
+
});
|
|
160
197
|
await pg.waitForLoadState('networkidle').catch(() => pg.waitForTimeout(2000));
|
|
161
198
|
|
|
162
199
|
const twoFA = await stagehand.extract(
|
|
@@ -200,8 +237,28 @@ export async function loginWithStagehand(
|
|
|
200
237
|
console.warn('[crawler] 2FA detected but no oneTimeCode provided');
|
|
201
238
|
return false;
|
|
202
239
|
}
|
|
203
|
-
await logStep('2fa_inject', () =>
|
|
204
|
-
|
|
240
|
+
await logStep('2fa_inject', async () => {
|
|
241
|
+
try {
|
|
242
|
+
return await actWithMouseSim(stagehand, `Enter "${code}" into the ${twoFA.fieldLabel ?? 'verification code'} field`);
|
|
243
|
+
} catch (err: any) {
|
|
244
|
+
if (!isNoObjectGeneratedError(err)) throw err;
|
|
245
|
+
logger.warn('[crawler] 2fa_inject act() failed — falling back to DOM');
|
|
246
|
+
const sel = 'input[autocomplete="one-time-code"], input[name*="otp" i], input[name*="code" i], input[name*="token" i], input[inputmode="numeric"], input[type="number"]';
|
|
247
|
+
await pg.locator(sel).first().fill(code as string);
|
|
248
|
+
return { description: '2fa_inject via DOM fallback' };
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
await logStep('2fa_submit', async () => {
|
|
252
|
+
try {
|
|
253
|
+
return await actWithMouseSim(stagehand, 'Click verify or confirm to submit the verification code');
|
|
254
|
+
} catch (err: any) {
|
|
255
|
+
if (!isNoObjectGeneratedError(err)) throw err;
|
|
256
|
+
logger.warn('[crawler] 2fa_submit act() failed — falling back to DOM');
|
|
257
|
+
const sel = 'button[type="submit"], button:has-text("Verify"), button:has-text("Confirm"), button:has-text("Submit"), button:has-text("Continue")';
|
|
258
|
+
await pg.locator(sel).first().click();
|
|
259
|
+
return { description: '2fa_submit via DOM fallback' };
|
|
260
|
+
}
|
|
261
|
+
});
|
|
205
262
|
await pg.waitForLoadState('networkidle').catch(() => pg.waitForTimeout(2000));
|
|
206
263
|
}
|
|
207
264
|
|
package/src/crawler.ts
CHANGED
|
@@ -1802,7 +1802,7 @@ export async function crawlProject(opts: {
|
|
|
1802
1802
|
type: 'AUTH_FAILURE',
|
|
1803
1803
|
payload: { url, landed: landedUrl, reason: 'Re-authentication failed. Session is unrecoverable. Re-capture the session in project settings and retry the crawl.' },
|
|
1804
1804
|
}).catch(() => { });
|
|
1805
|
-
|
|
1805
|
+
throw new Error('[AUTH_FAILURE] Re-authentication failed. Session is unrecoverable. Re-capture the session in project settings and retry the crawl.');
|
|
1806
1806
|
}
|
|
1807
1807
|
} else {
|
|
1808
1808
|
logger.warn('[crawler] Session expired mid-crawl, no credentials to recover — stopping BFS');
|
|
@@ -1811,7 +1811,7 @@ export async function crawlProject(opts: {
|
|
|
1811
1811
|
type: 'AUTH_FAILURE',
|
|
1812
1812
|
payload: { url, landed: landedUrl, reason: 'Session expired and no credentials are configured. Add credentials in project settings and re-capture the session.' },
|
|
1813
1813
|
}).catch(() => { });
|
|
1814
|
-
|
|
1814
|
+
throw new Error('[AUTH_FAILURE] Session expired and no credentials are configured. Add credentials in project settings and re-capture the session.');
|
|
1815
1815
|
}
|
|
1816
1816
|
}
|
|
1817
1817
|
// If weWantedThisPage (intentional /login, /session-expired, /404 navigation) —
|