@detiq/crawler 0.1.3
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/Dockerfile +49 -0
- package/README.md +11 -0
- package/dist/accessibility-auditor.d.ts +29 -0
- package/dist/accessibility-auditor.js +57 -0
- package/dist/accessibility-auditor.js.map +7 -0
- package/dist/ai-login.d.ts +32 -0
- package/dist/ai-login.js +177 -0
- package/dist/ai-login.js.map +7 -0
- package/dist/auth-token-rotator.d.ts +47 -0
- package/dist/auth-token-rotator.js +58 -0
- package/dist/auth-token-rotator.js.map +7 -0
- package/dist/auth.d.ts +20 -0
- package/dist/auth.js +329 -0
- package/dist/auth.js.map +7 -0
- package/dist/browser-extension.d.ts +37 -0
- package/dist/browser-extension.js +113 -0
- package/dist/browser-extension.js.map +7 -0
- package/dist/captcha-solver.d.ts +16 -0
- package/dist/captcha-solver.js +105 -0
- package/dist/captcha-solver.js.map +7 -0
- package/dist/color-contrast.d.ts +30 -0
- package/dist/color-contrast.js +78 -0
- package/dist/color-contrast.js.map +7 -0
- package/dist/content-chunker.d.ts +18 -0
- package/dist/content-chunker.js +152 -0
- package/dist/content-chunker.js.map +7 -0
- package/dist/content-dedup.d.ts +23 -0
- package/dist/content-dedup.js +60 -0
- package/dist/content-dedup.js.map +7 -0
- package/dist/cookie-consent.d.ts +25 -0
- package/dist/cookie-consent.js +78 -0
- package/dist/cookie-consent.js.map +7 -0
- package/dist/cookie-jar.d.ts +20 -0
- package/dist/cookie-jar.js +61 -0
- package/dist/cookie-jar.js.map +7 -0
- package/dist/crawler.d.ts +179 -0
- package/dist/crawler.js +3672 -0
- package/dist/crawler.js.map +7 -0
- package/dist/custom-js-injector.d.ts +42 -0
- package/dist/custom-js-injector.js +50 -0
- package/dist/custom-js-injector.js.map +7 -0
- package/dist/dark-mode-capture.d.ts +23 -0
- package/dist/dark-mode-capture.js +75 -0
- package/dist/dark-mode-capture.js.map +7 -0
- package/dist/dead-link-detector.d.ts +37 -0
- package/dist/dead-link-detector.js +50 -0
- package/dist/dead-link-detector.js.map +7 -0
- package/dist/element-hash.d.ts +9 -0
- package/dist/element-hash.js +9 -0
- package/dist/element-hash.js.map +7 -0
- package/dist/email-otp.d.ts +42 -0
- package/dist/email-otp.js +177 -0
- package/dist/email-otp.js.map +7 -0
- package/dist/engine-router.d.ts +47 -0
- package/dist/engine-router.js +94 -0
- package/dist/engine-router.js.map +7 -0
- package/dist/export/bulk-export.d.ts +26 -0
- package/dist/export/bulk-export.js +76 -0
- package/dist/export/bulk-export.js.map +7 -0
- package/dist/export/vector-export.d.ts +76 -0
- package/dist/export/vector-export.js +85 -0
- package/dist/export/vector-export.js.map +7 -0
- package/dist/extractors/advanced.d.ts +54 -0
- package/dist/extractors/advanced.js +245 -0
- package/dist/extractors/advanced.js.map +7 -0
- package/dist/extractors/artifact-envelope.d.ts +63 -0
- package/dist/extractors/artifact-envelope.js +63 -0
- package/dist/extractors/artifact-envelope.js.map +7 -0
- package/dist/failure-analysis-agent.d.ts +16 -0
- package/dist/failure-analysis-agent.js +40 -0
- package/dist/failure-analysis-agent.js.map +7 -0
- package/dist/feature-flag-detector.d.ts +30 -0
- package/dist/feature-flag-detector.js +55 -0
- package/dist/feature-flag-detector.js.map +7 -0
- package/dist/form-detector.d.ts +34 -0
- package/dist/form-detector.js +58 -0
- package/dist/form-detector.js.map +7 -0
- package/dist/frontier.d.ts +32 -0
- package/dist/frontier.js +83 -0
- package/dist/frontier.js.map +7 -0
- package/dist/gap-pass.d.ts +35 -0
- package/dist/gap-pass.js +81 -0
- package/dist/gap-pass.js.map +7 -0
- package/dist/graphql-detector.d.ts +39 -0
- package/dist/graphql-detector.js +86 -0
- package/dist/graphql-detector.js.map +7 -0
- package/dist/http-engine.d.ts +80 -0
- package/dist/http-engine.js +304 -0
- package/dist/http-engine.js.map +7 -0
- package/dist/incremental-diff.d.ts +59 -0
- package/dist/incremental-diff.js +61 -0
- package/dist/incremental-diff.js.map +7 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +7 -0
- package/dist/input-fuzzer.d.ts +30 -0
- package/dist/input-fuzzer.js +105 -0
- package/dist/input-fuzzer.js.map +7 -0
- package/dist/interactive-probe.d.ts +104 -0
- package/dist/interactive-probe.js +442 -0
- package/dist/interactive-probe.js.map +7 -0
- package/dist/job-queue.d.ts +171 -0
- package/dist/job-queue.js +2256 -0
- package/dist/job-queue.js.map +7 -0
- package/dist/js-coverage.d.ts +29 -0
- package/dist/js-coverage.js +56 -0
- package/dist/js-coverage.js.map +7 -0
- package/dist/js-router-detector.d.ts +17 -0
- package/dist/js-router-detector.js +90 -0
- package/dist/js-router-detector.js.map +7 -0
- package/dist/language-detector.d.ts +22 -0
- package/dist/language-detector.js +57 -0
- package/dist/language-detector.js.map +7 -0
- package/dist/link-classifier.d.ts +23 -0
- package/dist/link-classifier.js +40 -0
- package/dist/link-classifier.js.map +7 -0
- package/dist/live-view.d.ts +36 -0
- package/dist/live-view.js +100 -0
- package/dist/live-view.js.map +7 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +10 -0
- package/dist/logger.js.map +7 -0
- package/dist/markdown-extract.d.ts +8 -0
- package/dist/markdown-extract.js +54 -0
- package/dist/markdown-extract.js.map +7 -0
- package/dist/native-crawler.d.ts +69 -0
- package/dist/native-crawler.js +839 -0
- package/dist/native-crawler.js.map +7 -0
- package/dist/network-observer.d.ts +49 -0
- package/dist/network-observer.js +149 -0
- package/dist/network-observer.js.map +7 -0
- package/dist/network-throttle.d.ts +25 -0
- package/dist/network-throttle.js +34 -0
- package/dist/network-throttle.js.map +7 -0
- package/dist/openapi-generator.d.ts +49 -0
- package/dist/openapi-generator.js +89 -0
- package/dist/openapi-generator.js.map +7 -0
- package/dist/pdf-extractor.d.ts +28 -0
- package/dist/pdf-extractor.js +81 -0
- package/dist/pdf-extractor.js.map +7 -0
- package/dist/performance-metrics.d.ts +53 -0
- package/dist/performance-metrics.js +83 -0
- package/dist/performance-metrics.js.map +7 -0
- package/dist/phash.d.ts +38 -0
- package/dist/phash.js +115 -0
- package/dist/phash.js.map +7 -0
- package/dist/pii-scrubber.d.ts +21 -0
- package/dist/pii-scrubber.js +75 -0
- package/dist/pii-scrubber.js.map +7 -0
- package/dist/playwright-recorder.d.ts +39 -0
- package/dist/playwright-recorder.js +123 -0
- package/dist/playwright-recorder.js.map +7 -0
- package/dist/policies/crawl-safety.d.ts +9 -0
- package/dist/policies/crawl-safety.js +37 -0
- package/dist/policies/crawl-safety.js.map +7 -0
- package/dist/profiles/domain-profile.d.ts +19 -0
- package/dist/profiles/domain-profile.js +140 -0
- package/dist/profiles/domain-profile.js.map +7 -0
- package/dist/proxy.d.ts +50 -0
- package/dist/proxy.js +165 -0
- package/dist/proxy.js.map +7 -0
- package/dist/rate-limiter.d.ts +31 -0
- package/dist/rate-limiter.js +64 -0
- package/dist/rate-limiter.js.map +7 -0
- package/dist/redirect-tracker.d.ts +37 -0
- package/dist/redirect-tracker.js +65 -0
- package/dist/redirect-tracker.js.map +7 -0
- package/dist/redis-frontier.d.ts +48 -0
- package/dist/redis-frontier.js +126 -0
- package/dist/redis-frontier.js.map +7 -0
- package/dist/redis-queue.d.ts +48 -0
- package/dist/redis-queue.js +165 -0
- package/dist/redis-queue.js.map +7 -0
- package/dist/response-cache.d.ts +38 -0
- package/dist/response-cache.js +140 -0
- package/dist/response-cache.js.map +7 -0
- package/dist/responsive-detector.d.ts +29 -0
- package/dist/responsive-detector.js +46 -0
- package/dist/responsive-detector.js.map +7 -0
- package/dist/retry-policy.d.ts +20 -0
- package/dist/retry-policy.js +54 -0
- package/dist/retry-policy.js.map +7 -0
- package/dist/robots-txt.d.ts +9 -0
- package/dist/robots-txt.js +77 -0
- package/dist/robots-txt.js.map +7 -0
- package/dist/screen-reader.d.ts +16 -0
- package/dist/screen-reader.js +46 -0
- package/dist/screen-reader.js.map +7 -0
- package/dist/screenshot-masker.d.ts +28 -0
- package/dist/screenshot-masker.js +62 -0
- package/dist/screenshot-masker.js.map +7 -0
- package/dist/seo-checker.d.ts +33 -0
- package/dist/seo-checker.js +91 -0
- package/dist/seo-checker.js.map +7 -0
- package/dist/session-capture.d.ts +35 -0
- package/dist/session-capture.js +267 -0
- package/dist/session-capture.js.map +7 -0
- package/dist/session-export.d.ts +49 -0
- package/dist/session-export.js +117 -0
- package/dist/session-export.js.map +7 -0
- package/dist/sitemap-writer.d.ts +21 -0
- package/dist/sitemap-writer.js +47 -0
- package/dist/sitemap-writer.js.map +7 -0
- package/dist/sitemap.d.ts +51 -0
- package/dist/sitemap.js +152 -0
- package/dist/sitemap.js.map +7 -0
- package/dist/skyvern.d.ts +56 -0
- package/dist/skyvern.js +129 -0
- package/dist/skyvern.js.map +7 -0
- package/dist/sqs-queue.d.ts +2 -0
- package/dist/sqs-queue.js +47 -0
- package/dist/sqs-queue.js.map +7 -0
- package/dist/sso-token.d.ts +29 -0
- package/dist/sso-token.js +76 -0
- package/dist/sso-token.js.map +7 -0
- package/dist/stealth/human-behavior.d.ts +20 -0
- package/dist/stealth/human-behavior.js +168 -0
- package/dist/stealth/human-behavior.js.map +7 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +77 -0
- package/dist/telemetry.js.map +7 -0
- package/dist/test-executor.d.ts +67 -0
- package/dist/test-executor.js +311 -0
- package/dist/test-executor.js.map +7 -0
- package/dist/tls-fingerprint.d.ts +50 -0
- package/dist/tls-fingerprint.js +81 -0
- package/dist/tls-fingerprint.js.map +7 -0
- package/dist/url-pattern-completer.d.ts +28 -0
- package/dist/url-pattern-completer.js +87 -0
- package/dist/url-pattern-completer.js.map +7 -0
- package/dist/visual-diff-verdict.d.ts +40 -0
- package/dist/visual-diff-verdict.js +25 -0
- package/dist/visual-diff-verdict.js.map +7 -0
- package/dist/webhook-notifier.d.ts +30 -0
- package/dist/webhook-notifier.js +76 -0
- package/dist/webhook-notifier.js.map +7 -0
- package/dist/websocket-detector.d.ts +24 -0
- package/dist/websocket-detector.js +60 -0
- package/dist/websocket-detector.js.map +7 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +33 -0
- package/dist/worker.js.map +7 -0
- package/package.json +45 -0
- package/src/accessibility-auditor.ts +102 -0
- package/src/ai-login.ts +218 -0
- package/src/auth-token-rotator.ts +89 -0
- package/src/auth.ts +396 -0
- package/src/browser-extension.ts +186 -0
- package/src/captcha-solver.ts +143 -0
- package/src/color-contrast.ts +116 -0
- package/src/content-chunker.ts +170 -0
- package/src/content-dedup.ts +99 -0
- package/src/cookie-consent.ts +106 -0
- package/src/cookie-jar.ts +91 -0
- package/src/crawler.ts +4305 -0
- package/src/custom-js-injector.ts +93 -0
- package/src/dark-mode-capture.ts +110 -0
- package/src/dead-link-detector.ts +76 -0
- package/src/element-hash.ts +16 -0
- package/src/email-otp.ts +252 -0
- package/src/engine-router.ts +141 -0
- package/src/export/bulk-export.ts +85 -0
- package/src/export/vector-export.ts +160 -0
- package/src/extractors/advanced.ts +283 -0
- package/src/extractors/artifact-envelope.ts +111 -0
- package/src/failure-analysis-agent.ts +49 -0
- package/src/feature-flag-detector.ts +97 -0
- package/src/form-detector.ts +103 -0
- package/src/frontier.test.ts +64 -0
- package/src/frontier.ts +112 -0
- package/src/gap-pass.ts +122 -0
- package/src/graphql-detector.ts +129 -0
- package/src/http-engine.ts +433 -0
- package/src/incremental-diff.ts +121 -0
- package/src/index.ts +35 -0
- package/src/input-fuzzer.ts +129 -0
- package/src/interactive-probe.ts +623 -0
- package/src/job-queue.test.ts +120 -0
- package/src/job-queue.ts +2701 -0
- package/src/js-coverage.ts +87 -0
- package/src/js-router-detector.ts +112 -0
- package/src/language-detector.ts +82 -0
- package/src/link-classifier.ts +65 -0
- package/src/live-view.ts +141 -0
- package/src/logger.ts +9 -0
- package/src/markdown-extract.test.ts +73 -0
- package/src/markdown-extract.ts +76 -0
- package/src/native-crawler.ts +1106 -0
- package/src/network-observer.ts +204 -0
- package/src/network-throttle.ts +69 -0
- package/src/openapi-generator.ts +135 -0
- package/src/pdf-extractor.ts +123 -0
- package/src/performance-metrics.ts +134 -0
- package/src/phash.ts +151 -0
- package/src/pii-scrubber.ts +99 -0
- package/src/playwright-recorder.ts +170 -0
- package/src/policies/crawl-safety.ts +46 -0
- package/src/profiles/domain-profile.ts +167 -0
- package/src/proxy.ts +238 -0
- package/src/rate-limiter.ts +87 -0
- package/src/redirect-tracker.ts +97 -0
- package/src/redis-frontier.ts +143 -0
- package/src/redis-queue.ts +208 -0
- package/src/response-cache.ts +150 -0
- package/src/responsive-detector.ts +92 -0
- package/src/retry-policy.ts +87 -0
- package/src/robots-txt.ts +96 -0
- package/src/screen-reader.ts +89 -0
- package/src/screenshot-masker.ts +98 -0
- package/src/seo-checker.ts +134 -0
- package/src/session-capture.ts +325 -0
- package/src/session-export.test.ts +75 -0
- package/src/session-export.ts +172 -0
- package/src/sitemap-writer.ts +65 -0
- package/src/sitemap.test.ts +131 -0
- package/src/sitemap.ts +224 -0
- package/src/skyvern.ts +199 -0
- package/src/sqs-queue.ts +49 -0
- package/src/sso-token.ts +120 -0
- package/src/stealth/human-behavior.ts +191 -0
- package/src/telemetry.ts +122 -0
- package/src/test-executor.test.ts +72 -0
- package/src/test-executor.ts +424 -0
- package/src/tls-fingerprint.ts +111 -0
- package/src/url-pattern-completer.ts +114 -0
- package/src/visual-diff-verdict.test.ts +80 -0
- package/src/visual-diff-verdict.ts +57 -0
- package/src/webhook-notifier.ts +109 -0
- package/src/websocket-detector.ts +83 -0
- package/src/worker.ts +48 -0
- package/tsconfig.json +13 -0
- package/types/accessibility-auditor.d.ts +29 -0
- package/types/ai-login.d.ts +32 -0
- package/types/auth-token-rotator.d.ts +47 -0
- package/types/auth.d.ts +20 -0
- package/types/browser-extension.d.ts +37 -0
- package/types/bull-worker.d.ts +6 -0
- package/types/captcha-solver.d.ts +16 -0
- package/types/color-contrast.d.ts +30 -0
- package/types/content-chunker.d.ts +18 -0
- package/types/content-dedup.d.ts +23 -0
- package/types/cookie-consent.d.ts +25 -0
- package/types/cookie-jar.d.ts +20 -0
- package/types/crawler.d.ts +179 -0
- package/types/custom-js-injector.d.ts +42 -0
- package/types/dark-mode-capture.d.ts +23 -0
- package/types/dead-link-detector.d.ts +37 -0
- package/types/element-hash.d.ts +9 -0
- package/types/email-otp.d.ts +42 -0
- package/types/engine-router.d.ts +47 -0
- package/types/export/bulk-export.d.ts +26 -0
- package/types/export/vector-export.d.ts +76 -0
- package/types/extractors/advanced.d.ts +54 -0
- package/types/extractors/artifact-envelope.d.ts +63 -0
- package/types/failure-analysis-agent.d.ts +16 -0
- package/types/feature-flag-detector.d.ts +30 -0
- package/types/form-detector.d.ts +34 -0
- package/types/frontier.d.ts +32 -0
- package/types/gap-pass.d.ts +35 -0
- package/types/graphql-detector.d.ts +39 -0
- package/types/http-engine.d.ts +80 -0
- package/types/incremental-diff.d.ts +59 -0
- package/types/index.d.ts +35 -0
- package/types/input-fuzzer.d.ts +30 -0
- package/types/interactive-probe.d.ts +104 -0
- package/types/job-queue.d.ts +171 -0
- package/types/js-coverage.d.ts +29 -0
- package/types/js-router-detector.d.ts +17 -0
- package/types/language-detector.d.ts +22 -0
- package/types/link-classifier.d.ts +23 -0
- package/types/live-view.d.ts +36 -0
- package/types/logger.d.ts +2 -0
- package/types/markdown-extract.d.ts +8 -0
- package/types/native-crawler.d.ts +69 -0
- package/types/network-observer.d.ts +49 -0
- package/types/network-throttle.d.ts +25 -0
- package/types/openapi-generator.d.ts +49 -0
- package/types/pdf-extractor.d.ts +28 -0
- package/types/performance-metrics.d.ts +53 -0
- package/types/phash.d.ts +38 -0
- package/types/pii-scrubber.d.ts +21 -0
- package/types/playwright-recorder.d.ts +39 -0
- package/types/policies/crawl-safety.d.ts +9 -0
- package/types/profiles/domain-profile.d.ts +19 -0
- package/types/proxy.d.ts +50 -0
- package/types/rate-limiter.d.ts +31 -0
- package/types/redirect-tracker.d.ts +37 -0
- package/types/redis-frontier.d.ts +48 -0
- package/types/redis-queue.d.ts +48 -0
- package/types/response-cache.d.ts +38 -0
- package/types/responsive-detector.d.ts +29 -0
- package/types/retry-policy.d.ts +20 -0
- package/types/robots-txt.d.ts +9 -0
- package/types/screen-reader.d.ts +16 -0
- package/types/screenshot-masker.d.ts +28 -0
- package/types/seo-checker.d.ts +33 -0
- package/types/session-capture.d.ts +38 -0
- package/types/session-export.d.ts +49 -0
- package/types/sitemap-writer.d.ts +21 -0
- package/types/sitemap.d.ts +51 -0
- package/types/skyvern.d.ts +56 -0
- package/types/sqs-queue.d.ts +2 -0
- package/types/sso-token.d.ts +29 -0
- package/types/stealth/human-behavior.d.ts +20 -0
- package/types/telemetry.d.ts +34 -0
- package/types/test-executor.d.ts +67 -0
- package/types/tls-fingerprint.d.ts +50 -0
- package/types/url-pattern-completer.d.ts +28 -0
- package/types/visual-diff-verdict.d.ts +40 -0
- package/types/webhook-notifier.d.ts +30 -0
- package/types/websocket-detector.d.ts +24 -0
- package/types/worker.d.ts +1 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAPTCHA auto-solving via 2captcha or CapSolver APIs.
|
|
3
|
+
* Supports reCAPTCHA v2, hCaptcha, and Cloudflare Turnstile.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type CaptchaSolverProvider = '2captcha' | 'capsolver';
|
|
7
|
+
|
|
8
|
+
interface SolveResult {
|
|
9
|
+
token: string;
|
|
10
|
+
provider: CaptchaSolverProvider;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Poll interval and max attempts
|
|
14
|
+
const POLL_INTERVAL_MS = 3_000;
|
|
15
|
+
const MAX_ATTEMPTS = 40; // 2 min max
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detect CAPTCHA type and sitekey on the page, then solve it.
|
|
19
|
+
* Returns null if no CAPTCHA found or solver not configured.
|
|
20
|
+
* Throws on solve failure.
|
|
21
|
+
*/
|
|
22
|
+
export async function solveCaptcha(
|
|
23
|
+
page: any,
|
|
24
|
+
pageUrl: string,
|
|
25
|
+
apiKey: string,
|
|
26
|
+
provider: CaptchaSolverProvider = '2captcha'
|
|
27
|
+
): Promise<SolveResult | null> {
|
|
28
|
+
// Detect CAPTCHA type
|
|
29
|
+
const captchaInfo = await page.evaluate(() => {
|
|
30
|
+
// reCAPTCHA v2
|
|
31
|
+
const recaptchaEl = document.querySelector('.g-recaptcha, [data-sitekey]') as HTMLElement | null;
|
|
32
|
+
if (recaptchaEl) {
|
|
33
|
+
const sitekey = recaptchaEl.getAttribute('data-sitekey') || '';
|
|
34
|
+
return { type: 'recaptcha', sitekey };
|
|
35
|
+
}
|
|
36
|
+
// hCaptcha
|
|
37
|
+
const hcaptchaEl = document.querySelector('.h-captcha, [data-hcaptcha-sitekey]') as HTMLElement | null;
|
|
38
|
+
if (hcaptchaEl) {
|
|
39
|
+
const sitekey = hcaptchaEl.getAttribute('data-sitekey') || hcaptchaEl.getAttribute('data-hcaptcha-sitekey') || '';
|
|
40
|
+
return { type: 'hcaptcha', sitekey };
|
|
41
|
+
}
|
|
42
|
+
// Cloudflare Turnstile
|
|
43
|
+
const turnstileEl = document.querySelector('.cf-turnstile') as HTMLElement | null;
|
|
44
|
+
if (turnstileEl) {
|
|
45
|
+
const sitekey = turnstileEl.getAttribute('data-sitekey') || '';
|
|
46
|
+
return { type: 'turnstile', sitekey };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!captchaInfo?.sitekey) return null;
|
|
52
|
+
|
|
53
|
+
const token = provider === 'capsolver'
|
|
54
|
+
? await solveWithCapSolver(captchaInfo.type, captchaInfo.sitekey, pageUrl, apiKey)
|
|
55
|
+
: await solveWith2Captcha(captchaInfo.type, captchaInfo.sitekey, pageUrl, apiKey);
|
|
56
|
+
|
|
57
|
+
// Inject the token into the page
|
|
58
|
+
await page.evaluate((t: string, type: string) => {
|
|
59
|
+
// reCAPTCHA
|
|
60
|
+
if (type === 'recaptcha') {
|
|
61
|
+
const el = document.querySelector('textarea.g-recaptcha-response') as HTMLTextAreaElement | null;
|
|
62
|
+
if (el) { el.value = t; }
|
|
63
|
+
// Also trigger the callback if registered
|
|
64
|
+
if ((window as any).grecaptcha?.getResponse && document.querySelector('[data-callback]')) {
|
|
65
|
+
const cb = (document.querySelector('[data-callback]') as HTMLElement)?.getAttribute('data-callback');
|
|
66
|
+
if (cb && (window as any)[cb]) (window as any)[cb](t);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// hCaptcha
|
|
70
|
+
if (type === 'hcaptcha') {
|
|
71
|
+
const el = document.querySelector('textarea[name="h-captcha-response"]') as HTMLTextAreaElement | null;
|
|
72
|
+
if (el) { el.value = t; }
|
|
73
|
+
}
|
|
74
|
+
// Turnstile
|
|
75
|
+
if (type === 'turnstile') {
|
|
76
|
+
const el = document.querySelector('[name="cf-turnstile-response"]') as HTMLInputElement | null;
|
|
77
|
+
if (el) { el.value = t; }
|
|
78
|
+
}
|
|
79
|
+
}, token, captchaInfo.type);
|
|
80
|
+
|
|
81
|
+
return { token, provider };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function solveWith2Captcha(type: string, sitekey: string, pageUrl: string, apiKey: string): Promise<string> {
|
|
85
|
+
const method = type === 'hcaptcha' ? 'hcaptcha' : type === 'turnstile' ? 'turnstile' : 'userrecaptcha';
|
|
86
|
+
|
|
87
|
+
const submitRes = await fetch('http://2captcha.com/in.php', {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
body: new URLSearchParams({
|
|
90
|
+
key: apiKey,
|
|
91
|
+
method,
|
|
92
|
+
googlekey: sitekey,
|
|
93
|
+
pageurl: pageUrl,
|
|
94
|
+
json: '1',
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
const submitData = await submitRes.json() as { status: number; request: string };
|
|
98
|
+
if (submitData.status !== 1) throw new Error(`2captcha submit failed: ${submitData.request}`);
|
|
99
|
+
const taskId = submitData.request;
|
|
100
|
+
|
|
101
|
+
// Poll for result
|
|
102
|
+
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
|
103
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
104
|
+
const pollRes = await fetch(`http://2captcha.com/res.php?key=${apiKey}&action=get&id=${taskId}&json=1`);
|
|
105
|
+
const pollData = await pollRes.json() as { status: number; request: string };
|
|
106
|
+
if (pollData.status === 1) return pollData.request;
|
|
107
|
+
if (pollData.request !== 'CAPCHA_NOT_READY') throw new Error(`2captcha error: ${pollData.request}`);
|
|
108
|
+
}
|
|
109
|
+
throw new Error('2captcha timed out after 2 minutes');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function solveWithCapSolver(type: string, sitekey: string, pageUrl: string, apiKey: string): Promise<string> {
|
|
113
|
+
const taskType = type === 'hcaptcha' ? 'HCaptchaTaskProxyless'
|
|
114
|
+
: type === 'turnstile' ? 'AntiTurnstileTaskProxyless'
|
|
115
|
+
: 'ReCaptchaV2TaskProxyless';
|
|
116
|
+
|
|
117
|
+
const createRes = await fetch('https://api.capsolver.com/createTask', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
clientKey: apiKey,
|
|
122
|
+
task: { type: taskType, websiteURL: pageUrl, websiteKey: sitekey },
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
const createData = await createRes.json() as { errorId: number; taskId?: string; errorDescription?: string };
|
|
126
|
+
if (createData.errorId !== 0) throw new Error(`CapSolver create failed: ${createData.errorDescription}`);
|
|
127
|
+
const taskId = createData.taskId!;
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
|
130
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
131
|
+
const pollRes = await fetch('https://api.capsolver.com/getTaskResult', {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({ clientKey: apiKey, taskId }),
|
|
135
|
+
});
|
|
136
|
+
const pollData = await pollRes.json() as { errorId: number; status: string; solution?: { gRecaptchaResponse?: string; token?: string }; errorDescription?: string };
|
|
137
|
+
if (pollData.errorId !== 0) throw new Error(`CapSolver error: ${pollData.errorDescription}`);
|
|
138
|
+
if (pollData.status === 'ready') {
|
|
139
|
+
return pollData.solution?.gRecaptchaResponse ?? pollData.solution?.token ?? '';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw new Error('CapSolver timed out after 2 minutes');
|
|
143
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WCAG color contrast checker.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates text contrast ratios against backgrounds for all visible text elements.
|
|
5
|
+
* Reports elements that fail WCAG 2.1 AA (4.5:1 for normal text, 3:1 for large text)
|
|
6
|
+
* or AAA (7:1 for normal, 4.5:1 for large text) requirements.
|
|
7
|
+
*
|
|
8
|
+
* Uses computedStyle to get actual rendered colors — accounts for CSS variables
|
|
9
|
+
* and inheritance.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Page } from 'playwright';
|
|
13
|
+
|
|
14
|
+
export interface ContrastViolation {
|
|
15
|
+
selector: string;
|
|
16
|
+
text: string;
|
|
17
|
+
foreground: string; // rgb(r, g, b)
|
|
18
|
+
background: string; // rgb(r, g, b)
|
|
19
|
+
ratio: number; // actual contrast ratio
|
|
20
|
+
required: number; // required ratio (4.5 or 3.0)
|
|
21
|
+
level: 'AA' | 'AAA';
|
|
22
|
+
isLargeText: boolean; // font-size >= 18px or bold >= 14px
|
|
23
|
+
severity: 'fail' | 'warn'; // fail = below AA, warn = below AAA
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ContrastAuditResult {
|
|
27
|
+
url: string;
|
|
28
|
+
violations: ContrastViolation[];
|
|
29
|
+
passCount: number;
|
|
30
|
+
score: number; // 0-100
|
|
31
|
+
auditedAt: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function auditColorContrast(page: Page, url: string): Promise<ContrastAuditResult> {
|
|
35
|
+
const auditedAt = new Date().toISOString();
|
|
36
|
+
|
|
37
|
+
const violations: ContrastViolation[] = await page.evaluate(() => {
|
|
38
|
+
function parseRgb(color: string): [number, number, number, number] | null {
|
|
39
|
+
const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/);
|
|
40
|
+
return m ? [+m[1], +m[2], +m[3], m[4] !== undefined ? +m[4] : 1] : null;
|
|
41
|
+
}
|
|
42
|
+
function luminance([r, g, b]: [number, number, number, number]): number {
|
|
43
|
+
const toLinear = (c: number) => {
|
|
44
|
+
const n = c / 255;
|
|
45
|
+
return n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
|
|
46
|
+
};
|
|
47
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
48
|
+
}
|
|
49
|
+
function contrastRatio(fg: [number,number,number,number], bg: [number,number,number,number]): number {
|
|
50
|
+
const l1 = luminance(fg);
|
|
51
|
+
const l2 = luminance(bg);
|
|
52
|
+
const lighter = Math.max(l1, l2);
|
|
53
|
+
const darker = Math.min(l1, l2);
|
|
54
|
+
return Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const violations: any[] = [];
|
|
58
|
+
const elements = Array.from(document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, a, span, li, td, th, label, button'));
|
|
59
|
+
let checked = 0;
|
|
60
|
+
|
|
61
|
+
for (const el of elements as HTMLElement[]) {
|
|
62
|
+
if (checked >= 100) break; // limit to avoid perf hit
|
|
63
|
+
const text = (el.textContent ?? '').trim().slice(0, 60);
|
|
64
|
+
if (!text) continue;
|
|
65
|
+
|
|
66
|
+
const style = window.getComputedStyle(el);
|
|
67
|
+
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
68
|
+
|
|
69
|
+
const fg = parseRgb(style.color);
|
|
70
|
+
const bg = parseRgb(style.backgroundColor);
|
|
71
|
+
if (!fg || !bg || bg[3] === 0) continue; // transparent bg — can't check
|
|
72
|
+
|
|
73
|
+
const fontSize = parseFloat(style.fontSize);
|
|
74
|
+
const fontWeight = style.fontWeight;
|
|
75
|
+
const isLargeText = fontSize >= 18 || (fontSize >= 14 && (fontWeight === 'bold' || +fontWeight >= 700));
|
|
76
|
+
const ratio = contrastRatio(fg, bg);
|
|
77
|
+
const aaRequired = isLargeText ? 3.0 : 4.5;
|
|
78
|
+
const aaaRequired = isLargeText ? 4.5 : 7.0;
|
|
79
|
+
|
|
80
|
+
if (ratio < aaRequired) {
|
|
81
|
+
violations.push({
|
|
82
|
+
selector: el.tagName.toLowerCase(),
|
|
83
|
+
text: text.slice(0, 50),
|
|
84
|
+
foreground: style.color,
|
|
85
|
+
background: style.backgroundColor,
|
|
86
|
+
ratio,
|
|
87
|
+
required: aaRequired,
|
|
88
|
+
level: 'AA',
|
|
89
|
+
isLargeText,
|
|
90
|
+
severity: 'fail',
|
|
91
|
+
});
|
|
92
|
+
} else if (ratio < aaaRequired) {
|
|
93
|
+
violations.push({
|
|
94
|
+
selector: el.tagName.toLowerCase(),
|
|
95
|
+
text: text.slice(0, 50),
|
|
96
|
+
foreground: style.color,
|
|
97
|
+
background: style.backgroundColor,
|
|
98
|
+
ratio,
|
|
99
|
+
required: aaaRequired,
|
|
100
|
+
level: 'AAA',
|
|
101
|
+
isLargeText,
|
|
102
|
+
severity: 'warn',
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
checked++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return violations;
|
|
109
|
+
}).catch(() => [] as ContrastViolation[]);
|
|
110
|
+
|
|
111
|
+
const passCount = Math.max(0, 100 - violations.length);
|
|
112
|
+
const criticals = violations.filter(v => v.severity === 'fail').length;
|
|
113
|
+
const score = Math.max(0, 100 - criticals * 15 - violations.filter(v => v.severity === 'warn').length * 3);
|
|
114
|
+
|
|
115
|
+
return { url, violations, passCount, score, auditedAt };
|
|
116
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export type ChunkingStrategy = 'fixed' | 'overlap' | 'sentence' | 'paragraph' | 'markdown-heading' | 'semantic' | 'recursive';
|
|
2
|
+
|
|
3
|
+
export interface ChunkOpts {
|
|
4
|
+
strategy: ChunkingStrategy;
|
|
5
|
+
chunkSize?: number; // token estimate target (1 token ≈ 4 chars). Default 2000.
|
|
6
|
+
overlap?: number; // overlap tokens for 'overlap' strategy. Default 200.
|
|
7
|
+
separators?: string[]; // for 'recursive' strategy. Default ['\\n\\n', '\\n', '. ', ' ']
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Chunk {
|
|
11
|
+
index: number;
|
|
12
|
+
text: string;
|
|
13
|
+
tokenEstimate: number;
|
|
14
|
+
startChar: number;
|
|
15
|
+
endChar: number;
|
|
16
|
+
heading?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function estimateTokens(text: string): number {
|
|
20
|
+
return Math.ceil(text.length / 4);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function chunkContent(text: string, opts: ChunkOpts): Chunk[] {
|
|
24
|
+
const size = opts.chunkSize ?? 2000;
|
|
25
|
+
const charSize = size * 4;
|
|
26
|
+
const overlap = opts.overlap ?? 200;
|
|
27
|
+
const overlapChars = overlap * 4;
|
|
28
|
+
|
|
29
|
+
switch (opts.strategy) {
|
|
30
|
+
case 'fixed': return fixedChunk(text, charSize);
|
|
31
|
+
case 'overlap': return overlapChunk(text, charSize, overlapChars);
|
|
32
|
+
case 'sentence': return sentenceChunk(text, charSize);
|
|
33
|
+
case 'paragraph': return paragraphChunk(text, charSize);
|
|
34
|
+
case 'markdown-heading': return markdownHeadingChunk(text, charSize);
|
|
35
|
+
case 'semantic': return semanticChunk(text, charSize);
|
|
36
|
+
case 'recursive': return recursiveChunk(text, charSize, opts.separators ?? ['\n\n', '\n', '. ', ' ']);
|
|
37
|
+
default: return fixedChunk(text, charSize);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeChunk(text: string, index: number, startChar: number, heading?: string): Chunk {
|
|
42
|
+
return { index, text, tokenEstimate: estimateTokens(text), startChar, endChar: startChar + text.length, heading };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fixedChunk(text: string, charSize: number): Chunk[] {
|
|
46
|
+
const chunks: Chunk[] = [];
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < text.length) {
|
|
49
|
+
const slice = text.slice(i, i + charSize);
|
|
50
|
+
chunks.push(makeChunk(slice, chunks.length, i));
|
|
51
|
+
i += charSize;
|
|
52
|
+
}
|
|
53
|
+
return chunks;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function overlapChunk(text: string, charSize: number, overlapChars: number): Chunk[] {
|
|
57
|
+
const chunks: Chunk[] = [];
|
|
58
|
+
let i = 0;
|
|
59
|
+
while (i < text.length) {
|
|
60
|
+
const slice = text.slice(i, i + charSize);
|
|
61
|
+
chunks.push(makeChunk(slice, chunks.length, i));
|
|
62
|
+
i += charSize - overlapChars;
|
|
63
|
+
if (i <= 0) break;
|
|
64
|
+
}
|
|
65
|
+
return chunks;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sentenceChunk(text: string, charSize: number): Chunk[] {
|
|
69
|
+
const sentences = text.match(/[^.!?]*[.!?]+["']?\s*/g) ?? [text];
|
|
70
|
+
return groupIntoChunks(sentences, charSize);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function paragraphChunk(text: string, charSize: number): Chunk[] {
|
|
74
|
+
const paras = text.split(/\n\n+/).filter(p => p.trim());
|
|
75
|
+
return groupIntoChunks(paras, charSize);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function markdownHeadingChunk(text: string, charSize: number): Chunk[] {
|
|
79
|
+
const chunks: Chunk[] = [];
|
|
80
|
+
const headingRe = /^(#{1,3}\s.+)$/m;
|
|
81
|
+
const sections = text.split(/(?=^#{1,3}\s)/m).filter(s => s.trim());
|
|
82
|
+
let pos = 0;
|
|
83
|
+
for (const section of sections) {
|
|
84
|
+
const headingMatch = section.match(headingRe);
|
|
85
|
+
const heading = headingMatch ? headingMatch[1].replace(/^#+\s*/, '').trim() : undefined;
|
|
86
|
+
if (section.length <= charSize) {
|
|
87
|
+
chunks.push({ ...makeChunk(section.trim(), chunks.length, pos), heading });
|
|
88
|
+
} else {
|
|
89
|
+
// Section too large — sub-chunk by paragraph
|
|
90
|
+
const sub = paragraphChunk(section, charSize);
|
|
91
|
+
for (const s of sub) {
|
|
92
|
+
chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar, heading });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
pos += section.length;
|
|
96
|
+
}
|
|
97
|
+
return chunks.length > 0 ? chunks : fixedChunk(text, charSize);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function semanticChunk(text: string, charSize: number): Chunk[] {
|
|
101
|
+
// Like paragraph but merges short paragraphs with the next
|
|
102
|
+
const paras = text.split(/\n\n+/).filter(p => p.trim());
|
|
103
|
+
const merged: string[] = [];
|
|
104
|
+
let current = '';
|
|
105
|
+
for (const para of paras) {
|
|
106
|
+
if (current.length + para.length + 2 <= charSize) {
|
|
107
|
+
current = current ? current + '\n\n' + para : para;
|
|
108
|
+
} else {
|
|
109
|
+
if (current) merged.push(current);
|
|
110
|
+
current = para;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (current) merged.push(current);
|
|
114
|
+
return groupIntoChunks(merged, charSize);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function recursiveChunk(text: string, charSize: number, separators: string[]): Chunk[] {
|
|
118
|
+
if (text.length <= charSize) return [makeChunk(text, 0, 0)];
|
|
119
|
+
const [sep, ...rest] = separators;
|
|
120
|
+
if (!sep) return fixedChunk(text, charSize);
|
|
121
|
+
const parts = text.split(sep).filter(p => p);
|
|
122
|
+
const chunks: Chunk[] = [];
|
|
123
|
+
let pos = 0;
|
|
124
|
+
for (const part of parts) {
|
|
125
|
+
if (part.length <= charSize) {
|
|
126
|
+
chunks.push(makeChunk(part, chunks.length, pos));
|
|
127
|
+
} else {
|
|
128
|
+
const sub = recursiveChunk(part, charSize, rest);
|
|
129
|
+
for (const s of sub) chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar });
|
|
130
|
+
}
|
|
131
|
+
pos += part.length + sep.length;
|
|
132
|
+
}
|
|
133
|
+
return chunks;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function groupIntoChunks(parts: string[], charSize: number): Chunk[] {
|
|
137
|
+
const chunks: Chunk[] = [];
|
|
138
|
+
let current = '';
|
|
139
|
+
let startChar = 0;
|
|
140
|
+
let pos = 0;
|
|
141
|
+
for (const part of parts) {
|
|
142
|
+
if (current.length + part.length + 1 > charSize && current) {
|
|
143
|
+
chunks.push(makeChunk(current.trim(), chunks.length, startChar));
|
|
144
|
+
startChar = pos;
|
|
145
|
+
current = part;
|
|
146
|
+
} else {
|
|
147
|
+
current = current ? current + ' ' + part : part;
|
|
148
|
+
}
|
|
149
|
+
pos += part.length + 1;
|
|
150
|
+
}
|
|
151
|
+
if (current.trim()) chunks.push(makeChunk(current.trim(), chunks.length, startChar));
|
|
152
|
+
return chunks;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function mergeSmallChunks(chunks: Chunk[], minTokens: number): Chunk[] {
|
|
156
|
+
const minChars = minTokens * 4;
|
|
157
|
+
const result: Chunk[] = [];
|
|
158
|
+
let i = 0;
|
|
159
|
+
while (i < chunks.length) {
|
|
160
|
+
if (chunks[i].text.length < minChars && i + 1 < chunks.length) {
|
|
161
|
+
const merged = chunks[i].text + '\n\n' + chunks[i + 1].text;
|
|
162
|
+
result.push(makeChunk(merged, result.length, chunks[i].startChar, chunks[i].heading ?? chunks[i + 1].heading));
|
|
163
|
+
i += 2;
|
|
164
|
+
} else {
|
|
165
|
+
result.push({ ...chunks[i], index: result.length });
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content deduplication — detect near-duplicate pages during crawl.
|
|
3
|
+
*
|
|
4
|
+
* Uses MinHash-inspired approach: instead of full MinHash (needs many hash functions),
|
|
5
|
+
* we use 8 structural fingerprints extracted from the markdown to detect pages that
|
|
6
|
+
* share the same layout/template even if the data differs.
|
|
7
|
+
*
|
|
8
|
+
* Typical duplicates: /products?page=1 vs /products?page=2 (same template, different data).
|
|
9
|
+
* These waste crawl budget and generate redundant test cases.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Extract structural fingerprints from markdown — layout-sensitive, data-insensitive */
|
|
13
|
+
function extractFingerprints(markdown: string): string[] {
|
|
14
|
+
const lines = markdown.split('\n').filter(l => l.trim().length > 0);
|
|
15
|
+
const fps: string[] = [];
|
|
16
|
+
|
|
17
|
+
// FP1: heading structure (H1-H6 count distribution)
|
|
18
|
+
const headings = lines.filter(l => /^#{1,6} /.test(l));
|
|
19
|
+
const hCounts = [0, 0, 0, 0, 0, 0];
|
|
20
|
+
for (const h of headings) {
|
|
21
|
+
const lvl = h.match(/^(#{1,6}) /)?.[1].length ?? 1;
|
|
22
|
+
hCounts[lvl - 1]++;
|
|
23
|
+
}
|
|
24
|
+
fps.push('h:' + hCounts.join(','));
|
|
25
|
+
|
|
26
|
+
// FP2: link count (grouped into buckets)
|
|
27
|
+
const linkCount = (markdown.match(/\[.+?\]\(.+?\)/g) ?? []).length;
|
|
28
|
+
fps.push('lk:' + Math.floor(linkCount / 5));
|
|
29
|
+
|
|
30
|
+
// FP3: list item count (grouped)
|
|
31
|
+
const listCount = lines.filter(l => /^[\-\*\+] |^\d+\. /.test(l)).length;
|
|
32
|
+
fps.push('li:' + Math.floor(listCount / 3));
|
|
33
|
+
|
|
34
|
+
// FP4: code block presence
|
|
35
|
+
fps.push('code:' + (markdown.includes('```') ? '1' : '0'));
|
|
36
|
+
|
|
37
|
+
// FP5: table presence
|
|
38
|
+
fps.push('tbl:' + (markdown.includes('|') ? '1' : '0'));
|
|
39
|
+
|
|
40
|
+
// FP6: approximate paragraph count
|
|
41
|
+
const paraCount = markdown.split(/\n{2,}/).filter(p => p.trim().length > 30).length;
|
|
42
|
+
fps.push('p:' + Math.floor(paraCount / 2));
|
|
43
|
+
|
|
44
|
+
// FP7: first heading text (normalized)
|
|
45
|
+
const firstH = headings[0]?.replace(/^#{1,6} /, '').toLowerCase().replace(/[^a-z0-9]/g, '') ?? '';
|
|
46
|
+
fps.push('fh:' + firstH.slice(0, 20));
|
|
47
|
+
|
|
48
|
+
// FP8: word count bucket
|
|
49
|
+
const wordCount = markdown.split(/\s+/).length;
|
|
50
|
+
fps.push('wc:' + Math.floor(wordCount / 50));
|
|
51
|
+
|
|
52
|
+
return fps;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function similarity(a: string[], b: string[]): number {
|
|
56
|
+
if (a.length !== b.length) return 0;
|
|
57
|
+
let matches = 0;
|
|
58
|
+
for (let i = 0; i < a.length; i++) {
|
|
59
|
+
if (a[i] === b[i]) matches++;
|
|
60
|
+
}
|
|
61
|
+
return matches / a.length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DedupResult {
|
|
65
|
+
isDuplicate: boolean;
|
|
66
|
+
similarUrl?: string;
|
|
67
|
+
similarityScore: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ContentDeduplicator {
|
|
71
|
+
private seen = new Map<string, { fps: string[]; url: string }>();
|
|
72
|
+
private readonly threshold: number;
|
|
73
|
+
|
|
74
|
+
constructor(threshold = 0.85) {
|
|
75
|
+
this.threshold = threshold;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
check(url: string, markdown: string): DedupResult {
|
|
79
|
+
const fps = extractFingerprints(markdown);
|
|
80
|
+
|
|
81
|
+
for (const [key, entry] of this.seen) {
|
|
82
|
+
const score = similarity(fps, entry.fps);
|
|
83
|
+
if (score >= this.threshold) {
|
|
84
|
+
return { isDuplicate: true, similarUrl: entry.url, similarityScore: score };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.seen.set(url, { fps, url });
|
|
89
|
+
return { isDuplicate: false, similarityScore: 1.0 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
size(): number {
|
|
93
|
+
return this.seen.size;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
reset(): void {
|
|
97
|
+
this.seen.clear();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie consent auto-acceptor.
|
|
3
|
+
*
|
|
4
|
+
* Detects and clicks "Accept" on common cookie consent banners before the
|
|
5
|
+
* crawler proceeds. Without this, many EU sites show a consent wall that
|
|
6
|
+
* blocks content or shows a different layout, making crawl results unreliable.
|
|
7
|
+
*
|
|
8
|
+
* Supports: Cookiebot, OneTrust, TrustArc, Usercentrics, GDPR Cookie Compliance,
|
|
9
|
+
* generic "Accept All" / "Accept Cookies" buttons.
|
|
10
|
+
*
|
|
11
|
+
* Strategy: try known selectors first, then fall back to text-based button search.
|
|
12
|
+
* Non-blocking — if no banner is found or click fails, crawl proceeds normally.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Page } from 'playwright';
|
|
16
|
+
|
|
17
|
+
export interface ConsentResult {
|
|
18
|
+
found: boolean;
|
|
19
|
+
clicked: boolean;
|
|
20
|
+
method?: string; // which selector/strategy matched
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Known consent platform selectors
|
|
25
|
+
const CONSENT_SELECTORS = [
|
|
26
|
+
// Cookiebot
|
|
27
|
+
'#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
|
|
28
|
+
'#CybotCookiebotDialogBodyButtonAccept',
|
|
29
|
+
// OneTrust
|
|
30
|
+
'#onetrust-accept-btn-handler',
|
|
31
|
+
'.ot-sdk-accept-btn',
|
|
32
|
+
// TrustArc
|
|
33
|
+
'.truste_popframe [title*="Accept"]',
|
|
34
|
+
'#truste-consent-button',
|
|
35
|
+
// Usercentrics
|
|
36
|
+
'[data-testid="uc-accept-all-button"]',
|
|
37
|
+
// Generic high-confidence
|
|
38
|
+
'[id*="accept"][id*="cookie"]',
|
|
39
|
+
'[class*="accept"][class*="cookie"]',
|
|
40
|
+
'[class*="cookie"][class*="accept"]',
|
|
41
|
+
// GDPR Cookie Compliance
|
|
42
|
+
'.gdpr-cookie-notice-accept',
|
|
43
|
+
// Quantcast
|
|
44
|
+
'.qc-cmp2-summary-buttons button:first-child',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Fallback: button text matching
|
|
48
|
+
const ACCEPT_TEXT_PATTERNS = [
|
|
49
|
+
/^accept all$/i,
|
|
50
|
+
/^accept all cookies$/i,
|
|
51
|
+
/^allow all$/i,
|
|
52
|
+
/^i accept$/i,
|
|
53
|
+
/^agree$/i,
|
|
54
|
+
/^ok$/i,
|
|
55
|
+
/^got it$/i,
|
|
56
|
+
/^allow cookies$/i,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export async function acceptCookieConsent(page: Page, opts: { waitMs?: number } = {}): Promise<ConsentResult> {
|
|
60
|
+
const { waitMs = 1_500 } = opts;
|
|
61
|
+
|
|
62
|
+
// Wait briefly for consent banners to appear
|
|
63
|
+
await page.waitForTimeout(waitMs).catch(() => {});
|
|
64
|
+
|
|
65
|
+
// Try known selectors first
|
|
66
|
+
for (const selector of CONSENT_SELECTORS) {
|
|
67
|
+
try {
|
|
68
|
+
const el = await page.$(selector);
|
|
69
|
+
if (el && await el.isVisible()) {
|
|
70
|
+
await el.click({ timeout: 2_000 });
|
|
71
|
+
await page.waitForTimeout(500);
|
|
72
|
+
return { found: true, clicked: true, method: `selector: ${selector}` };
|
|
73
|
+
}
|
|
74
|
+
} catch { /* try next */ }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fallback: find buttons with accept-like text
|
|
78
|
+
try {
|
|
79
|
+
const buttons = await page.$$('button, [role="button"], a[class*="cookie"]');
|
|
80
|
+
for (const btn of buttons) {
|
|
81
|
+
const text = ((await btn.textContent()) ?? '').trim();
|
|
82
|
+
if (ACCEPT_TEXT_PATTERNS.some(re => re.test(text))) {
|
|
83
|
+
if (await btn.isVisible()) {
|
|
84
|
+
await btn.click({ timeout: 2_000 });
|
|
85
|
+
await page.waitForTimeout(500);
|
|
86
|
+
return { found: true, clicked: true, method: `text: "${text}"` };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch { /* non-fatal */ }
|
|
91
|
+
|
|
92
|
+
return { found: false, clicked: false };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Check if a consent banner is still visible after attempted dismissal */
|
|
96
|
+
export async function isConsentBannerVisible(page: Page): Promise<boolean> {
|
|
97
|
+
try {
|
|
98
|
+
for (const sel of CONSENT_SELECTORS.slice(0, 5)) {
|
|
99
|
+
const el = await page.$(sel);
|
|
100
|
+
if (el && await el.isVisible()) return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|