@detiq/crawler 1.1.7 → 1.1.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/accessibility-auditor.js +74 -54
- package/dist/ai-login.js +249 -227
- package/dist/auth-token-rotator.js +67 -57
- package/dist/auth.js +336 -299
- package/dist/browser-extension.js +142 -104
- package/dist/captcha-solver.js +125 -98
- package/dist/color-contrast.js +89 -77
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +71 -54
- package/dist/cookie-consent.js +83 -68
- package/dist/cookie-jar.js +60 -53
- package/dist/crawler.js +5345 -4486
- package/dist/custom-js-injector.js +61 -45
- package/dist/dark-mode-capture.js +81 -67
- package/dist/dead-link-detector.js +55 -47
- package/dist/element-hash.js +12 -8
- package/dist/email-otp.js +195 -154
- package/dist/engine-router.js +98 -87
- package/dist/export/bulk-export.js +55 -70
- package/dist/export/vector-export.js +105 -77
- package/dist/extractors/advanced.d.ts +10 -0
- package/dist/extractors/advanced.js +266 -249
- package/dist/extractors/artifact-envelope.js +58 -59
- package/dist/failure-analysis-agent.js +33 -37
- package/dist/feature-flag-detector.js +66 -51
- package/dist/form-detector.js +75 -55
- package/dist/frontier.js +101 -76
- package/dist/gap-pass.js +89 -68
- package/dist/graphql-detector.js +72 -62
- package/dist/http-engine.js +330 -282
- package/dist/incremental-diff.js +87 -54
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -63
- package/dist/index.js.map +2 -2
- package/dist/input-fuzzer.js +97 -93
- package/dist/interactive-probe.d.ts +36 -0
- package/dist/interactive-probe.js +827 -715
- package/dist/job-queue.js +2169 -2151
- package/dist/js-coverage.js +56 -52
- package/dist/js-router-detector.js +117 -86
- package/dist/language-detector.js +63 -51
- package/dist/link-classifier.js +50 -32
- package/dist/live-view.js +108 -91
- package/dist/locator-extractor.d.ts +37 -0
- package/dist/locator-extractor.js +199 -0
- package/dist/locator-extractor.js.map +7 -0
- package/dist/logger.js +7 -9
- package/dist/markdown-extract.js +61 -48
- package/dist/native-crawler.js +946 -780
- package/dist/network-observer.js +156 -135
- package/dist/network-throttle.js +40 -29
- package/dist/openapi-generator.js +92 -77
- package/dist/pdf-extractor.js +90 -72
- package/dist/performance-metrics.js +102 -80
- package/dist/phash.js +144 -125
- package/dist/pii-scrubber.js +68 -65
- package/dist/playwright-recorder.js +58 -70
- package/dist/policies/crawl-safety.js +34 -31
- package/dist/profiles/domain-profile.js +125 -130
- package/dist/proxy.js +172 -146
- package/dist/rate-limiter.js +65 -58
- package/dist/redirect-tracker.js +64 -60
- package/dist/redis-frontier.js +136 -116
- package/dist/redis-queue.js +144 -157
- package/dist/response-cache.js +140 -127
- package/dist/responsive-detector.js +46 -42
- package/dist/retry-policy.js +50 -46
- package/dist/robots-txt.js +83 -69
- package/dist/screen-reader.js +70 -45
- package/dist/screenshot-masker.js +77 -59
- package/dist/seo-checker.js +107 -88
- package/dist/session-capture.js +212 -192
- package/dist/session-export.js +133 -107
- package/dist/sitemap-writer.js +41 -38
- package/dist/sitemap.js +202 -141
- package/dist/skyvern.js +148 -123
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +90 -74
- package/dist/stealth/human-behavior.js +173 -156
- package/dist/telemetry.js +82 -68
- package/dist/test-executor.js +335 -309
- package/dist/tls-fingerprint.js +88 -77
- package/dist/url-pattern-completer.js +90 -71
- package/dist/visual-diff-verdict.js +38 -23
- package/dist/webhook-notifier.js +69 -71
- package/dist/websocket-detector.js +68 -56
- package/dist/worker.js +38 -29
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/locator-extractor.ts +226 -0
- package/types/index.d.ts +2 -0
- package/types/locator-extractor.d.ts +37 -0
package/dist/live-view.js
CHANGED
|
@@ -1,101 +1,118 @@
|
|
|
1
|
-
import { chromium } from
|
|
2
|
-
import { randomBytes } from
|
|
3
|
-
import { logger } from
|
|
4
|
-
const sessions =
|
|
5
|
-
const SESSION_TTL_MS = 15 * 60 *
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
const sessions = new Map();
|
|
5
|
+
const SESSION_TTL_MS = 15 * 60 * 1_000; // 15 min
|
|
6
6
|
function generateId() {
|
|
7
|
-
|
|
7
|
+
return randomBytes(16).toString('hex');
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Start a live view session: launches a headless browser, navigates to appUrl,
|
|
11
|
+
* begins capturing frames. Returns the session ID.
|
|
12
|
+
*/
|
|
13
|
+
export async function startLiveView(appUrl, storageState) {
|
|
14
|
+
const id = generateId();
|
|
15
|
+
const isDocker = !!process.env.PLAYWRIGHT_IN_DOCKER || process.platform === 'linux';
|
|
16
|
+
const browser = await chromium.launch({
|
|
17
|
+
headless: true,
|
|
18
|
+
args: isDocker ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'] : [],
|
|
19
|
+
});
|
|
20
|
+
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
21
|
+
// Restore session if provided
|
|
22
|
+
if (storageState) {
|
|
23
|
+
try {
|
|
24
|
+
const state = JSON.parse(storageState);
|
|
25
|
+
if (state.cookies?.length)
|
|
26
|
+
await context.addCookies(state.cookies);
|
|
27
|
+
}
|
|
28
|
+
catch { /* ignore */ }
|
|
22
29
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
setTimeout(() => closeLiveView(id).catch(() => {
|
|
48
|
-
}), SESSION_TTL_MS);
|
|
49
|
-
logger.info({ id, appUrl }, "[live-view] Session started");
|
|
50
|
-
return id;
|
|
30
|
+
const page = await context.newPage();
|
|
31
|
+
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
32
|
+
const session = {
|
|
33
|
+
id, browser, context, page, appUrl,
|
|
34
|
+
lastFrame: null, streaming: true, frameInterval: null,
|
|
35
|
+
createdAt: Date.now(),
|
|
36
|
+
};
|
|
37
|
+
// Start capturing frames (every 500ms)
|
|
38
|
+
session.frameInterval = setInterval(async () => {
|
|
39
|
+
if (!session.streaming)
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
const buf = await page.screenshot({ type: 'jpeg', quality: 60, fullPage: false });
|
|
43
|
+
session.lastFrame = buf.toString('base64');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
session.streaming = false;
|
|
47
|
+
}
|
|
48
|
+
}, 500);
|
|
49
|
+
sessions.set(id, session);
|
|
50
|
+
// Auto-cleanup after TTL
|
|
51
|
+
setTimeout(() => closeLiveView(id).catch(() => { }), SESSION_TTL_MS);
|
|
52
|
+
logger.info({ id, appUrl }, '[live-view] Session started');
|
|
53
|
+
return id;
|
|
51
54
|
}
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Get the latest frame for a session (base64 JPEG).
|
|
57
|
+
*/
|
|
58
|
+
export function getFrame(id) {
|
|
59
|
+
return sessions.get(id)?.lastFrame ?? null;
|
|
54
60
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Navigate the live view browser to a URL.
|
|
63
|
+
*/
|
|
64
|
+
export async function liveViewNavigate(id, url) {
|
|
65
|
+
const session = sessions.get(id);
|
|
66
|
+
if (!session)
|
|
67
|
+
throw new Error(`Live view session ${id} not found`);
|
|
68
|
+
await session.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
59
69
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Capture storageState from the live view session, close the browser, return JSON string.
|
|
72
|
+
*/
|
|
73
|
+
export async function completeLiveView(id) {
|
|
74
|
+
const session = sessions.get(id);
|
|
75
|
+
if (!session)
|
|
76
|
+
throw new Error(`Live view session ${id} not found or expired`);
|
|
77
|
+
const state = await session.context.storageState();
|
|
78
|
+
const json = JSON.stringify(state);
|
|
79
|
+
await closeLiveView(id);
|
|
80
|
+
return json;
|
|
67
81
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Send a mouse or keyboard input event to the live view browser.
|
|
84
|
+
*/
|
|
85
|
+
export async function sendInput(id, event) {
|
|
86
|
+
const session = sessions.get(id);
|
|
87
|
+
if (!session)
|
|
88
|
+
throw new Error(`Live view session ${id} not found`);
|
|
89
|
+
if (event.type === 'click' && event.x !== undefined && event.y !== undefined) {
|
|
90
|
+
await session.page.mouse.click(event.x, event.y, { button: event.button ?? 'left' });
|
|
91
|
+
}
|
|
92
|
+
else if (event.type === 'type' && event.text) {
|
|
93
|
+
await session.page.keyboard.type(event.text);
|
|
94
|
+
}
|
|
95
|
+
else if (event.type === 'key' && event.key) {
|
|
96
|
+
await session.page.keyboard.press(event.key);
|
|
97
|
+
}
|
|
78
98
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Close a live view session without capturing.
|
|
101
|
+
*/
|
|
102
|
+
export async function closeLiveView(id) {
|
|
103
|
+
const session = sessions.get(id);
|
|
104
|
+
if (!session)
|
|
105
|
+
return;
|
|
106
|
+
session.streaming = false;
|
|
107
|
+
if (session.frameInterval)
|
|
108
|
+
clearInterval(session.frameInterval);
|
|
109
|
+
sessions.delete(id);
|
|
110
|
+
await session.browser.close().catch(() => { });
|
|
111
|
+
logger.info({ id }, '[live-view] Session closed');
|
|
88
112
|
}
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
/**
|
|
114
|
+
* List all active session IDs (for health checks / cleanup).
|
|
115
|
+
*/
|
|
116
|
+
export function listLiveViews() {
|
|
117
|
+
return [...sessions.keys()];
|
|
91
118
|
}
|
|
92
|
-
export {
|
|
93
|
-
closeLiveView,
|
|
94
|
-
completeLiveView,
|
|
95
|
-
getFrame,
|
|
96
|
-
listLiveViews,
|
|
97
|
-
liveViewNavigate,
|
|
98
|
-
sendInput,
|
|
99
|
-
startLiveView
|
|
100
|
-
};
|
|
101
|
-
//# sourceMappingURL=live-view.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts raw DB Element records (role, meaning, notes JSON) into Playwright-idiomatic locator strings.
|
|
3
|
+
* Priority: data-testid → aria-label/placeholder (inputs) → role+name (buttons/links) → heading → text → id
|
|
4
|
+
*/
|
|
5
|
+
export interface PlaywrightLocator {
|
|
6
|
+
key: string;
|
|
7
|
+
expression: string;
|
|
8
|
+
elementType: string;
|
|
9
|
+
confidence: 'high' | 'medium' | 'low';
|
|
10
|
+
source: 'testid' | 'label' | 'placeholder' | 'role-name' | 'heading' | 'text' | 'id';
|
|
11
|
+
description: string;
|
|
12
|
+
}
|
|
13
|
+
/** Convert a single DB Element record to a Playwright locator. Returns null if insufficient data. */
|
|
14
|
+
export declare function elementToPlaywrightLocator(element: {
|
|
15
|
+
role?: string | null;
|
|
16
|
+
meaning?: string | null;
|
|
17
|
+
text?: string | null;
|
|
18
|
+
notes?: unknown;
|
|
19
|
+
selector?: string | null;
|
|
20
|
+
}): PlaywrightLocator | null;
|
|
21
|
+
/**
|
|
22
|
+
* Convert an array of DB Element records to deduplicated Playwright locators.
|
|
23
|
+
* Deduplicates by key — higher confidence wins.
|
|
24
|
+
*/
|
|
25
|
+
export declare function elementsToPlaywrightLocators(elements: Array<{
|
|
26
|
+
role?: string | null;
|
|
27
|
+
meaning?: string | null;
|
|
28
|
+
text?: string | null;
|
|
29
|
+
notes?: unknown;
|
|
30
|
+
selector?: string | null;
|
|
31
|
+
isInteractive?: boolean | null;
|
|
32
|
+
}>): PlaywrightLocator[];
|
|
33
|
+
/**
|
|
34
|
+
* Format locators as a block for injection into LLM script generation prompts.
|
|
35
|
+
* Returns empty string if no locators.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatLocatorsForPrompt(locators: PlaywrightLocator[]): string;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts raw DB Element records (role, meaning, notes JSON) into Playwright-idiomatic locator strings.
|
|
3
|
+
* Priority: data-testid → aria-label/placeholder (inputs) → role+name (buttons/links) → heading → text → id
|
|
4
|
+
*/
|
|
5
|
+
/** Parse the notes JSON string stored on Element records. */
|
|
6
|
+
function parseNotes(notes) {
|
|
7
|
+
if (!notes)
|
|
8
|
+
return {};
|
|
9
|
+
try {
|
|
10
|
+
const parsed = typeof notes === 'string' ? JSON.parse(notes) : notes;
|
|
11
|
+
if (parsed && typeof parsed === 'object')
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
catch { }
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
/** Map crawler role strings to Playwright role values. */
|
|
18
|
+
function mapRole(role) {
|
|
19
|
+
if (!role)
|
|
20
|
+
return null;
|
|
21
|
+
const r = role.toLowerCase().trim();
|
|
22
|
+
if (r === 'button' || r === 'submit' || r === 'reset')
|
|
23
|
+
return 'button';
|
|
24
|
+
if (r === 'link' || r === 'a' || r === 'anchor')
|
|
25
|
+
return 'link';
|
|
26
|
+
if (r === 'textbox' || r === 'input' || r === 'text' || r === 'email' || r === 'password' || r === 'tel' || r === 'number' || r === 'search')
|
|
27
|
+
return 'textbox';
|
|
28
|
+
if (r === 'combobox' || r === 'select' || r === 'dropdown')
|
|
29
|
+
return 'combobox';
|
|
30
|
+
if (r === 'checkbox')
|
|
31
|
+
return 'checkbox';
|
|
32
|
+
if (r === 'radio')
|
|
33
|
+
return 'radio';
|
|
34
|
+
if (r === 'heading' || r === 'h1' || r === 'h2' || r === 'h3' || r === 'h4' || r === 'h5' || r === 'h6')
|
|
35
|
+
return 'heading';
|
|
36
|
+
if (r === 'img' || r === 'image')
|
|
37
|
+
return 'img';
|
|
38
|
+
if (r === 'nav' || r === 'navigation')
|
|
39
|
+
return 'navigation';
|
|
40
|
+
return r;
|
|
41
|
+
}
|
|
42
|
+
/** Convert "Submit Form Button" → "submitFormButton" */
|
|
43
|
+
function toKey(text) {
|
|
44
|
+
return text
|
|
45
|
+
.replace(/[^a-zA-Z0-9\s]/g, ' ')
|
|
46
|
+
.replace(/\s+(.)/g, (_, c) => c.toUpperCase())
|
|
47
|
+
.replace(/^(.)/, (_, c) => c.toLowerCase())
|
|
48
|
+
.replace(/\s/g, '')
|
|
49
|
+
.slice(0, 40)
|
|
50
|
+
|| 'element';
|
|
51
|
+
}
|
|
52
|
+
/** Convert a single DB Element record to a Playwright locator. Returns null if insufficient data. */
|
|
53
|
+
export function elementToPlaywrightLocator(element) {
|
|
54
|
+
const attrs = parseNotes(element.notes);
|
|
55
|
+
const testId = attrs['data-testid'];
|
|
56
|
+
const ariaLabel = attrs['aria-label'] || element.meaning;
|
|
57
|
+
const id = attrs['id'];
|
|
58
|
+
const placeholder = attrs['placeholder'];
|
|
59
|
+
const name = attrs['name'];
|
|
60
|
+
const text = (element.text ?? '').trim();
|
|
61
|
+
const meaning = (element.meaning ?? '').trim();
|
|
62
|
+
const role = mapRole(element.role);
|
|
63
|
+
const label = meaning || text || testId || ariaLabel || placeholder || id || name || 'element';
|
|
64
|
+
const key = toKey(label);
|
|
65
|
+
// Priority 1: data-testid
|
|
66
|
+
if (testId) {
|
|
67
|
+
return {
|
|
68
|
+
key,
|
|
69
|
+
expression: `page.getByTestId(${JSON.stringify(testId)})`,
|
|
70
|
+
elementType: role ?? 'element',
|
|
71
|
+
confidence: 'high',
|
|
72
|
+
source: 'testid',
|
|
73
|
+
description: meaning || testId,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Priority 2: input/combobox/textarea with label → getByLabel; with placeholder → getByPlaceholder
|
|
77
|
+
if (role === 'textbox' || role === 'combobox') {
|
|
78
|
+
if (ariaLabel) {
|
|
79
|
+
return {
|
|
80
|
+
key,
|
|
81
|
+
expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
|
|
82
|
+
elementType: 'input',
|
|
83
|
+
confidence: 'high',
|
|
84
|
+
source: 'label',
|
|
85
|
+
description: ariaLabel,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (placeholder) {
|
|
89
|
+
return {
|
|
90
|
+
key,
|
|
91
|
+
expression: `page.getByPlaceholder(${JSON.stringify(placeholder)})`,
|
|
92
|
+
elementType: 'input',
|
|
93
|
+
confidence: 'high',
|
|
94
|
+
source: 'placeholder',
|
|
95
|
+
description: placeholder,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Priority 3: checkbox/radio → getByLabel
|
|
100
|
+
if ((role === 'checkbox' || role === 'radio') && ariaLabel) {
|
|
101
|
+
return {
|
|
102
|
+
key,
|
|
103
|
+
expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,
|
|
104
|
+
elementType: role,
|
|
105
|
+
confidence: 'high',
|
|
106
|
+
source: 'label',
|
|
107
|
+
description: ariaLabel,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Priority 4: button/link with meaningful name → getByRole
|
|
111
|
+
if ((role === 'button' || role === 'link') && meaning) {
|
|
112
|
+
return {
|
|
113
|
+
key,
|
|
114
|
+
expression: `page.getByRole(${JSON.stringify(role)}, { name: ${JSON.stringify(meaning)} })`,
|
|
115
|
+
elementType: role,
|
|
116
|
+
confidence: 'high',
|
|
117
|
+
source: 'role-name',
|
|
118
|
+
description: meaning,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Priority 5: heading → getByRole heading
|
|
122
|
+
if (role === 'heading' && (meaning || text)) {
|
|
123
|
+
const name2 = meaning || text;
|
|
124
|
+
return {
|
|
125
|
+
key,
|
|
126
|
+
expression: `page.getByRole('heading', { name: ${JSON.stringify(name2)} })`,
|
|
127
|
+
elementType: 'heading',
|
|
128
|
+
confidence: 'medium',
|
|
129
|
+
source: 'heading',
|
|
130
|
+
description: name2,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Priority 6: existing CSS selector from crawler
|
|
134
|
+
if (element.selector && element.selector.length > 0) {
|
|
135
|
+
return {
|
|
136
|
+
key,
|
|
137
|
+
expression: `page.locator(${JSON.stringify(element.selector)})`,
|
|
138
|
+
elementType: role ?? 'element',
|
|
139
|
+
confidence: 'medium',
|
|
140
|
+
source: 'id',
|
|
141
|
+
description: meaning || text || element.selector,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Priority 7: id from attrs
|
|
145
|
+
if (id) {
|
|
146
|
+
return {
|
|
147
|
+
key,
|
|
148
|
+
expression: `page.locator(${JSON.stringify('#' + id)})`,
|
|
149
|
+
elementType: role ?? 'element',
|
|
150
|
+
confidence: 'medium',
|
|
151
|
+
source: 'id',
|
|
152
|
+
description: meaning || id,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// Priority 8: fallback to text content
|
|
156
|
+
if (meaning || text) {
|
|
157
|
+
const t = meaning || text;
|
|
158
|
+
return {
|
|
159
|
+
key,
|
|
160
|
+
expression: `page.getByText(${JSON.stringify(t)}, { exact: false })`,
|
|
161
|
+
elementType: role ?? 'text',
|
|
162
|
+
confidence: 'low',
|
|
163
|
+
source: 'text',
|
|
164
|
+
description: t,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Convert an array of DB Element records to deduplicated Playwright locators.
|
|
171
|
+
* Deduplicates by key — higher confidence wins.
|
|
172
|
+
*/
|
|
173
|
+
export function elementsToPlaywrightLocators(elements) {
|
|
174
|
+
const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2 };
|
|
175
|
+
const byKey = new Map();
|
|
176
|
+
for (const el of elements) {
|
|
177
|
+
const loc = elementToPlaywrightLocator(el);
|
|
178
|
+
if (!loc)
|
|
179
|
+
continue;
|
|
180
|
+
const existing = byKey.get(loc.key);
|
|
181
|
+
if (!existing || CONFIDENCE_RANK[loc.confidence] < CONFIDENCE_RANK[existing.confidence]) {
|
|
182
|
+
byKey.set(loc.key, loc);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return Array.from(byKey.values());
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Format locators as a block for injection into LLM script generation prompts.
|
|
189
|
+
* Returns empty string if no locators.
|
|
190
|
+
*/
|
|
191
|
+
export function formatLocatorsForPrompt(locators) {
|
|
192
|
+
if (locators.length === 0)
|
|
193
|
+
return '';
|
|
194
|
+
const lines = locators.map(l => ` ${l.key}: ${l.expression} // ${l.description} [${l.confidence}]`);
|
|
195
|
+
return [
|
|
196
|
+
'EXTRACTED PAGE LOCATORS (use these EXACTLY — do not invent other selectors; these were verified against the live page):',
|
|
197
|
+
...lines,
|
|
198
|
+
].join('\n');
|
|
199
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/locator-extractor.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Converts raw DB Element records (role, meaning, notes JSON) into Playwright-idiomatic locator strings.\n * Priority: data-testid \u2192 aria-label/placeholder (inputs) \u2192 role+name (buttons/links) \u2192 heading \u2192 text \u2192 id\n */\n\nexport interface PlaywrightLocator {\n key: string; // camelCase property name for page object\n expression: string; // e.g. \"page.getByRole('button', { name: 'Submit' })\"\n elementType: string; // button | input | link | heading | text | element\n confidence: 'high' | 'medium' | 'low';\n source: 'testid' | 'label' | 'placeholder' | 'role-name' | 'heading' | 'text' | 'id';\n description: string; // human-readable label\n}\n\n/** Parse the notes JSON string stored on Element records. */\nfunction parseNotes(notes: unknown): Record<string, string> {\n if (!notes) return {};\n try {\n const parsed = typeof notes === 'string' ? JSON.parse(notes) : notes;\n if (parsed && typeof parsed === 'object') return parsed as Record<string, string>;\n } catch {}\n return {};\n}\n\n/** Map crawler role strings to Playwright role values. */\nfunction mapRole(role?: string | null): string | null {\n if (!role) return null;\n const r = role.toLowerCase().trim();\n if (r === 'button' || r === 'submit' || r === 'reset') return 'button';\n if (r === 'link' || r === 'a' || r === 'anchor') return 'link';\n if (r === 'textbox' || r === 'input' || r === 'text' || r === 'email' || r === 'password' || r === 'tel' || r === 'number' || r === 'search') return 'textbox';\n if (r === 'combobox' || r === 'select' || r === 'dropdown') return 'combobox';\n if (r === 'checkbox') return 'checkbox';\n if (r === 'radio') return 'radio';\n if (r === 'heading' || r === 'h1' || r === 'h2' || r === 'h3' || r === 'h4' || r === 'h5' || r === 'h6') return 'heading';\n if (r === 'img' || r === 'image') return 'img';\n if (r === 'nav' || r === 'navigation') return 'navigation';\n return r;\n}\n\n/** Convert \"Submit Form Button\" \u2192 \"submitFormButton\" */\nfunction toKey(text: string): string {\n return text\n .replace(/[^a-zA-Z0-9\\s]/g, ' ')\n .replace(/\\s+(.)/g, (_, c) => c.toUpperCase())\n .replace(/^(.)/, (_, c) => c.toLowerCase())\n .replace(/\\s/g, '')\n .slice(0, 40)\n || 'element';\n}\n\n/** Convert a single DB Element record to a Playwright locator. Returns null if insufficient data. */\nexport function elementToPlaywrightLocator(element: {\n role?: string | null;\n meaning?: string | null;\n text?: string | null;\n notes?: unknown;\n selector?: string | null;\n}): PlaywrightLocator | null {\n const attrs = parseNotes(element.notes);\n const testId = attrs['data-testid'];\n const ariaLabel = attrs['aria-label'] || element.meaning;\n const id = attrs['id'];\n const placeholder = attrs['placeholder'];\n const name = attrs['name'];\n const text = (element.text ?? '').trim();\n const meaning = (element.meaning ?? '').trim();\n const role = mapRole(element.role);\n\n const label = meaning || text || testId || ariaLabel || placeholder || id || name || 'element';\n const key = toKey(label);\n\n // Priority 1: data-testid\n if (testId) {\n return {\n key,\n expression: `page.getByTestId(${JSON.stringify(testId)})`,\n elementType: role ?? 'element',\n confidence: 'high',\n source: 'testid',\n description: meaning || testId,\n };\n }\n\n // Priority 2: input/combobox/textarea with label \u2192 getByLabel; with placeholder \u2192 getByPlaceholder\n if (role === 'textbox' || role === 'combobox') {\n if (ariaLabel) {\n return {\n key,\n expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,\n elementType: 'input',\n confidence: 'high',\n source: 'label',\n description: ariaLabel,\n };\n }\n if (placeholder) {\n return {\n key,\n expression: `page.getByPlaceholder(${JSON.stringify(placeholder)})`,\n elementType: 'input',\n confidence: 'high',\n source: 'placeholder',\n description: placeholder,\n };\n }\n }\n\n // Priority 3: checkbox/radio \u2192 getByLabel\n if ((role === 'checkbox' || role === 'radio') && ariaLabel) {\n return {\n key,\n expression: `page.getByLabel(${JSON.stringify(ariaLabel)})`,\n elementType: role,\n confidence: 'high',\n source: 'label',\n description: ariaLabel,\n };\n }\n\n // Priority 4: button/link with meaningful name \u2192 getByRole\n if ((role === 'button' || role === 'link') && meaning) {\n return {\n key,\n expression: `page.getByRole(${JSON.stringify(role)}, { name: ${JSON.stringify(meaning)} })`,\n elementType: role,\n confidence: 'high',\n source: 'role-name',\n description: meaning,\n };\n }\n\n // Priority 5: heading \u2192 getByRole heading\n if (role === 'heading' && (meaning || text)) {\n const name2 = meaning || text;\n return {\n key,\n expression: `page.getByRole('heading', { name: ${JSON.stringify(name2)} })`,\n elementType: 'heading',\n confidence: 'medium',\n source: 'heading',\n description: name2,\n };\n }\n\n // Priority 6: existing CSS selector from crawler\n if (element.selector && element.selector.length > 0) {\n return {\n key,\n expression: `page.locator(${JSON.stringify(element.selector)})`,\n elementType: role ?? 'element',\n confidence: 'medium',\n source: 'id',\n description: meaning || text || element.selector,\n };\n }\n\n // Priority 7: id from attrs\n if (id) {\n return {\n key,\n expression: `page.locator(${JSON.stringify('#' + id)})`,\n elementType: role ?? 'element',\n confidence: 'medium',\n source: 'id',\n description: meaning || id,\n };\n }\n\n // Priority 8: fallback to text content\n if (meaning || text) {\n const t = meaning || text;\n return {\n key,\n expression: `page.getByText(${JSON.stringify(t)}, { exact: false })`,\n elementType: role ?? 'text',\n confidence: 'low',\n source: 'text',\n description: t,\n };\n }\n\n return null;\n}\n\n/**\n * Convert an array of DB Element records to deduplicated Playwright locators.\n * Deduplicates by key \u2014 higher confidence wins.\n */\nexport function elementsToPlaywrightLocators(elements: Array<{\n role?: string | null;\n meaning?: string | null;\n text?: string | null;\n notes?: unknown;\n selector?: string | null;\n isInteractive?: boolean | null;\n}>): PlaywrightLocator[] {\n const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2 };\n const byKey = new Map<string, PlaywrightLocator>();\n\n for (const el of elements) {\n const loc = elementToPlaywrightLocator(el);\n if (!loc) continue;\n const existing = byKey.get(loc.key);\n if (!existing || CONFIDENCE_RANK[loc.confidence] < CONFIDENCE_RANK[existing.confidence]) {\n byKey.set(loc.key, loc);\n }\n }\n\n return Array.from(byKey.values());\n}\n\n/**\n * Format locators as a block for injection into LLM script generation prompts.\n * Returns empty string if no locators.\n */\nexport function formatLocatorsForPrompt(locators: PlaywrightLocator[]): string {\n if (locators.length === 0) return '';\n const lines = locators.map(l =>\n ` ${l.key}: ${l.expression} // ${l.description} [${l.confidence}]`\n );\n return [\n 'EXTRACTED PAGE LOCATORS (use these EXACTLY \u2014 do not invent other selectors; these were verified against the live page):',\n ...lines,\n ].join('\\n');\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/logger.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import pino from
|
|
2
|
-
const logger = pino({
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
export const logger = pino({
|
|
3
|
+
level: process.env.LOG_LEVEL ?? 'info',
|
|
4
|
+
...(process.env.NODE_ENV === 'development'
|
|
5
|
+
? { transport: { target: 'pino-pretty', options: { colorize: true } } }
|
|
6
|
+
: {}),
|
|
7
|
+
base: { service: 'crawler' },
|
|
6
8
|
});
|
|
7
|
-
export {
|
|
8
|
-
logger
|
|
9
|
-
};
|
|
10
|
-
//# sourceMappingURL=logger.js.map
|
package/dist/markdown-extract.js
CHANGED
|
@@ -1,54 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* HTML → Markdown extraction for crawled pages — matches crawl4AI's markdown
|
|
3
|
+
* output capability. Two-tier, mirroring crawl4AI's "markdown" vs "fit markdown":
|
|
4
|
+
*
|
|
5
|
+
* - "fit" markdown: Readability's best guess at the primary content (title +
|
|
6
|
+
* article body), stripped of nav/ads/chrome. Ideal for content/marketing
|
|
7
|
+
* pages and RAG chunking. null when Readability finds no article (most
|
|
8
|
+
* app UI screens — dashboards, forms — legitimately have none).
|
|
9
|
+
* - "full" markdown: the whole visible page converted straight to Markdown,
|
|
10
|
+
* always present. The fallback for app screens where there's no "article".
|
|
11
|
+
*
|
|
12
|
+
* Runs entirely on an already-captured `page.content()` string — no extra
|
|
13
|
+
* network fetch, no extra page navigation.
|
|
14
|
+
*/
|
|
15
|
+
import { parseHTML } from 'linkedom';
|
|
16
|
+
import { Readability } from '@mozilla/readability';
|
|
17
|
+
import TurndownService from 'turndown';
|
|
18
|
+
const turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
|
|
19
|
+
turndown.remove(['script', 'style', 'noscript', 'svg', 'iframe']);
|
|
6
20
|
function stripNoiseElements(document) {
|
|
7
|
-
|
|
8
|
-
|
|
21
|
+
const noisy = document.querySelectorAll('script, style, noscript, svg, iframe, [aria-hidden="true"]');
|
|
22
|
+
noisy.forEach((el) => el.remove());
|
|
9
23
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const { document } = parseHTML(html);
|
|
13
|
-
stripNoiseElements(document);
|
|
14
|
-
let readabilityResult = null;
|
|
24
|
+
/** Best-effort HTML→Markdown for one crawled page. Never throws. */
|
|
25
|
+
export function extractMarkdown(html, url) {
|
|
15
26
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
const { document } = parseHTML(html);
|
|
28
|
+
stripNoiseElements(document);
|
|
29
|
+
let readabilityResult = null;
|
|
30
|
+
try {
|
|
31
|
+
// Readability mutates the DOM it's given — clone via re-parsing so the
|
|
32
|
+
// full-page fallback below still has an intact document.
|
|
33
|
+
const { document: readerDoc } = parseHTML(html);
|
|
34
|
+
stripNoiseElements(readerDoc);
|
|
35
|
+
readabilityResult = new Readability(readerDoc, { charThreshold: 50 }).parse();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
readabilityResult = null;
|
|
39
|
+
}
|
|
40
|
+
if (readabilityResult?.content && readabilityResult.textContent.trim().length >= 50) {
|
|
41
|
+
const markdown = turndown.turndown(readabilityResult.content);
|
|
42
|
+
return {
|
|
43
|
+
markdown: `# ${readabilityResult.title || document.title || url}\n\n${markdown}`,
|
|
44
|
+
method: 'readability',
|
|
45
|
+
title: readabilityResult.title || document.title || null,
|
|
46
|
+
wordCount: readabilityResult.textContent.trim().split(/\s+/).length,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const body = document.body;
|
|
50
|
+
if (!body)
|
|
51
|
+
return null;
|
|
52
|
+
const markdown = turndown.turndown(body.innerHTML || '');
|
|
53
|
+
const text = markdown.trim();
|
|
54
|
+
if (!text)
|
|
55
|
+
return null;
|
|
56
|
+
const title = document.title || null;
|
|
57
|
+
return {
|
|
58
|
+
markdown: title ? `# ${title}\n\n${markdown}` : markdown,
|
|
59
|
+
method: 'full-page',
|
|
60
|
+
title,
|
|
61
|
+
wordCount: text.split(/\s+/).length,
|
|
62
|
+
};
|
|
21
63
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
markdown: `# ${readabilityResult.title || document.title || url}
|
|
26
|
-
|
|
27
|
-
${markdown2}`,
|
|
28
|
-
method: "readability",
|
|
29
|
-
title: readabilityResult.title || document.title || null,
|
|
30
|
-
wordCount: readabilityResult.textContent.trim().split(/\s+/).length
|
|
31
|
-
};
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
32
66
|
}
|
|
33
|
-
const body = document.body;
|
|
34
|
-
if (!body) return null;
|
|
35
|
-
const markdown = turndown.turndown(body.innerHTML || "");
|
|
36
|
-
const text = markdown.trim();
|
|
37
|
-
if (!text) return null;
|
|
38
|
-
const title = document.title || null;
|
|
39
|
-
return {
|
|
40
|
-
markdown: title ? `# ${title}
|
|
41
|
-
|
|
42
|
-
${markdown}` : markdown,
|
|
43
|
-
method: "full-page",
|
|
44
|
-
title,
|
|
45
|
-
wordCount: text.split(/\s+/).length
|
|
46
|
-
};
|
|
47
|
-
} catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
67
|
}
|
|
51
|
-
export {
|
|
52
|
-
extractMarkdown
|
|
53
|
-
};
|
|
54
|
-
//# sourceMappingURL=markdown-extract.js.map
|