@detiq/crawler 1.0.4 → 1.0.5
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.d.ts +14 -0
- package/dist/crawler.js +170 -39
- package/dist/crawler.js.map +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +2 -2
- package/dist/job-queue.d.ts +7 -0
- package/dist/job-queue.js +43 -8
- package/dist/job-queue.js.map +2 -2
- package/dist/phash.d.ts +12 -0
- package/dist/phash.js +21 -0
- package/dist/phash.js.map +2 -2
- package/dist/session-capture.d.ts +4 -1
- package/dist/stealth/human-behavior.js +12 -11
- package/dist/stealth/human-behavior.js.map +2 -2
- package/dist/test-executor.d.ts +8 -1
- package/dist/visual-diff-verdict.d.ts +1 -1
- package/package.json +1 -1
- package/src/crawler.ts +98 -27
- package/src/job-queue.ts +50 -4
- package/types/index.d.ts +2 -0
- package/types/job-queue.d.ts +7 -0
- package/types/phash.d.ts +12 -0
package/dist/phash.d.ts
CHANGED
|
@@ -12,6 +12,18 @@ export declare function applyMask(imagePath: string, ignoredRegions: Array<{
|
|
|
12
12
|
width: number;
|
|
13
13
|
height: number;
|
|
14
14
|
}>): Promise<Buffer>;
|
|
15
|
+
/**
|
|
16
|
+
* Generates a pixelmatch diff image highlighting changed pixels in red.
|
|
17
|
+
* Applies the same ignoredRegions mask as screenshotDiffPercent so masked
|
|
18
|
+
* zones don't show as "changed" in the visual output.
|
|
19
|
+
* Returns a PNG Buffer suitable for upload to S3.
|
|
20
|
+
*/
|
|
21
|
+
export declare function generatePixelmatchDiff(pathA: string, pathB: string, ignoredRegions?: Array<{
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}>): Promise<Buffer>;
|
|
15
27
|
export declare function screenshotDiffPercent(pathA: string, pathB: string, ignoredRegions?: Array<{
|
|
16
28
|
x: number;
|
|
17
29
|
y: number;
|
package/dist/phash.js
CHANGED
|
@@ -31,6 +31,26 @@ function hammingDistance(a, b) {
|
|
|
31
31
|
}
|
|
32
32
|
return count;
|
|
33
33
|
}
|
|
34
|
+
async function generatePixelmatchDiff(pathA, pathB, ignoredRegions = []) {
|
|
35
|
+
const metaA = await sharp(pathA).metadata();
|
|
36
|
+
const W = metaA.width ?? 1200;
|
|
37
|
+
const H = metaA.height ?? 800;
|
|
38
|
+
const toRawRGBA = async (path) => {
|
|
39
|
+
const base = ignoredRegions.length > 0 ? sharp(await applyMask(path, ignoredRegions)) : sharp(path);
|
|
40
|
+
const { data } = await base.resize(W, H, { fit: "fill" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
41
|
+
return data;
|
|
42
|
+
};
|
|
43
|
+
const [rawA, rawB] = await Promise.all([toRawRGBA(pathA), toRawRGBA(pathB)]);
|
|
44
|
+
const { default: pixelmatch } = await import("pixelmatch");
|
|
45
|
+
const diffData = Buffer.alloc(W * H * 4);
|
|
46
|
+
pixelmatch(rawA, rawB, diffData, W, H, {
|
|
47
|
+
threshold: 0.1,
|
|
48
|
+
alpha: 0.3,
|
|
49
|
+
diffColor: [255, 0, 0],
|
|
50
|
+
aaColor: [255, 255, 0]
|
|
51
|
+
});
|
|
52
|
+
return sharp(diffData, { raw: { width: W, height: H, channels: 4 } }).png().toBuffer();
|
|
53
|
+
}
|
|
34
54
|
async function screenshotDiffPercent(pathA, pathB, ignoredRegions = []) {
|
|
35
55
|
const [hashA, hashB] = await Promise.all([
|
|
36
56
|
hashWithMasks(pathA, ignoredRegions),
|
|
@@ -109,6 +129,7 @@ async function gridLayoutDiff(pathA, pathB, ignoredRegions = []) {
|
|
|
109
129
|
}
|
|
110
130
|
export {
|
|
111
131
|
applyMask,
|
|
132
|
+
generatePixelmatchDiff,
|
|
112
133
|
gridLayoutDiff,
|
|
113
134
|
screenshotDiffPercent
|
|
114
135
|
};
|
package/dist/phash.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/phash.ts"],
|
|
4
|
-
"sourcesContent": ["import sharp from 'sharp';\nimport type { OverlayOptions } from 'sharp';\n\n/**\n * Composites neutral grey (128,128,128) over each ignoredRegion (fractional\n * 0-1 coordinates) so masked content \u2014 timestamps, live counters, ads \u2014\n * contributes nothing to any downstream pixel comparison. Shared by every\n * comparison path (whole-image hash, per-cell grid hash, AI vision) so a\n * region masked once is actually ignored everywhere, not just in the\n * cheapest check.\n */\nexport async function applyMask(\n imagePath: string,\n ignoredRegions: Array<{ x: number; y: number; width: number; height: number }>,\n): Promise<Buffer> {\n const meta = await sharp(imagePath).metadata();\n const W = meta.width ?? 1200;\n const H = meta.height ?? 800;\n\n const overlays: OverlayOptions[] = await Promise.all(\n ignoredRegions.map(async (r) => {\n const rW = Math.max(1, Math.round(r.width * W));\n const rH = Math.max(1, Math.round(r.height * H));\n const buf = await sharp({\n create: { width: rW, height: rH, channels: 3, background: { r: 128, g: 128, b: 128 } },\n }).png().toBuffer();\n return { input: buf, top: Math.round(r.y * H), left: Math.round(r.x * W) } as OverlayOptions;\n }),\n );\n\n return sharp(imagePath).composite(overlays).png().toBuffer();\n}\n\n/**\n * Compute an 8x8 average hash (aHash) of an image file, with optional mask overlays.\n * ignoredRegions are fractional (0-1 range), converted to pixels here.\n * Masked regions are filled with neutral grey (128,128,128) before hashing.\n */\nasync function hashWithMasks(\n imagePath: string,\n ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],\n): Promise<bigint> {\n const pipeline = ignoredRegions.length > 0\n ? sharp(await applyMask(imagePath, ignoredRegions))\n : sharp(imagePath);\n\n const buf = await pipeline.resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer();\n const pixels = Array.from(buf);\n const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;\n return pixels.reduce((h, p, i) => (p >= mean ? h | (1n << BigInt(63 - i)) : h), 0n);\n}\n\nfunction hammingDistance(a: bigint, b: bigint): number {\n let xor = a ^ b;\n let count = 0;\n while (xor !== 0n) { count += Number(xor & 1n); xor >>= 1n; }\n return count;\n}\n\nexport async function screenshotDiffPercent(\n pathA: string,\n pathB: string,\n ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],\n): Promise<{ pHashDistance: number; changeLevel: 'IDENTICAL' | 'MINOR' | 'MODERATE' | 'SIGNIFICANT' }> {\n const [hashA, hashB] = await Promise.all([\n hashWithMasks(pathA, ignoredRegions),\n hashWithMasks(pathB, ignoredRegions),\n ]);\n let xor = hashA ^ hashB;\n let dist = 0;\n while (xor !== 0n) { dist += Number(xor & 1n); xor >>= 1n; }\n const pct = parseFloat(((dist / 64) * 100).toFixed(1));\n const changeLevel = pct === 0 ? 'IDENTICAL' : pct < 10 ? 'MINOR' : pct < 30 ? 'MODERATE' : 'SIGNIFICANT';\n return { pHashDistance: pct, changeLevel };\n}\n\n/**\n * Grid-based layout analysis: divides screenshots into a 3\u00D73 grid and computes\n * pHash per cell. Returns which regions changed and where, for the VisualDiff.changes field.\n * Uses sharp's `extract` to crop each cell before hashing.\n */\nexport async function gridLayoutDiff(\n pathA: string,\n pathB: string,\n ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],\n): Promise<Array<{ location: string; severity: string; description: string }>> {\n const GRID_LABELS = [\n 'top-left', 'top-center', 'top-right',\n 'mid-left', 'mid-center', 'mid-right',\n 'bottom-left', 'bottom-center', 'bottom-right',\n ];\n\n try {\n // Mask before cropping into cells \u2014 a masked region (e.g. a timestamp)\n // then contributes nothing to any cell's hash, instead of only being\n // ignored by the separate whole-image hash in screenshotDiffPercent().\n const [sourceA, sourceB] = ignoredRegions.length > 0\n ? await Promise.all([applyMask(pathA, ignoredRegions), applyMask(pathB, ignoredRegions)])\n : [pathA, pathB];\n\n const [metaA, metaB] = await Promise.all([\n sharp(sourceA).metadata(),\n sharp(sourceB).metadata(),\n ]);\n const W = Math.min(metaA.width ?? 1200, metaB.width ?? 1200);\n const H = Math.min(metaA.height ?? 800, metaB.height ?? 800);\n const cW = Math.floor(W / 3);\n const cH = Math.floor(H / 3);\n\n const changes: Array<{ location: string; severity: string; description: string }> = [];\n\n for (let row = 0; row < 3; row++) {\n for (let col = 0; col < 3; col++) {\n const region = { left: col * cW, top: row * cH, width: cW, height: cH };\n const label = GRID_LABELS[row * 3 + col];\n\n const [hashA, hashB] = await Promise.all([\n sharp(sourceA).extract(region).resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer()\n .then((buf) => {\n const pixels = Array.from(buf);\n const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;\n return pixels.reduce((h, p, i) => p >= mean ? h | (1n << BigInt(63 - i)) : h, 0n);\n }),\n sharp(sourceB).extract(region).resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer()\n .then((buf) => {\n const pixels = Array.from(buf);\n const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;\n return pixels.reduce((h, p, i) => p >= mean ? h | (1n << BigInt(63 - i)) : h, 0n);\n }),\n ]);\n\n let xor = hashA ^ hashB;\n let dist = 0;\n while (xor !== 0n) { dist += Number(xor & 1n); xor >>= 1n; }\n const pct = (dist / 64) * 100;\n\n if (pct >= 5) {\n const severity = pct >= 20 ? 'HIGH' : pct >= 10 ? 'MEDIUM' : 'LOW';\n changes.push({\n location: label,\n severity,\n description: `${Math.round(pct)}% visual change in ${label.replace('-', ' ')} region`,\n });\n }\n }\n }\n return changes;\n } catch {\n return [];\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import sharp from 'sharp';\nimport type { OverlayOptions } from 'sharp';\n\n/**\n * Composites neutral grey (128,128,128) over each ignoredRegion (fractional\n * 0-1 coordinates) so masked content \u2014 timestamps, live counters, ads \u2014\n * contributes nothing to any downstream pixel comparison. Shared by every\n * comparison path (whole-image hash, per-cell grid hash, AI vision) so a\n * region masked once is actually ignored everywhere, not just in the\n * cheapest check.\n */\nexport async function applyMask(\n imagePath: string,\n ignoredRegions: Array<{ x: number; y: number; width: number; height: number }>,\n): Promise<Buffer> {\n const meta = await sharp(imagePath).metadata();\n const W = meta.width ?? 1200;\n const H = meta.height ?? 800;\n\n const overlays: OverlayOptions[] = await Promise.all(\n ignoredRegions.map(async (r) => {\n const rW = Math.max(1, Math.round(r.width * W));\n const rH = Math.max(1, Math.round(r.height * H));\n const buf = await sharp({\n create: { width: rW, height: rH, channels: 3, background: { r: 128, g: 128, b: 128 } },\n }).png().toBuffer();\n return { input: buf, top: Math.round(r.y * H), left: Math.round(r.x * W) } as OverlayOptions;\n }),\n );\n\n return sharp(imagePath).composite(overlays).png().toBuffer();\n}\n\n/**\n * Compute an 8x8 average hash (aHash) of an image file, with optional mask overlays.\n * ignoredRegions are fractional (0-1 range), converted to pixels here.\n * Masked regions are filled with neutral grey (128,128,128) before hashing.\n */\nasync function hashWithMasks(\n imagePath: string,\n ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],\n): Promise<bigint> {\n const pipeline = ignoredRegions.length > 0\n ? sharp(await applyMask(imagePath, ignoredRegions))\n : sharp(imagePath);\n\n const buf = await pipeline.resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer();\n const pixels = Array.from(buf);\n const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;\n return pixels.reduce((h, p, i) => (p >= mean ? h | (1n << BigInt(63 - i)) : h), 0n);\n}\n\nfunction hammingDistance(a: bigint, b: bigint): number {\n let xor = a ^ b;\n let count = 0;\n while (xor !== 0n) { count += Number(xor & 1n); xor >>= 1n; }\n return count;\n}\n\n/**\n * Generates a pixelmatch diff image highlighting changed pixels in red.\n * Applies the same ignoredRegions mask as screenshotDiffPercent so masked\n * zones don't show as \"changed\" in the visual output.\n * Returns a PNG Buffer suitable for upload to S3.\n */\nexport async function generatePixelmatchDiff(\n pathA: string,\n pathB: string,\n ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],\n): Promise<Buffer> {\n const metaA = await sharp(pathA).metadata();\n const W = metaA.width ?? 1200;\n const H = metaA.height ?? 800;\n\n const toRawRGBA = async (path: string): Promise<Buffer> => {\n const base = ignoredRegions.length > 0 ? sharp(await applyMask(path, ignoredRegions)) : sharp(path);\n const { data } = await base.resize(W, H, { fit: 'fill' }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });\n return data;\n };\n\n const [rawA, rawB] = await Promise.all([toRawRGBA(pathA), toRawRGBA(pathB)]);\n const { default: pixelmatch } = await import('pixelmatch');\n const diffData = Buffer.alloc(W * H * 4);\n pixelmatch(rawA, rawB, diffData, W, H, {\n threshold: 0.1,\n alpha: 0.3,\n diffColor: [255, 0, 0],\n aaColor: [255, 255, 0],\n });\n return sharp(diffData, { raw: { width: W, height: H, channels: 4 } }).png().toBuffer();\n}\n\nexport async function screenshotDiffPercent(\n pathA: string,\n pathB: string,\n ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],\n): Promise<{ pHashDistance: number; changeLevel: 'IDENTICAL' | 'MINOR' | 'MODERATE' | 'SIGNIFICANT' }> {\n const [hashA, hashB] = await Promise.all([\n hashWithMasks(pathA, ignoredRegions),\n hashWithMasks(pathB, ignoredRegions),\n ]);\n let xor = hashA ^ hashB;\n let dist = 0;\n while (xor !== 0n) { dist += Number(xor & 1n); xor >>= 1n; }\n const pct = parseFloat(((dist / 64) * 100).toFixed(1));\n const changeLevel = pct === 0 ? 'IDENTICAL' : pct < 10 ? 'MINOR' : pct < 30 ? 'MODERATE' : 'SIGNIFICANT';\n return { pHashDistance: pct, changeLevel };\n}\n\n/**\n * Grid-based layout analysis: divides screenshots into a 3\u00D73 grid and computes\n * pHash per cell. Returns which regions changed and where, for the VisualDiff.changes field.\n * Uses sharp's `extract` to crop each cell before hashing.\n */\nexport async function gridLayoutDiff(\n pathA: string,\n pathB: string,\n ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],\n): Promise<Array<{ location: string; severity: string; description: string }>> {\n const GRID_LABELS = [\n 'top-left', 'top-center', 'top-right',\n 'mid-left', 'mid-center', 'mid-right',\n 'bottom-left', 'bottom-center', 'bottom-right',\n ];\n\n try {\n // Mask before cropping into cells \u2014 a masked region (e.g. a timestamp)\n // then contributes nothing to any cell's hash, instead of only being\n // ignored by the separate whole-image hash in screenshotDiffPercent().\n const [sourceA, sourceB] = ignoredRegions.length > 0\n ? await Promise.all([applyMask(pathA, ignoredRegions), applyMask(pathB, ignoredRegions)])\n : [pathA, pathB];\n\n const [metaA, metaB] = await Promise.all([\n sharp(sourceA).metadata(),\n sharp(sourceB).metadata(),\n ]);\n const W = Math.min(metaA.width ?? 1200, metaB.width ?? 1200);\n const H = Math.min(metaA.height ?? 800, metaB.height ?? 800);\n const cW = Math.floor(W / 3);\n const cH = Math.floor(H / 3);\n\n const changes: Array<{ location: string; severity: string; description: string }> = [];\n\n for (let row = 0; row < 3; row++) {\n for (let col = 0; col < 3; col++) {\n const region = { left: col * cW, top: row * cH, width: cW, height: cH };\n const label = GRID_LABELS[row * 3 + col];\n\n const [hashA, hashB] = await Promise.all([\n sharp(sourceA).extract(region).resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer()\n .then((buf) => {\n const pixels = Array.from(buf);\n const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;\n return pixels.reduce((h, p, i) => p >= mean ? h | (1n << BigInt(63 - i)) : h, 0n);\n }),\n sharp(sourceB).extract(region).resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer()\n .then((buf) => {\n const pixels = Array.from(buf);\n const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;\n return pixels.reduce((h, p, i) => p >= mean ? h | (1n << BigInt(63 - i)) : h, 0n);\n }),\n ]);\n\n let xor = hashA ^ hashB;\n let dist = 0;\n while (xor !== 0n) { dist += Number(xor & 1n); xor >>= 1n; }\n const pct = (dist / 64) * 100;\n\n if (pct >= 5) {\n const severity = pct >= 20 ? 'HIGH' : pct >= 10 ? 'MEDIUM' : 'LOW';\n changes.push({\n location: label,\n severity,\n description: `${Math.round(pct)}% visual change in ${label.replace('-', ' ')} region`,\n });\n }\n }\n }\n return changes;\n } catch {\n return [];\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -28,9 +28,12 @@ export declare function validateAndCaptureSession(appUrl: string, creds: {
|
|
|
28
28
|
}, llmConfig?: {
|
|
29
29
|
modelName: string;
|
|
30
30
|
apiKey: string;
|
|
31
|
+
}, captchaOpts?: {
|
|
32
|
+
apiKey?: string;
|
|
33
|
+
provider?: '2captcha' | 'capsolver';
|
|
31
34
|
}): Promise<{
|
|
32
35
|
storageState: string;
|
|
33
36
|
cookies: number;
|
|
34
37
|
localStorage: number;
|
|
35
|
-
finalUrl
|
|
38
|
+
finalUrl?: string;
|
|
36
39
|
}>;
|
|
@@ -89,18 +89,11 @@ async function injectFingerprintOverrides(page) {
|
|
|
89
89
|
async function infiniteScrollToBottom(page, opts = {}) {
|
|
90
90
|
const { maxScrolls = 50, scrollDelay = 400, stabilizeMs = 600 } = opts;
|
|
91
91
|
let scrollCount = 0;
|
|
92
|
-
let
|
|
93
|
-
let
|
|
92
|
+
let stableScrollCount = 0;
|
|
93
|
+
let previousHeight = 0;
|
|
94
94
|
try {
|
|
95
95
|
while (scrollCount < maxScrolls) {
|
|
96
|
-
|
|
97
|
-
if (currentHeight === lastHeight) {
|
|
98
|
-
sameHeightCount++;
|
|
99
|
-
if (sameHeightCount >= 3) break;
|
|
100
|
-
} else {
|
|
101
|
-
sameHeightCount = 0;
|
|
102
|
-
lastHeight = currentHeight;
|
|
103
|
-
}
|
|
96
|
+
previousHeight = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0);
|
|
104
97
|
const scrollBy = 500 + Math.floor(Math.random() * 300);
|
|
105
98
|
await page.evaluate((by) => window.scrollBy(0, by), scrollBy).catch(() => {
|
|
106
99
|
});
|
|
@@ -108,10 +101,18 @@ async function infiniteScrollToBottom(page, opts = {}) {
|
|
|
108
101
|
await page.waitForTimeout(stabilizeMs).catch(() => {
|
|
109
102
|
});
|
|
110
103
|
scrollCount++;
|
|
104
|
+
const newHeight = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0);
|
|
105
|
+
if (newHeight - previousHeight < 10) {
|
|
106
|
+
stableScrollCount++;
|
|
107
|
+
if (stableScrollCount >= 3) break;
|
|
108
|
+
} else {
|
|
109
|
+
stableScrollCount = 0;
|
|
110
|
+
}
|
|
111
|
+
previousHeight = newHeight;
|
|
111
112
|
}
|
|
112
113
|
} catch {
|
|
113
114
|
}
|
|
114
|
-
return { scrollCount, heightReached:
|
|
115
|
+
return { scrollCount, heightReached: previousHeight };
|
|
115
116
|
}
|
|
116
117
|
async function extractIframeContent(page) {
|
|
117
118
|
const results = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/stealth/human-behavior.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Page } from 'playwright';\n\n// Gaussian random using Box-Muller transform\nfunction gaussian(mean: number, sigma: number): number {\n const u1 = Math.random();\n const u2 = Math.random();\n const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n return mean + z * sigma;\n}\n\nexport async function humanDelay(min = 80, max = 450): Promise<void> {\n const mean = (min + max) / 2;\n const sigma = (max - min) / 6;\n const delay = Math.max(min, Math.min(max, gaussian(mean, sigma)));\n await new Promise(r => setTimeout(r, Math.round(delay)));\n}\n\nexport async function simulateMouseMovement(page: Page, targetX: number, targetY: number): Promise<void> {\n try {\n const mouse = page.mouse;\n const steps = 8 + Math.floor(Math.random() * 8);\n // Generate Bezier control points with slight perpendicular offset\n const startX = targetX - 100 + Math.random() * 200;\n const startY = targetY - 100 + Math.random() * 200;\n const cpX = (startX + targetX) / 2 + (Math.random() - 0.5) * 80;\n const cpY = (startY + targetY) / 2 + (Math.random() - 0.5) * 80;\n\n for (let i = 0; i <= steps; i++) {\n const t = i / steps;\n // Quadratic Bezier: B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2\n const x = (1 - t) * (1 - t) * startX + 2 * (1 - t) * t * cpX + t * t * targetX;\n const y = (1 - t) * (1 - t) * startY + 2 * (1 - t) * t * cpY + t * t * targetY;\n await mouse.move(x + (Math.random() - 0.5) * 2, y + (Math.random() - 0.5) * 2);\n if (i < steps) await humanDelay(8, 25);\n }\n // Small overshoot correction\n await mouse.move(targetX + (Math.random() - 0.5) * 4, targetY + (Math.random() - 0.5) * 4);\n await humanDelay(30, 80);\n await mouse.move(targetX, targetY);\n } catch { /* non-fatal */ }\n}\n\nexport async function simulateMouseHover(page: Page, selector: string): Promise<void> {\n try {\n const el = await page.$(selector);\n if (!el) return;\n const box = await el.boundingBox();\n if (!box) return;\n const cx = box.x + box.width / 2;\n const cy = box.y + box.height / 2;\n await simulateMouseMovement(page, cx, cy);\n await humanDelay(100, 300);\n } catch { /* non-fatal */ }\n}\n\nexport async function simulateClick(page: Page, selector: string): Promise<void> {\n try {\n await simulateMouseHover(page, selector);\n await humanDelay(50, 150);\n await page.click(selector, { delay: Math.round(gaussian(80, 30)) });\n } catch { /* non-fatal */ }\n}\n\nexport async function injectFingerprintOverrides(page: Page): Promise<void> {\n await page.addInitScript(() => {\n // Canvas noise \u2014 subtle per-pixel perturbation\n const origToDataURL = HTMLCanvasElement.prototype.toDataURL;\n HTMLCanvasElement.prototype.toDataURL = function(this: HTMLCanvasElement, ...args: any[]) {\n const ctx = this.getContext('2d');\n if (ctx) {\n const imageData = ctx.getImageData(0, 0, this.width, this.height);\n for (let i = 0; i < imageData.data.length; i += 4) {\n imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + (Math.random() < 0.01 ? (Math.random() > 0.5 ? 1 : -1) : 0)));\n }\n ctx.putImageData(imageData, 0, 0);\n }\n return origToDataURL.apply(this, args as [type?: string, quality?: number]);\n };\n\n // WebGL parameter spoofing\n const origGetParam = WebGLRenderingContext.prototype.getParameter;\n WebGLRenderingContext.prototype.getParameter = function(this: WebGLRenderingContext, parameter: number) {\n const UNMASKED_VENDOR_WEBGL = 0x9245;\n const UNMASKED_RENDERER_WEBGL = 0x9246;\n if (parameter === UNMASKED_VENDOR_WEBGL) return 'Intel Inc.';\n if (parameter === UNMASKED_RENDERER_WEBGL) return 'Intel Iris OpenGL Engine';\n return origGetParam.call(this, parameter);\n };\n\n // Hardware concurrency \u2014 pick from common values\n const cpuCounts = [4, 6, 8, 8];\n Object.defineProperty(navigator, 'hardwareConcurrency', {\n get: () => cpuCounts[Math.floor(Math.random() * cpuCounts.length)],\n configurable: true,\n });\n\n // Device memory\n Object.defineProperty(navigator, 'deviceMemory', {\n get: () => [4, 8][Math.floor(Math.random() * 2)],\n configurable: true,\n });\n }).catch(() => {});\n}\n\nexport async function infiniteScrollToBottom(\n page: Page,\n opts: { maxScrolls?: number; scrollDelay?: number; stabilizeMs?: number } = {}\n): Promise<{ scrollCount: number; heightReached: number }> {\n const { maxScrolls = 50, scrollDelay = 400, stabilizeMs = 600 } = opts;\n let scrollCount = 0;\n let
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import type { Page } from 'playwright';\n\n// Gaussian random using Box-Muller transform\nfunction gaussian(mean: number, sigma: number): number {\n const u1 = Math.random();\n const u2 = Math.random();\n const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n return mean + z * sigma;\n}\n\nexport async function humanDelay(min = 80, max = 450): Promise<void> {\n const mean = (min + max) / 2;\n const sigma = (max - min) / 6;\n const delay = Math.max(min, Math.min(max, gaussian(mean, sigma)));\n await new Promise(r => setTimeout(r, Math.round(delay)));\n}\n\nexport async function simulateMouseMovement(page: Page, targetX: number, targetY: number): Promise<void> {\n try {\n const mouse = page.mouse;\n const steps = 8 + Math.floor(Math.random() * 8);\n // Generate Bezier control points with slight perpendicular offset\n const startX = targetX - 100 + Math.random() * 200;\n const startY = targetY - 100 + Math.random() * 200;\n const cpX = (startX + targetX) / 2 + (Math.random() - 0.5) * 80;\n const cpY = (startY + targetY) / 2 + (Math.random() - 0.5) * 80;\n\n for (let i = 0; i <= steps; i++) {\n const t = i / steps;\n // Quadratic Bezier: B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2\n const x = (1 - t) * (1 - t) * startX + 2 * (1 - t) * t * cpX + t * t * targetX;\n const y = (1 - t) * (1 - t) * startY + 2 * (1 - t) * t * cpY + t * t * targetY;\n await mouse.move(x + (Math.random() - 0.5) * 2, y + (Math.random() - 0.5) * 2);\n if (i < steps) await humanDelay(8, 25);\n }\n // Small overshoot correction\n await mouse.move(targetX + (Math.random() - 0.5) * 4, targetY + (Math.random() - 0.5) * 4);\n await humanDelay(30, 80);\n await mouse.move(targetX, targetY);\n } catch { /* non-fatal */ }\n}\n\nexport async function simulateMouseHover(page: Page, selector: string): Promise<void> {\n try {\n const el = await page.$(selector);\n if (!el) return;\n const box = await el.boundingBox();\n if (!box) return;\n const cx = box.x + box.width / 2;\n const cy = box.y + box.height / 2;\n await simulateMouseMovement(page, cx, cy);\n await humanDelay(100, 300);\n } catch { /* non-fatal */ }\n}\n\nexport async function simulateClick(page: Page, selector: string): Promise<void> {\n try {\n await simulateMouseHover(page, selector);\n await humanDelay(50, 150);\n await page.click(selector, { delay: Math.round(gaussian(80, 30)) });\n } catch { /* non-fatal */ }\n}\n\nexport async function injectFingerprintOverrides(page: Page): Promise<void> {\n await page.addInitScript(() => {\n // Canvas noise \u2014 subtle per-pixel perturbation\n const origToDataURL = HTMLCanvasElement.prototype.toDataURL;\n HTMLCanvasElement.prototype.toDataURL = function(this: HTMLCanvasElement, ...args: any[]) {\n const ctx = this.getContext('2d');\n if (ctx) {\n const imageData = ctx.getImageData(0, 0, this.width, this.height);\n for (let i = 0; i < imageData.data.length; i += 4) {\n imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + (Math.random() < 0.01 ? (Math.random() > 0.5 ? 1 : -1) : 0)));\n }\n ctx.putImageData(imageData, 0, 0);\n }\n return origToDataURL.apply(this, args as [type?: string, quality?: number]);\n };\n\n // WebGL parameter spoofing\n const origGetParam = WebGLRenderingContext.prototype.getParameter;\n WebGLRenderingContext.prototype.getParameter = function(this: WebGLRenderingContext, parameter: number) {\n const UNMASKED_VENDOR_WEBGL = 0x9245;\n const UNMASKED_RENDERER_WEBGL = 0x9246;\n if (parameter === UNMASKED_VENDOR_WEBGL) return 'Intel Inc.';\n if (parameter === UNMASKED_RENDERER_WEBGL) return 'Intel Iris OpenGL Engine';\n return origGetParam.call(this, parameter);\n };\n\n // Hardware concurrency \u2014 pick from common values\n const cpuCounts = [4, 6, 8, 8];\n Object.defineProperty(navigator, 'hardwareConcurrency', {\n get: () => cpuCounts[Math.floor(Math.random() * cpuCounts.length)],\n configurable: true,\n });\n\n // Device memory\n Object.defineProperty(navigator, 'deviceMemory', {\n get: () => [4, 8][Math.floor(Math.random() * 2)],\n configurable: true,\n });\n }).catch(() => {});\n}\n\nexport async function infiniteScrollToBottom(\n page: Page,\n opts: { maxScrolls?: number; scrollDelay?: number; stabilizeMs?: number } = {}\n): Promise<{ scrollCount: number; heightReached: number }> {\n const { maxScrolls = 50, scrollDelay = 400, stabilizeMs = 600 } = opts;\n let scrollCount = 0;\n let stableScrollCount = 0;\n let previousHeight = 0;\n\n try {\n while (scrollCount < maxScrolls) {\n previousHeight = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0);\n\n const scrollBy = 500 + Math.floor(Math.random() * 300);\n await page.evaluate((by: number) => window.scrollBy(0, by), scrollBy).catch(() => {});\n await humanDelay(scrollDelay - 100, scrollDelay + 200);\n\n // Wait for potential lazy-loaded content\n await page.waitForTimeout(stabilizeMs).catch(() => {});\n scrollCount++;\n\n const newHeight: number = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0);\n if (newHeight - previousHeight < 10) {\n stableScrollCount++;\n if (stableScrollCount >= 3) break; // no meaningful new content after 3 consecutive non-growing scrolls\n } else {\n stableScrollCount = 0;\n }\n previousHeight = newHeight;\n }\n } catch { /* non-fatal */ }\n\n return { scrollCount, heightReached: previousHeight };\n}\n\nexport async function extractIframeContent(page: Page): Promise<Array<{ src: string; content: string; markdown: string }>> {\n const results: Array<{ src: string; content: string; markdown: string }> = [];\n try {\n const iframeSrcs: string[] = await page.evaluate(() =>\n Array.from(document.querySelectorAll('iframe')).map(f => f.getAttribute('src') ?? f.getAttribute('data-src') ?? '')\n ).catch(() => []);\n\n for (const frame of page.frames()) {\n const src = frame.url();\n if (!src || src === 'about:blank' || src === page.url()) continue;\n try {\n const content = await frame.content().catch(() => '');\n if (!content || content.length < 50) continue;\n // Simple text extraction from iframe HTML\n const text = content.replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n results.push({ src, content: content.slice(0, 10_000), markdown: text.slice(0, 5_000) });\n } catch { /* skip this frame */ }\n }\n } catch { /* non-fatal */ }\n return results;\n}\n\nexport async function pierceShadowDom(page: Page): Promise<string> {\n try {\n return await page.evaluate(() => {\n const parts: string[] = [];\n function walkShadow(root: Element | ShadowRoot): void {\n const shadow = (root as any).shadowRoot;\n if (shadow) {\n parts.push(shadow.innerHTML ?? '');\n for (const child of [...shadow.querySelectorAll('*')]) walkShadow(child);\n }\n for (const child of Array.from(root.querySelectorAll(':not(script):not(style)'))) {\n if ((child as any).shadowRoot) walkShadow(child);\n }\n }\n walkShadow(document.body);\n return parts\n .join('\\n')\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 10_000);\n }).catch(() => '');\n } catch {\n return '';\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/test-executor.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExecutionStep } from '@
|
|
1
|
+
import type { ExecutionStep } from '@detiq/agents';
|
|
2
2
|
export interface StepResult {
|
|
3
3
|
stepNumber: number;
|
|
4
4
|
description: string;
|
|
@@ -13,6 +13,13 @@ export interface StepResult {
|
|
|
13
13
|
healedVia?: 'mechanical' | 'ai';
|
|
14
14
|
healReasoning?: string;
|
|
15
15
|
healConfidence?: number;
|
|
16
|
+
elementBoundingRect?: {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
} | null;
|
|
22
|
+
elementSelector?: string;
|
|
16
23
|
}
|
|
17
24
|
export interface ExecutionResult {
|
|
18
25
|
stepResults: StepResult[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure visual-diff verdict logic, split out of crawler.ts so it can be unit-tested
|
|
3
3
|
* without pulling in crawler.ts's full import graph (Playwright, Stagehand, and
|
|
4
|
-
* transitively @
|
|
4
|
+
* transitively @detiq/database, which instantiates a PrismaClient at module load and
|
|
5
5
|
* so requires a live DATABASE_URL just to import the module).
|
|
6
6
|
*/
|
|
7
7
|
export interface VisualDiffVerdict {
|
package/package.json
CHANGED
package/src/crawler.ts
CHANGED
|
@@ -531,46 +531,118 @@ export async function crawlProject(opts: {
|
|
|
531
531
|
|
|
532
532
|
const isDocker = !!process.env.PLAYWRIGHT_IN_DOCKER || process.env.NODE_ENV === 'production';
|
|
533
533
|
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
let
|
|
534
|
+
// Stagehand v3 uses Vercel AI SDK internally. Model format: "aisdk-provider/model-name".
|
|
535
|
+
// Each provider maps to a native AI SDK client; OpenAI-compat baseURL used for non-native ones.
|
|
536
|
+
// Provider → AI SDK prefix mapping:
|
|
537
|
+
// anthropic → anthropic, openai → openai, gemini → google, grok → xai,
|
|
538
|
+
// groq → groq (native), openrouter → openai+baseURL, ollama → ollama, nvidia → openai+baseURL
|
|
539
|
+
|
|
540
|
+
let stagehandModel = config.llmModel.includes('/') ? config.llmModel : `openai/${config.llmModel}`;
|
|
541
|
+
// clientOptions spread into Stagehand model object (apiKey, baseURL, etc.)
|
|
542
|
+
let stagehandModelOpts: Record<string, string | undefined> = {};
|
|
543
|
+
let stagehandConfigured = false;
|
|
541
544
|
let configuredProvider = '';
|
|
545
|
+
|
|
542
546
|
try {
|
|
543
547
|
// getTaskAgentLLMConfig: tenant config → platform admin config → null (proper fallback chain)
|
|
544
548
|
const crawlLLM = await getTaskAgentLLMConfig(opts.tenantId, 'crawl');
|
|
545
549
|
if (crawlLLM) {
|
|
546
550
|
configuredProvider = crawlLLM.provider;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
551
|
+
const hasKey = crawlLLM.apiKey !== undefined && crawlLLM.apiKey !== null;
|
|
552
|
+
const apiKey = hasKey ? crawlLLM.apiKey : undefined;
|
|
553
|
+
const baseUrl = crawlLLM.baseUrl || undefined;
|
|
554
|
+
const m = crawlLLM.model;
|
|
555
|
+
|
|
556
|
+
if (m && (hasKey || crawlLLM.provider === 'ollama' || crawlLLM.provider === 'bedrock' || crawlLLM.provider === 'vertex')) {
|
|
557
|
+
stagehandConfigured = true;
|
|
558
|
+
switch (crawlLLM.provider) {
|
|
559
|
+
case 'anthropic':
|
|
560
|
+
stagehandModel = m.includes('/') ? m : `anthropic/${m}`;
|
|
561
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
562
|
+
break;
|
|
563
|
+
case 'openai':
|
|
564
|
+
stagehandModel = m.includes('/') ? m : `openai/${m}`;
|
|
565
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
566
|
+
break;
|
|
567
|
+
case 'gemini':
|
|
568
|
+
stagehandModel = `google/${m}`;
|
|
569
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
570
|
+
break;
|
|
571
|
+
case 'grok':
|
|
572
|
+
stagehandModel = `xai/${m}`;
|
|
573
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
574
|
+
break;
|
|
575
|
+
case 'openrouter':
|
|
576
|
+
stagehandModel = `openai/${m}`;
|
|
577
|
+
stagehandModelOpts = { ...(apiKey ? { apiKey } : {}), baseURL: baseUrl || 'https://openrouter.ai/api/v1' };
|
|
578
|
+
break;
|
|
579
|
+
case 'ollama':
|
|
580
|
+
stagehandModel = `ollama/${m}`;
|
|
581
|
+
stagehandModelOpts = { baseURL: baseUrl || 'http://localhost:11434' };
|
|
582
|
+
break;
|
|
583
|
+
case 'nvidia':
|
|
584
|
+
stagehandModel = `openai/${m.startsWith('openai/') ? m.slice(7) : m}`;
|
|
585
|
+
stagehandModelOpts = { ...(apiKey ? { apiKey } : {}), baseURL: baseUrl || 'https://integrate.api.nvidia.com/v1' };
|
|
586
|
+
break;
|
|
587
|
+
case 'groq':
|
|
588
|
+
stagehandModel = `groq/${m}`;
|
|
589
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
590
|
+
break;
|
|
591
|
+
case 'mistral':
|
|
592
|
+
stagehandModel = `mistral/${m}`;
|
|
593
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
594
|
+
break;
|
|
595
|
+
case 'deepseek':
|
|
596
|
+
stagehandModel = `deepseek/${m}`;
|
|
597
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
598
|
+
break;
|
|
599
|
+
case 'perplexity':
|
|
600
|
+
stagehandModel = `perplexity/${m}`;
|
|
601
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
602
|
+
break;
|
|
603
|
+
case 'togetherai':
|
|
604
|
+
stagehandModel = `togetherai/${m}`;
|
|
605
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
606
|
+
break;
|
|
607
|
+
case 'bedrock':
|
|
608
|
+
// Uses AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGION from env
|
|
609
|
+
stagehandModel = `bedrock/${m}`;
|
|
610
|
+
stagehandModelOpts = {};
|
|
611
|
+
break;
|
|
612
|
+
case 'vertex':
|
|
613
|
+
// Uses GOOGLE_APPLICATION_CREDENTIALS or GOOGLE_VERTEX_PROJECT from env
|
|
614
|
+
stagehandModel = `vertex/${m}`;
|
|
615
|
+
stagehandModelOpts = {};
|
|
616
|
+
break;
|
|
617
|
+
case 'azure':
|
|
618
|
+
stagehandModel = `azure/${m}`;
|
|
619
|
+
stagehandModelOpts = { ...(apiKey ? { apiKey } : {}), ...(baseUrl ? { baseURL: baseUrl } : {}) };
|
|
620
|
+
break;
|
|
621
|
+
default:
|
|
622
|
+
stagehandModel = `openai/${m}`;
|
|
623
|
+
stagehandModelOpts = { ...(apiKey ? { apiKey } : {}), ...(baseUrl ? { baseURL: baseUrl } : {}) };
|
|
555
624
|
}
|
|
556
625
|
}
|
|
557
626
|
}
|
|
558
627
|
} catch { /* ignore DB errors, fall through to env var below */ }
|
|
559
|
-
|
|
560
|
-
if
|
|
628
|
+
|
|
629
|
+
// Env var fallback — if DB lookup failed or returned no key
|
|
630
|
+
if (!stagehandConfigured) {
|
|
561
631
|
// Honor STAGEHAND_MODEL env var to determine preferred provider.
|
|
562
|
-
// Stagehand v3 requires "provider/model" format — mismatched pair silently fails.
|
|
563
632
|
const envModel = process.env.STAGEHAND_MODEL ?? '';
|
|
564
633
|
const preferAnthropic = envModel.includes('claude') || (!envModel && !process.env.OPENAI_API_KEY);
|
|
565
634
|
if (preferAnthropic && process.env.ANTHROPIC_API_KEY) {
|
|
566
|
-
|
|
635
|
+
stagehandModelOpts = { apiKey: process.env.ANTHROPIC_API_KEY };
|
|
567
636
|
stagehandModel = envModel.includes('/') ? envModel : `anthropic/${envModel || 'claude-sonnet-4-6'}`;
|
|
637
|
+
stagehandConfigured = true;
|
|
568
638
|
} else if (process.env.OPENAI_API_KEY) {
|
|
569
|
-
|
|
639
|
+
stagehandModelOpts = { apiKey: process.env.OPENAI_API_KEY };
|
|
570
640
|
stagehandModel = envModel.includes('/') ? envModel : `openai/${envModel || 'gpt-4o'}`;
|
|
641
|
+
stagehandConfigured = true;
|
|
571
642
|
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
572
|
-
|
|
643
|
+
stagehandModelOpts = { apiKey: process.env.ANTHROPIC_API_KEY };
|
|
573
644
|
stagehandModel = envModel.includes('/') ? envModel : `anthropic/${envModel || 'claude-sonnet-4-6'}`;
|
|
645
|
+
stagehandConfigured = true;
|
|
574
646
|
}
|
|
575
647
|
}
|
|
576
648
|
|
|
@@ -606,12 +678,11 @@ export async function crawlProject(opts: {
|
|
|
606
678
|
payload: { profile: domainProfile, routeDecision },
|
|
607
679
|
}).catch((err: any) => console.warn('[crawler] profile evidence save failed:', err?.message ?? err));
|
|
608
680
|
|
|
609
|
-
if (!
|
|
610
|
-
const unsupported = configuredProvider && !['anthropic', 'openai', 'openrouter'].includes(configuredProvider);
|
|
681
|
+
if (!stagehandConfigured) {
|
|
611
682
|
throw new Error(
|
|
612
|
-
|
|
613
|
-
? `
|
|
614
|
-
: 'No AI provider configured. Go to Workspace → Settings → AI Provider to set up
|
|
683
|
+
configuredProvider
|
|
684
|
+
? `No API key configured for provider "${configuredProvider}". Go to Workspace → Settings → AI Provider.`
|
|
685
|
+
: 'No AI provider configured. Go to Workspace → Settings → AI Provider to set up an AI provider for browser crawling.'
|
|
615
686
|
);
|
|
616
687
|
}
|
|
617
688
|
|
|
@@ -770,7 +841,7 @@ export async function crawlProject(opts: {
|
|
|
770
841
|
|
|
771
842
|
const stagehand = new Stagehand({
|
|
772
843
|
env: 'LOCAL',
|
|
773
|
-
model: { modelName: stagehandModel as any,
|
|
844
|
+
model: { modelName: stagehandModel as any, ...stagehandModelOpts },
|
|
774
845
|
serverCache: true,
|
|
775
846
|
verbose: 1,
|
|
776
847
|
logger: stagehandLogger,
|
package/src/job-queue.ts
CHANGED
|
@@ -258,6 +258,33 @@ export async function requeueStaleRunningJobs(): Promise<void> {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Requeues any RUNNING jobs claimed by a different worker (i.e., a previous
|
|
263
|
+
* process instance). Call once at startup before the poll loop begins — this
|
|
264
|
+
* provides immediate recovery instead of waiting the full STALE_JOB_TIMEOUT_MINUTES.
|
|
265
|
+
* Safe to call from multiple replicas simultaneously (each uses its own WORKER_ID).
|
|
266
|
+
*/
|
|
267
|
+
export async function requeueOrphanedJobs(): Promise<void> {
|
|
268
|
+
const rows = await prismaAdmin.$queryRaw<Array<{ id: string }>>`
|
|
269
|
+
UPDATE "PlatformJob"
|
|
270
|
+
SET status = 'QUEUED',
|
|
271
|
+
"workerId" = NULL,
|
|
272
|
+
result = jsonb_set(
|
|
273
|
+
COALESCE(result::jsonb, '{}'::jsonb),
|
|
274
|
+
'{step}',
|
|
275
|
+
to_jsonb('Worker restarted — queued for recovery'::text),
|
|
276
|
+
true
|
|
277
|
+
)
|
|
278
|
+
WHERE status = 'RUNNING'
|
|
279
|
+
AND "workerId" IS NOT NULL
|
|
280
|
+
AND "workerId" != ${WORKER_ID}
|
|
281
|
+
RETURNING id
|
|
282
|
+
`;
|
|
283
|
+
if (rows.length > 0) {
|
|
284
|
+
logger.warn({ jobIds: rows.map((r: any) => r.id), workerId: WORKER_ID }, '[job-worker] requeued orphaned RUNNING job(s) from dead worker');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
261
288
|
/** Dispatches a claimed job to its type's processor. Last-resort safety net — every
|
|
262
289
|
* process*Job function already catches and records its own failures. */
|
|
263
290
|
async function dispatchClaimedJob(job: ClaimedJob): Promise<void> {
|
|
@@ -325,6 +352,11 @@ export function startJobWorkerLoop(opts?: { pollIntervalMs?: number; concurrency
|
|
|
325
352
|
};
|
|
326
353
|
|
|
327
354
|
workerLoopTimer = setInterval(tick, pollIntervalMs);
|
|
355
|
+
// On startup: immediately requeue jobs orphaned by the previous process instance.
|
|
356
|
+
// This gives instant recovery rather than waiting STALE_JOB_TIMEOUT_MINUTES (30 min).
|
|
357
|
+
requeueOrphanedJobs().catch((err) =>
|
|
358
|
+
logger.error({ err: String(err) }, '[job-worker] startup orphan sweep failed')
|
|
359
|
+
);
|
|
328
360
|
void tick();
|
|
329
361
|
}
|
|
330
362
|
|
|
@@ -402,13 +434,27 @@ export async function getJob(tenantId: string, jobId: string) {
|
|
|
402
434
|
|
|
403
435
|
/** List recent jobs for a project. */
|
|
404
436
|
export async function listJobs(tenantId: string, projectId: string) {
|
|
405
|
-
return withTenant(tenantId, (tx) =>
|
|
406
|
-
tx.platformJob.findMany({
|
|
437
|
+
return withTenant(tenantId, async (tx) => {
|
|
438
|
+
const jobs = await tx.platformJob.findMany({
|
|
407
439
|
where: { tenantId, projectId },
|
|
408
440
|
orderBy: { queuedAt: 'desc' },
|
|
409
441
|
take: 50,
|
|
410
|
-
})
|
|
411
|
-
|
|
442
|
+
});
|
|
443
|
+
// Attach the most-recently-used model for each job from LLMCallLog
|
|
444
|
+
const jobIds = jobs.map((j) => j.id);
|
|
445
|
+
if (jobIds.length === 0) return jobs;
|
|
446
|
+
const modelRows = await (tx as any).lLMCallLog.groupBy({
|
|
447
|
+
by: ['jobId'],
|
|
448
|
+
where: { tenantId, jobId: { in: jobIds } },
|
|
449
|
+
_max: { model: true, calledAt: true },
|
|
450
|
+
});
|
|
451
|
+
const modelByJobId = new Map<string, string>(
|
|
452
|
+
modelRows
|
|
453
|
+
.filter((r: any) => r.jobId && r._max?.model)
|
|
454
|
+
.map((r: any) => [r.jobId as string, r._max.model as string])
|
|
455
|
+
);
|
|
456
|
+
return jobs.map((j) => ({ ...j, llmModel: modelByJobId.get(j.id) ?? null }));
|
|
457
|
+
});
|
|
412
458
|
}
|
|
413
459
|
|
|
414
460
|
/** Cancel a RUNNING or QUEUED job. Aborts the in-process crawl and marks DB CANCELLED. */
|
package/types/index.d.ts
CHANGED
|
@@ -33,3 +33,5 @@ export type { ProxyTier, ProxyTierConfig } from './proxy.js';
|
|
|
33
33
|
export { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';
|
|
34
34
|
export { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';
|
|
35
35
|
export type { Span, FinishedSpan } from './telemetry.js';
|
|
36
|
+
export { analyzeFailure } from './failure-analysis-agent.js';
|
|
37
|
+
export type { FailureContext } from './failure-analysis-agent.js';
|
package/types/job-queue.d.ts
CHANGED
|
@@ -84,6 +84,13 @@ export declare function claimNextQueuedJob(): Promise<ClaimedJob | null>;
|
|
|
84
84
|
* died (crash, restart, OOM) without ever marking them DONE/FAILED. Idempotent and
|
|
85
85
|
* safe to run from every worker process; only affects rows past the timeout. */
|
|
86
86
|
export declare function requeueStaleRunningJobs(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Requeues any RUNNING jobs claimed by a different worker (i.e., a previous
|
|
89
|
+
* process instance). Call once at startup before the poll loop begins — this
|
|
90
|
+
* provides immediate recovery instead of waiting the full STALE_JOB_TIMEOUT_MINUTES.
|
|
91
|
+
* Safe to call from multiple replicas simultaneously (each uses its own WORKER_ID).
|
|
92
|
+
*/
|
|
93
|
+
export declare function requeueOrphanedJobs(): Promise<void>;
|
|
87
94
|
/**
|
|
88
95
|
* Starts the durable job worker poll loop. This is what makes job processing
|
|
89
96
|
* survive a restart — enqueueJob()'s immediate same-process claim attempt is
|
package/types/phash.d.ts
CHANGED
|
@@ -12,6 +12,18 @@ export declare function applyMask(imagePath: string, ignoredRegions: Array<{
|
|
|
12
12
|
width: number;
|
|
13
13
|
height: number;
|
|
14
14
|
}>): Promise<Buffer>;
|
|
15
|
+
/**
|
|
16
|
+
* Generates a pixelmatch diff image highlighting changed pixels in red.
|
|
17
|
+
* Applies the same ignoredRegions mask as screenshotDiffPercent so masked
|
|
18
|
+
* zones don't show as "changed" in the visual output.
|
|
19
|
+
* Returns a PNG Buffer suitable for upload to S3.
|
|
20
|
+
*/
|
|
21
|
+
export declare function generatePixelmatchDiff(pathA: string, pathB: string, ignoredRegions?: Array<{
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}>): Promise<Buffer>;
|
|
15
27
|
export declare function screenshotDiffPercent(pathA: string, pathB: string, ignoredRegions?: Array<{
|
|
16
28
|
x: number;
|
|
17
29
|
y: number;
|