@detiq/crawler 1.1.20 → 1.1.22
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 +5 -0
- package/dist/crawler.js +48 -10
- package/dist/crawler.js.map +3 -3
- package/dist/job-queue.js +7 -1
- package/dist/job-queue.js.map +2 -2
- package/package.json +1 -1
- package/src/crawler.ts +37 -6
- package/src/job-queue.ts +98 -86
- package/types/job-queue.d.ts +3 -1
package/Dockerfile
CHANGED
|
@@ -36,6 +36,11 @@ COPY packages/vscode-extension/package.json packages/vscode-extension/
|
|
|
36
36
|
COPY packages/web/package.json packages/web/
|
|
37
37
|
RUN npm ci --include=dev
|
|
38
38
|
|
|
39
|
+
# Patch Stagehand's act() schema to add `additionalProperties: false` on the
|
|
40
|
+
# inner anyOf[0] object. OpenAI strict mode (v1/responses) requires it on every
|
|
41
|
+
# nested object; Stagehand ≤3.6 omits it, causing a 400 "invalid_json_schema".
|
|
42
|
+
RUN node -e "const fs=require('fs');['packages/crawler/node_modules/@browserbasehq/stagehand/dist/cjs/lib/inference.js','packages/crawler/node_modules/@browserbasehq/stagehand/dist/esm/lib/inference.js'].forEach(f=>{if(!fs.existsSync(f)){console.log('skip',f);return;}let c=fs.readFileSync(f,'utf8');if(c.includes('.strict()')){console.log('already patched',f);return;}c=c.replace(/\\.nullable\\(\\)\\n(\\s+)\\.describe\\(\"The element to act on/g,'\.strict()\n\$1.nullable()\n\$1.describe(\"The element to act on');fs.writeFileSync(f,c);console.log('patched',f);});"
|
|
43
|
+
|
|
39
44
|
# Re-install chromium after npm ci so the browser revision matches the
|
|
40
45
|
# exact playwright version resolved by the lockfile (not the base image's
|
|
41
46
|
# bundled version, which may differ if the lockfile was updated).
|
package/dist/crawler.js
CHANGED
|
@@ -73,7 +73,7 @@ const _crawlLLMCache = /* @__PURE__ */ new Map();
|
|
|
73
73
|
async function getCachedCrawlLLM(tenantId) {
|
|
74
74
|
if (_crawlLLMCache.has(tenantId)) return _crawlLLMCache.get(tenantId);
|
|
75
75
|
const cfg = await getTaskAgentLLMConfig(tenantId, "crawl").catch(() => null);
|
|
76
|
-
_crawlLLMCache.set(tenantId, cfg);
|
|
76
|
+
if (cfg) _crawlLLMCache.set(tenantId, cfg);
|
|
77
77
|
return cfg;
|
|
78
78
|
}
|
|
79
79
|
function getGitInfo() {
|
|
@@ -88,7 +88,7 @@ function getGitInfo() {
|
|
|
88
88
|
const ALLOWED_NUMERIC_PATHS = /* @__PURE__ */ new Set(["/2fa", "/404", "/500", "/403", "/401"]);
|
|
89
89
|
const UUID_PATH_SEGMENT_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
90
90
|
const CUID_PATH_SEGMENT_RE = /^c[a-z0-9]{20,}$/i;
|
|
91
|
-
const NUMERIC_ID_PATH_SEGMENT_RE = /^\d
|
|
91
|
+
const NUMERIC_ID_PATH_SEGMENT_RE = /^\d{4,}$/;
|
|
92
92
|
const COMPOUND_NUMERIC_ID_PATH_SEGMENT_RE = /^\d+-\d+$/;
|
|
93
93
|
function normalizePathForNumericGuard(pathname) {
|
|
94
94
|
const normalized = pathname.replace(/\/+$/, "") || "/";
|
|
@@ -374,6 +374,7 @@ const TwoFASchema = z.object({
|
|
|
374
374
|
has2FA: z.boolean(),
|
|
375
375
|
fieldLabel: z.string().optional()
|
|
376
376
|
});
|
|
377
|
+
const INLINE_AUTH_WALL_RE = /your login session (was|has been) expired|session (has )?expired[^.]*click|please (log ?in|sign ?in) to continue|you (are|were) logged out|your session has timed out/i;
|
|
377
378
|
async function crawlProject(opts, onProgress) {
|
|
378
379
|
const tuning = await getCrawlTuning();
|
|
379
380
|
if (opts.screenshotDir) await fs.mkdir(opts.screenshotDir, { recursive: true });
|
|
@@ -1597,6 +1598,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
1597
1598
|
};
|
|
1598
1599
|
const edges = [];
|
|
1599
1600
|
const pendingTransitions = [];
|
|
1601
|
+
const pendingSummaryWrites = [];
|
|
1600
1602
|
const discovered = [];
|
|
1601
1603
|
const performanceMetrics = [];
|
|
1602
1604
|
const seenDomHashes = new Set(opts.knownDomHashes ?? []);
|
|
@@ -1681,7 +1683,6 @@ async function crawlProject(opts, onProgress) {
|
|
|
1681
1683
|
const name = pathParts.map((p) => p.charAt(0).toUpperCase() + p.slice(1).replace(/[-_]/g, " ")).join(" \u2014 ") || "Home";
|
|
1682
1684
|
const screen = await AppBrain.saveScreen(opts.tenantId, opts.projectId, name, url, opts.jobId, discoveryMethodFor(url));
|
|
1683
1685
|
visited.set(canonUrl, screen.id);
|
|
1684
|
-
const INLINE_AUTH_WALL_RE = /your login session (was|has been) expired|session (has )?expired[^.]*click|please (log ?in|sign ?in) to continue|you (are|were) logged out|your session has timed out/i;
|
|
1685
1686
|
const lwBodyText = await workerPage.evaluate(() => document.body?.innerText ?? "").catch(() => "");
|
|
1686
1687
|
if (INLINE_AUTH_WALL_RE.test(lwBodyText)) {
|
|
1687
1688
|
logger.warn({ url, screenId: screen.id }, "[lite-worker] Inline session-expired content detected \u2014 marking requiresAuth");
|
|
@@ -2875,9 +2876,9 @@ async function crawlProject(opts, onProgress) {
|
|
|
2875
2876
|
})();
|
|
2876
2877
|
}
|
|
2877
2878
|
}
|
|
2878
|
-
const
|
|
2879
|
+
const INLINE_AUTH_WALL_RE2 = /your login session (was|has been) expired|session (has )?expired[^.]*click|please (log ?in|sign ?in) to continue|you (are|were) logged out|your session has timed out/i;
|
|
2879
2880
|
const pageBodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
|
|
2880
|
-
if (
|
|
2881
|
+
if (INLINE_AUTH_WALL_RE2.test(pageBodyText)) {
|
|
2881
2882
|
logger.warn({ url, screenId: screen.id }, "[crawler] Inline session-expired content detected \u2014 skipping DOM snapshot, marking requiresAuth");
|
|
2882
2883
|
AppBrain.updateScreenRequiresAuth(opts.tenantId, screen.id, true).catch(() => {
|
|
2883
2884
|
});
|
|
@@ -2932,6 +2933,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
2932
2933
|
const summaryPromise = (async () => {
|
|
2933
2934
|
try {
|
|
2934
2935
|
const llmCfg = await getCachedCrawlLLM(opts.tenantId);
|
|
2936
|
+
if (!llmCfg) return void 0;
|
|
2935
2937
|
const snippet = extracted.markdown.slice(0, 1200);
|
|
2936
2938
|
if (snippet.trim().length < 80) return void 0;
|
|
2937
2939
|
const summarySeed = deterministicSeed(`${opts.tenantId}:${opts.projectId ?? ""}:page-summary:${url}`);
|
|
@@ -2972,10 +2974,15 @@ ${snippet}`,
|
|
|
2972
2974
|
markdown: fitMarkdown ?? extracted.markdown,
|
|
2973
2975
|
heading: extracted.title || name
|
|
2974
2976
|
}).catch((err) => console.warn("[crawler] markdown extraction save failed:", err));
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2977
|
+
pendingSummaryWrites.push(
|
|
2978
|
+
summaryPromise.then((aiSummary) => {
|
|
2979
|
+
if (aiSummary) AppBrain.updateScreenAiSummary(opts.tenantId, screen.id, aiSummary).catch((e) => {
|
|
2980
|
+
logger.warn({ screenId: screen.id, err: String(e?.message ?? e) }, "[crawler] aiSummary DB save failed");
|
|
2981
|
+
});
|
|
2982
|
+
}).catch((e) => {
|
|
2983
|
+
logger.warn({ screenId: screen.id, err: String(e?.message ?? e) }, "[crawler] aiSummary generation failed");
|
|
2984
|
+
})
|
|
2985
|
+
);
|
|
2979
2986
|
}
|
|
2980
2987
|
const structuredJson = summarizeStructuredElements(atElements);
|
|
2981
2988
|
const artifact = buildCrawlArtifactEnvelope({
|
|
@@ -3413,7 +3420,7 @@ ${snippet}`,
|
|
|
3413
3420
|
if (liteWorkerPromises.length > 0) await Promise.all(liteWorkerPromises);
|
|
3414
3421
|
if (opts.discoveryMode) {
|
|
3415
3422
|
return {
|
|
3416
|
-
discoveredUrls: Array.from(visited.values()),
|
|
3423
|
+
discoveredUrls: Array.from(visited.values()).filter((v) => v.startsWith("http")),
|
|
3417
3424
|
screensFound: 0,
|
|
3418
3425
|
screensErrored: 0,
|
|
3419
3426
|
screensSkippedToNextSegment: 0,
|
|
@@ -3614,6 +3621,13 @@ ${snippet}`,
|
|
|
3614
3621
|
await AppBrain.saveElement(opts.tenantId, opts.projectId, existingScreenId, el.meaning, el.role, el.expectedData, el.notes, void 0, true, el.boundingRect, el.ariaState, el.parentLandmark).catch(() => {
|
|
3615
3622
|
});
|
|
3616
3623
|
}
|
|
3624
|
+
} else {
|
|
3625
|
+
const p3aBodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
|
|
3626
|
+
if (INLINE_AUTH_WALL_RE.test(p3aBodyText)) {
|
|
3627
|
+
logger.warn({ url: revisitUrl, screenId: existingScreenId }, "[P3a] Re-visit got 0 elements \u2014 inline session-expired content, marking requiresAuth");
|
|
3628
|
+
AppBrain.updateScreenRequiresAuth(opts.tenantId, existingScreenId, true).catch(() => {
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3617
3631
|
}
|
|
3618
3632
|
const revisitLinks = await discoverLinks(stagehand, origin, page, linkLLM ?? void 0, false, rawPage ?? void 0).catch(() => []);
|
|
3619
3633
|
for (const href of revisitLinks) {
|
|
@@ -3753,6 +3767,14 @@ ${snippet}`,
|
|
|
3753
3767
|
await AppBrain.saveElement(opts.tenantId, opts.projectId, gapScreen.id, el.meaning, el.role, el.expectedData, el.notes, void 0, true, el.boundingRect, el.ariaState, el.parentLandmark).catch(() => {
|
|
3754
3768
|
});
|
|
3755
3769
|
}
|
|
3770
|
+
if (gapElems.length === 0) {
|
|
3771
|
+
const gapBodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
|
|
3772
|
+
if (INLINE_AUTH_WALL_RE.test(gapBodyText)) {
|
|
3773
|
+
logger.warn({ url: gapUrl, screenId: gapScreen.id }, "[P6] Gap URL got 0 elements \u2014 inline session-expired content, marking requiresAuth");
|
|
3774
|
+
AppBrain.updateScreenRequiresAuth(opts.tenantId, gapScreen.id, true).catch(() => {
|
|
3775
|
+
});
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3756
3778
|
visited.set(canonicalize(gapUrl), gapScreen.id);
|
|
3757
3779
|
discovered.push({ url: gapUrl, screenId: gapScreen.id, elements: gapElems.length, version: gapScreen.version });
|
|
3758
3780
|
for (const lm of gapLinkMaps) {
|
|
@@ -4134,6 +4156,12 @@ ${snippet}`,
|
|
|
4134
4156
|
}
|
|
4135
4157
|
const screensSucceeded = discovered.filter((d) => !!d.screenId).length;
|
|
4136
4158
|
const screensErrored = discovered.filter((d) => !!d.error && !d.screenId).length;
|
|
4159
|
+
if (pendingSummaryWrites.length > 0) {
|
|
4160
|
+
await Promise.race([
|
|
4161
|
+
Promise.allSettled(pendingSummaryWrites),
|
|
4162
|
+
new Promise((resolve) => setTimeout(resolve, Math.min(6e4, pendingSummaryWrites.length * 3e3)))
|
|
4163
|
+
]);
|
|
4164
|
+
}
|
|
4137
4165
|
if (screensSucceeded >= 3) {
|
|
4138
4166
|
const screensWithElements = discovered.filter((d) => (d.elements ?? 0) > 0 && d.screenId).length;
|
|
4139
4167
|
if (screensWithElements === 0) {
|
|
@@ -5867,6 +5895,16 @@ async function captureAuthDiscoveryScreen(page, opts, requestedUrl, source, ordi
|
|
|
5867
5895
|
await AppBrain.updateScreenDomSnapshot(opts.tenantId, screen.id, authDomPath, authDomCloudUrl).catch(() => {
|
|
5868
5896
|
});
|
|
5869
5897
|
}
|
|
5898
|
+
const authDiscoveryElements = await readScreen(page).catch(() => []);
|
|
5899
|
+
if (authDiscoveryElements.length > 0) {
|
|
5900
|
+
await AppBrain.clearScreenElements(opts.tenantId, opts.projectId, screen.id).catch(() => {
|
|
5901
|
+
});
|
|
5902
|
+
for (const el of authDiscoveryElements) {
|
|
5903
|
+
await AppBrain.saveElement(opts.tenantId, opts.projectId, screen.id, el.label, el.role, el.expectedData, void 0, void 0, true, void 0, void 0, void 0).catch(() => {
|
|
5904
|
+
});
|
|
5905
|
+
}
|
|
5906
|
+
logger.info({ screenId: screen.id, elements: authDiscoveryElements.length }, "[crawler] auth-discovery: saved elements");
|
|
5907
|
+
}
|
|
5870
5908
|
await AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
5871
5909
|
screenId: screen.id,
|
|
5872
5910
|
jobId: opts.jobId,
|