webcrawl 1.2.4 → 1.2.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.
Files changed (114) hide show
  1. package/dist/cli.js +3 -2
  2. package/dist/core/engine/cache.d.ts +70 -0
  3. package/dist/core/engine/cache.js +297 -0
  4. package/dist/core/engine/crawl-policy.d.ts +36 -0
  5. package/dist/core/engine/crawl-policy.js +134 -0
  6. package/dist/core/engine/crawler.d.ts +151 -0
  7. package/dist/core/engine/crawler.js +885 -0
  8. package/dist/core/engine/fetcher.d.ts +66 -0
  9. package/dist/core/engine/fetcher.js +185 -0
  10. package/dist/core/engine/index.d.ts +4 -0
  11. package/dist/core/engine/index.js +4 -0
  12. package/dist/core/engine/markdown.d.ts +33 -0
  13. package/dist/core/engine/markdown.js +334 -0
  14. package/dist/core/engine/path-scope.d.ts +21 -0
  15. package/dist/core/engine/path-scope.js +88 -0
  16. package/dist/core/engine/safe-url.d.ts +23 -0
  17. package/dist/core/engine/safe-url.js +92 -0
  18. package/dist/core/engine/stealth.d.ts +79 -0
  19. package/dist/core/engine/stealth.js +155 -0
  20. package/dist/core/engine.d.ts +1 -0
  21. package/dist/core/engine.js +1 -0
  22. package/dist/core/extract/business.d.ts +28 -0
  23. package/dist/core/extract/business.js +344 -0
  24. package/dist/core/extract/currency.d.ts +15 -0
  25. package/dist/core/extract/currency.js +259 -0
  26. package/dist/core/extract/cve-cache.d.ts +62 -0
  27. package/dist/core/extract/cve-cache.js +277 -0
  28. package/dist/core/extract/cve.d.ts +57 -0
  29. package/dist/core/extract/cve.js +397 -0
  30. package/dist/core/extract/dedupe-key.d.ts +12 -0
  31. package/dist/core/extract/dedupe-key.js +36 -0
  32. package/dist/core/extract/flights.d.ts +37 -0
  33. package/dist/core/extract/flights.js +373 -0
  34. package/dist/core/extract/hotels.d.ts +35 -0
  35. package/dist/core/extract/hotels.js +473 -0
  36. package/dist/core/extract/index.d.ts +8 -0
  37. package/dist/core/extract/index.js +8 -0
  38. package/dist/core/extract/real-estate.d.ts +53 -0
  39. package/dist/core/extract/real-estate.js +835 -0
  40. package/dist/core/extract/scaledown.d.ts +14 -0
  41. package/dist/core/extract/scaledown.js +484 -0
  42. package/dist/core/extract/shopping.d.ts +44 -0
  43. package/dist/core/extract/shopping.js +393 -0
  44. package/dist/core/image-url.d.ts +5 -0
  45. package/dist/core/image-url.js +13 -0
  46. package/dist/core/interact/driver.d.ts +49 -0
  47. package/dist/core/interact/driver.js +264 -0
  48. package/dist/core/interact/remote.d.ts +41 -0
  49. package/dist/core/interact/remote.js +133 -0
  50. package/dist/core/interact/spans.d.ts +46 -0
  51. package/dist/core/interact/spans.js +32 -0
  52. package/dist/core/interact/validate.d.ts +34 -0
  53. package/dist/core/interact/validate.js +135 -0
  54. package/dist/core/interact/vm.d.ts +42 -0
  55. package/dist/core/interact/vm.js +296 -0
  56. package/dist/core/local-engine.d.ts +35 -0
  57. package/dist/core/local-engine.js +367 -0
  58. package/dist/core/parse/detect.d.ts +29 -0
  59. package/dist/core/parse/detect.js +152 -0
  60. package/dist/core/parse/fetch.d.ts +20 -0
  61. package/dist/core/parse/fetch.js +39 -0
  62. package/dist/core/parse/image.d.ts +13 -0
  63. package/dist/core/parse/image.js +100 -0
  64. package/dist/core/parse/index.d.ts +53 -0
  65. package/dist/core/parse/index.js +153 -0
  66. package/dist/core/parse/limits.d.ts +12 -0
  67. package/dist/core/parse/limits.js +29 -0
  68. package/dist/core/parse/media.d.ts +42 -0
  69. package/dist/core/parse/media.js +238 -0
  70. package/dist/core/parse/merge.d.ts +19 -0
  71. package/dist/core/parse/merge.js +48 -0
  72. package/dist/core/parse/ocr.d.ts +25 -0
  73. package/dist/core/parse/ocr.js +121 -0
  74. package/dist/core/parse/office.d.ts +28 -0
  75. package/dist/core/parse/office.js +83 -0
  76. package/dist/core/parse/pdf.d.ts +27 -0
  77. package/dist/core/parse/pdf.js +156 -0
  78. package/dist/core/parse/types.d.ts +38 -0
  79. package/dist/core/parse/types.js +1 -0
  80. package/dist/core/parse/youtube.d.ts +30 -0
  81. package/dist/core/parse/youtube.js +264 -0
  82. package/dist/core/ranker/decision.d.ts +28 -0
  83. package/dist/core/ranker/decision.js +151 -0
  84. package/dist/core/ranker/index.d.ts +5 -0
  85. package/dist/core/ranker/index.js +361 -0
  86. package/dist/core/ranker/lexical.d.ts +82 -0
  87. package/dist/core/ranker/lexical.js +642 -0
  88. package/dist/core/ranker/types.d.ts +90 -0
  89. package/dist/core/ranker/types.js +1 -0
  90. package/dist/core/ranker.d.ts +1 -0
  91. package/dist/core/ranker.js +1 -0
  92. package/dist/core/sandbox.d.ts +29 -0
  93. package/dist/core/sandbox.js +182 -0
  94. package/dist/core/search/catalog.d.ts +174 -0
  95. package/dist/core/search/catalog.js +488 -0
  96. package/dist/core/search/index.d.ts +3 -0
  97. package/dist/core/search/index.js +3 -0
  98. package/dist/core/search/pipeline.d.ts +7 -0
  99. package/dist/core/search/pipeline.js +704 -0
  100. package/dist/core/search/providers.d.ts +62 -0
  101. package/dist/core/search/providers.js +1175 -0
  102. package/dist/core/search.d.ts +1 -0
  103. package/dist/core/search.js +1 -0
  104. package/dist/core/targets/developer.d.ts +44 -0
  105. package/dist/core/targets/developer.js +338 -0
  106. package/dist/core/targets/index.d.ts +3 -0
  107. package/dist/core/targets/index.js +3 -0
  108. package/dist/core/targets/registry.d.ts +32 -0
  109. package/dist/core/targets/registry.js +411 -0
  110. package/dist/core/targets/types.d.ts +72 -0
  111. package/dist/core/targets/types.js +8 -0
  112. package/dist/core/targets.d.ts +1 -0
  113. package/dist/core/targets.js +1 -0
  114. package/package.json +82 -4
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Input checks for /v1/interact. Pure functions so they can be unit-tested
3
+ * and reused by the API route before a workflow run is started.
4
+ */
5
+ export const INTERACT_ACTION_TYPES = [
6
+ "click",
7
+ "fill",
8
+ "type",
9
+ "hover",
10
+ "select",
11
+ "press",
12
+ "wait",
13
+ "scroll",
14
+ "evaluate",
15
+ "extract",
16
+ ];
17
+ export const MAX_INTERACT_ACTIONS = 20;
18
+ /** Browser session length: 2 min by default (first minute in the base price), 10 min max. */
19
+ export const DEFAULT_SESSION_SECONDS = 120;
20
+ export const MAX_SESSION_SECONDS = 600;
21
+ const NEEDS_SELECTOR = new Set(["click", "fill", "type", "hover", "select", "extract"]);
22
+ const ALIASES = {
23
+ input: "fill",
24
+ write: "fill",
25
+ keypress: "press",
26
+ scrape: "extract",
27
+ wait_for: "wait",
28
+ waitfor: "wait",
29
+ js: "evaluate",
30
+ execute: "evaluate",
31
+ };
32
+ const str = (v) => typeof v === "string" ? v : typeof v === "number" ? String(v) : undefined;
33
+ const num = (v) => {
34
+ const n = typeof v === "string" ? Number(v) : v;
35
+ return typeof n === "number" && Number.isFinite(n) ? n : undefined;
36
+ };
37
+ export function normalizeActionType(raw) {
38
+ const t = String(raw ?? "").trim().toLowerCase();
39
+ if (INTERACT_ACTION_TYPES.includes(t))
40
+ return t;
41
+ return ALIASES[t];
42
+ }
43
+ /** Human label used in the trace, e.g. `Click #submit`. */
44
+ export function describeAction(a) {
45
+ switch (a.type) {
46
+ case "wait":
47
+ return a.selector ? "Wait for element" : `Wait ${a.ms ?? 500}ms`;
48
+ case "scroll":
49
+ return a.selector ? "Scroll to element" : `Scroll ${a.y ?? 800}px`;
50
+ case "press":
51
+ return `Press ${a.key ?? "Enter"}`;
52
+ case "fill":
53
+ return "Fill";
54
+ case "evaluate":
55
+ return "Run script";
56
+ default:
57
+ return a.type.charAt(0).toUpperCase() + a.type.slice(1);
58
+ }
59
+ }
60
+ export function validateInteractRequest(body) {
61
+ const rawUrl = str(body.url)?.trim() ?? "";
62
+ if (!rawUrl)
63
+ return { ok: false, error: "Missing `url` parameter" };
64
+ let url;
65
+ try {
66
+ const parsed = new URL(/^https?:\/\//i.test(rawUrl) ? rawUrl : `https://${rawUrl}`);
67
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
68
+ throw new Error();
69
+ url = parsed.toString();
70
+ }
71
+ catch {
72
+ return { ok: false, error: `Invalid url: ${rawUrl}` };
73
+ }
74
+ const rawActions = Array.isArray(body.actions)
75
+ ? body.actions
76
+ : body.selector
77
+ ? [{ type: "extract", selector: body.selector }]
78
+ : [];
79
+ if (rawActions.length > MAX_INTERACT_ACTIONS) {
80
+ return { ok: false, error: `Too many actions (${rawActions.length}). The limit is ${MAX_INTERACT_ACTIONS}.` };
81
+ }
82
+ const actions = [];
83
+ for (let i = 0; i < rawActions.length; i++) {
84
+ const raw = rawActions[i];
85
+ const a = typeof raw === "object" && raw !== null ? raw : { type: raw };
86
+ const type = normalizeActionType(a.type ?? a.action);
87
+ const at = `actions[${i}]`;
88
+ if (!type) {
89
+ return {
90
+ ok: false,
91
+ error: `${at}: unknown type ${JSON.stringify(a.type ?? a.action ?? null)}. Valid types: ${INTERACT_ACTION_TYPES.join(", ")}.`,
92
+ };
93
+ }
94
+ const selector = str(a.selector)?.trim() || undefined;
95
+ if (NEEDS_SELECTOR.has(type) && !selector) {
96
+ return { ok: false, error: `${at}: \`${type}\` needs a \`selector\`.` };
97
+ }
98
+ const action = { type };
99
+ if (selector)
100
+ action.selector = selector.slice(0, 500);
101
+ const text = str(a.text) ?? str(a.value);
102
+ if (type === "fill" || type === "type") {
103
+ if (text === undefined)
104
+ return { ok: false, error: `${at}: \`${type}\` needs \`text\`.` };
105
+ action.text = text.slice(0, 5000);
106
+ }
107
+ if (type === "select") {
108
+ if (text === undefined)
109
+ return { ok: false, error: `${at}: \`select\` needs a \`value\`.` };
110
+ action.value = text.slice(0, 500);
111
+ }
112
+ if (type === "press")
113
+ action.key = (str(a.key) ?? "Enter").slice(0, 40);
114
+ if (type === "wait" && !selector) {
115
+ action.ms = Math.max(0, Math.min(num(a.ms ?? a.milliseconds) ?? 500, 15_000));
116
+ }
117
+ if (type === "scroll" && !selector)
118
+ action.y = Math.max(-20_000, Math.min(num(a.y ?? a.amount) ?? 800, 20_000));
119
+ if (type === "evaluate") {
120
+ const script = str(a.script ?? a.expression ?? a.code);
121
+ if (!script)
122
+ return { ok: false, error: `${at}: \`evaluate\` needs a \`script\`.` };
123
+ if (script.length > 10_000)
124
+ return { ok: false, error: `${at}: script is longer than 10,000 characters.` };
125
+ action.script = script;
126
+ }
127
+ const timeoutMs = num(a.timeoutMs ?? a.timeout);
128
+ if (timeoutMs !== undefined)
129
+ action.timeoutMs = Math.max(500, Math.min(timeoutMs, 30_000));
130
+ actions.push(action);
131
+ }
132
+ const rawSession = num(body.sessionSeconds ?? body.timeoutSeconds);
133
+ const sessionSeconds = Math.round(Math.max(30, Math.min(rawSession ?? DEFAULT_SESSION_SECONDS, MAX_SESSION_SECONDS)));
134
+ return { ok: true, url, actions, sessionSeconds };
135
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Scripts that run inside the interact sandbox VM.
3
+ *
4
+ * Layout inside the VM:
5
+ * /opt/wc/node_modules/playwright Playwright client (used over CDP only)
6
+ * /opt/wc/step.mjs runs ONE action against the live browser
7
+ * Chromium listens on 127.0.0.1:9222 (started detached, survives across commands)
8
+ *
9
+ * The step runner is plain JS so it can also run locally against a desktop
10
+ * Chromium in tests (set WC_PLAYWRIGHT / WC_CDP).
11
+ */
12
+ export declare const VM_ROOT = "/opt/wc";
13
+ export declare const CDP_PORT = 9222;
14
+ export declare const STEP_SCRIPT_PATH = "/opt/wc/step.mjs";
15
+ /** One-time VM setup. Baked into a snapshot; also used as a slow fallback. */
16
+ export declare const SETUP_SCRIPT: string;
17
+ export declare function chromeArgs(opts?: {
18
+ userAgent?: string;
19
+ width?: number;
20
+ height?: number;
21
+ }): string[];
22
+ /** Waits until the CDP endpoint answers. Exit 1 if it never does. */
23
+ export declare const WAIT_FOR_CDP_SCRIPT = "for i in $(seq 1 50); do curl -sf http://127.0.0.1:9222/json/version >/dev/null && echo up && exit 0; sleep 0.2; done; echo \"browser did not start\" >&2; exit 1";
24
+ /**
25
+ * Step runner. Usage: node step.mjs '<json action>'
26
+ * Prints exactly one line: `WCRESULT {"ok":true,...}`.
27
+ * Never throws: failures are reported as { ok: false, error }.
28
+ */
29
+ export declare const STEP_SCRIPT: string;
30
+ export type StepResultLine = {
31
+ ok: boolean;
32
+ url?: string;
33
+ title?: string;
34
+ error?: string;
35
+ data?: {
36
+ status?: number | null;
37
+ html?: string;
38
+ value?: unknown;
39
+ };
40
+ };
41
+ /** Parses the WCRESULT line emitted by STEP_SCRIPT. */
42
+ export declare function parseStepOutput(stdout: string, stderr?: string): StepResultLine;
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Scripts that run inside the interact sandbox VM.
3
+ *
4
+ * Layout inside the VM:
5
+ * /opt/wc/node_modules/playwright Playwright client (used over CDP only)
6
+ * /opt/wc/step.mjs runs ONE action against the live browser
7
+ * Chromium listens on 127.0.0.1:9222 (started detached, survives across commands)
8
+ *
9
+ * The step runner is plain JS so it can also run locally against a desktop
10
+ * Chromium in tests (set WC_PLAYWRIGHT / WC_CDP).
11
+ */
12
+ export const VM_ROOT = "/opt/wc";
13
+ export const CDP_PORT = 9222;
14
+ export const STEP_SCRIPT_PATH = `${VM_ROOT}/step.mjs`;
15
+ /** Shared libraries Chromium needs on Amazon Linux 2023 (sandbox base image). */
16
+ const CHROMIUM_DNF_DEPS = [
17
+ "nss",
18
+ "nspr",
19
+ "atk",
20
+ "at-spi2-atk",
21
+ "cups-libs",
22
+ "libdrm",
23
+ "libXcomposite",
24
+ "libXdamage",
25
+ "libXrandr",
26
+ "libXfixes",
27
+ "mesa-libgbm",
28
+ "pango",
29
+ "alsa-lib",
30
+ "libxkbcommon",
31
+ "libxshmfence",
32
+ ].join(" ");
33
+ /** One-time VM setup. Baked into a snapshot; also used as a slow fallback. */
34
+ export const SETUP_SCRIPT = [
35
+ "set -e",
36
+ `dnf install -y ${CHROMIUM_DNF_DEPS} >/tmp/wc-dnf.log 2>&1`,
37
+ `mkdir -p ${VM_ROOT} && cd ${VM_ROOT}`,
38
+ "[ -f package.json ] || npm init -y >/dev/null",
39
+ "npm i playwright@1 >/tmp/wc-npm.log 2>&1",
40
+ "npx playwright install chromium >/tmp/wc-pw.log 2>&1",
41
+ `node -e "console.log(require('playwright').chromium.executablePath())" > ${VM_ROOT}/chrome-path`,
42
+ `cat ${VM_ROOT}/chrome-path`,
43
+ ].join(" && ");
44
+ export function chromeArgs(opts = {}) {
45
+ const args = [
46
+ "--headless=new",
47
+ "--no-sandbox",
48
+ "--disable-gpu",
49
+ "--disable-dev-shm-usage",
50
+ "--no-first-run",
51
+ "--no-default-browser-check",
52
+ `--remote-debugging-port=${CDP_PORT}`,
53
+ "--remote-debugging-address=127.0.0.1",
54
+ "--user-data-dir=/tmp/wc-profile",
55
+ // Hide the automation flag (navigator.webdriver) that bot checks look for.
56
+ "--disable-blink-features=AutomationControlled",
57
+ `--window-size=${opts.width ?? 1280},${opts.height ?? 800}`,
58
+ ];
59
+ if (opts.userAgent)
60
+ args.push(`--user-agent=${opts.userAgent}`);
61
+ args.push("about:blank");
62
+ return args;
63
+ }
64
+ /** Waits until the CDP endpoint answers. Exit 1 if it never does. */
65
+ export const WAIT_FOR_CDP_SCRIPT = `for i in $(seq 1 50); do curl -sf http://127.0.0.1:${CDP_PORT}/json/version >/dev/null && echo up && exit 0; sleep 0.2; done; echo "browser did not start" >&2; exit 1`;
66
+ /**
67
+ * Step runner. Usage: node step.mjs '<json action>'
68
+ * Prints exactly one line: `WCRESULT {"ok":true,...}`.
69
+ * Never throws: failures are reported as { ok: false, error }.
70
+ */
71
+ export const STEP_SCRIPT = String.raw `
72
+ const PW = process.env.WC_PLAYWRIGHT || "/opt/wc/node_modules/playwright/index.mjs";
73
+ const CDP = process.env.WC_CDP || "http://127.0.0.1:9222";
74
+ const { chromium } = await import(PW);
75
+
76
+ const action = JSON.parse(process.argv[2] || "{}");
77
+ const timeout = Math.max(500, Math.min(Number(action.timeoutMs) || 10000, 30000));
78
+ const out = (r) => { process.stdout.write("WCRESULT " + JSON.stringify(r) + "\n"); };
79
+
80
+ let browser;
81
+ try {
82
+ const cdpHeaders = process.env.WC_CDP_HEADERS ? JSON.parse(process.env.WC_CDP_HEADERS) : undefined;
83
+ browser = await chromium.connectOverCDP(CDP, { timeout: 15000, ...(cdpHeaders ? { headers: cdpHeaders } : {}) });
84
+ const ctx = browser.contexts()[0] ?? (await browser.newContext());
85
+ const page = ctx.pages()[0] ?? (await ctx.newPage());
86
+ page.setDefaultTimeout(timeout);
87
+ const sel = action.selector ? String(action.selector) : undefined;
88
+ const need = () => { if (!sel) throw new Error("selector is required for " + action.type); return sel; };
89
+ // Fail fast with a clear message when the selector matches nothing.
90
+ const locate = async () => {
91
+ const loc = page.locator(need()).first();
92
+ try {
93
+ await loc.waitFor({ state: "attached", timeout });
94
+ } catch {
95
+ throw new Error("No element matches selector " + JSON.stringify(sel));
96
+ }
97
+ return loc;
98
+ };
99
+ let data;
100
+
101
+ switch (action.type) {
102
+ case "goto": {
103
+ const resp = await page.goto(String(action.url), { waitUntil: "domcontentloaded", timeout: Math.max(timeout, 20000) });
104
+ data = { status: resp ? resp.status() : null };
105
+ break;
106
+ }
107
+ case "wait": {
108
+ if (sel) await (await locate()).waitFor({ state: "visible", timeout });
109
+ else await page.waitForTimeout(Math.max(0, Math.min(Number(action.ms) || 500, 15000)));
110
+ break;
111
+ }
112
+ case "click": await (await locate()).click({ timeout }); break;
113
+ case "hover": await (await locate()).hover({ timeout }); break;
114
+ case "fill": await (await locate()).fill(String(action.text ?? action.value ?? ""), { timeout }); break;
115
+ case "type": await (await locate()).pressSequentially(String(action.text ?? action.value ?? ""), { delay: 20, timeout }); break;
116
+ case "select": await (await locate()).selectOption(String(action.value ?? action.text ?? ""), { timeout }); break;
117
+ case "press": {
118
+ const key = String(action.key || "Enter");
119
+ if (sel) await (await locate()).press(key, { timeout });
120
+ else await page.keyboard.press(key);
121
+ break;
122
+ }
123
+ case "scroll": {
124
+ if (sel) await (await locate()).scrollIntoViewIfNeeded({ timeout });
125
+ else await page.mouse.wheel(0, Number(action.y ?? action.amount ?? 800));
126
+ await page.waitForTimeout(300);
127
+ break;
128
+ }
129
+ case "evaluate": {
130
+ const script = String(action.script ?? action.expression ?? "");
131
+ if (!script) throw new Error("script is required for evaluate");
132
+ const value = await page.evaluate(script);
133
+ let json = JSON.stringify(value ?? null);
134
+ if (json.length > 8192) json = JSON.stringify(json.slice(0, 8192) + "…");
135
+ data = { value: JSON.parse(json) };
136
+ break;
137
+ }
138
+ case "extract": {
139
+ const loc = await locate();
140
+ const html = await loc.evaluate((el) => el.outerHTML);
141
+ data = { html: html.slice(0, 2000000) };
142
+ break;
143
+ }
144
+ case "snapshot": {
145
+ try { await page.waitForLoadState("networkidle", { timeout: 3000 }); } catch {}
146
+ data = { html: (await page.content()).slice(0, 4000000) };
147
+ break;
148
+ }
149
+ case "fetchMany": {
150
+ // Stealth fetch: pass the bot check once on the entry page in a real browser,
151
+ // then read every item with the cleared session (same IP, cookies, TLS).
152
+ const items = Array.isArray(action.items) ? action.items.slice(0, 40) : [];
153
+ const budget = Math.max(5000, Math.min(Number(action.budgetMs) || 60000, 240000));
154
+ const solveMs = Math.max(2000, Math.min(Number(action.solveMs) || 20000, 45000));
155
+ const PER_DOC = 1500000;
156
+ const TOTAL = 10000000;
157
+ const started = Date.now();
158
+ const markers = ["vercel security checkpoint", "just a moment...", "cf-browser-verification", "challenge-platform", "_cf_chl_opt", "enable javascript and cookies to continue", "_vercel/security", "attention required! | cloudflare"];
159
+ const looksBlocked = (s) => { const l = String(s || "").slice(0, 8000).toLowerCase(); return markers.some((m) => l.includes(m)); };
160
+ let ua = "";
161
+ try {
162
+ const cdp = await ctx.newCDPSession(page);
163
+ const ver = await cdp.send("Browser.getVersion");
164
+ ua = String(ver.userAgent || "").replace(/HeadlessChrome/g, "Chrome");
165
+ // A remote provider's browser already has a coherent fingerprint; overriding it would break that.
166
+ if (action.keepFingerprint) throw new Error("keep");
167
+ const major = (ua.match(/Chrome\/(\d+)/) || [])[1] || "140";
168
+ const full = (ua.match(/Chrome\/([\d.]+)/) || [])[1] || major + ".0.0.0";
169
+ await cdp.send("Emulation.setUserAgentOverride", {
170
+ userAgent: ua,
171
+ acceptLanguage: "en-US,en;q=0.9",
172
+ platform: "Linux x86_64",
173
+ userAgentMetadata: {
174
+ brands: [{ brand: "Chromium", version: major }, { brand: "Google Chrome", version: major }, { brand: "Not.A/Brand", version: "99" }],
175
+ fullVersion: full, platform: "Linux", platformVersion: "6.1.0", architecture: "x86", model: "", mobile: false,
176
+ },
177
+ });
178
+ await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: "Object.defineProperty(Navigator.prototype,'webdriver',{get:()=>false});" });
179
+ } catch {}
180
+ // Remote browsers bill proxy traffic per GB: skip bytes markdown never uses.
181
+ if (action.lightweight) {
182
+ const skip = new Set(["image", "media", "font"]);
183
+ await page.route("**/*", (r) => (skip.has(r.request().resourceType()) ? r.abort() : r.continue())).catch(() => {});
184
+ }
185
+ let navStatus = null;
186
+ page.on("response", (r) => { try { if (r.request().isNavigationRequest() && r.frame() === page.mainFrame()) navStatus = r.status(); } catch {} });
187
+ const render = async (url, wait) => {
188
+ navStatus = null;
189
+ try { await page.goto(url, { waitUntil: "domcontentloaded", timeout: Math.min(30000, wait + 10000) }); } catch (e) { if (navStatus === null) throw e; }
190
+ const deadline = Date.now() + wait;
191
+ let html = "";
192
+ for (;;) {
193
+ html = await page.content().catch(() => "");
194
+ if ((html && !looksBlocked(html)) || Date.now() > deadline) break;
195
+ await page.waitForLoadState("load", { timeout: 2000 }).catch(() => {});
196
+ await page.waitForTimeout(700);
197
+ }
198
+ try { await page.waitForLoadState("networkidle", { timeout: 2500 }); } catch {}
199
+ html = await page.content().catch(() => html);
200
+ const blocked = looksBlocked(html);
201
+ return { status: blocked ? navStatus : (navStatus && navStatus < 400 ? navStatus : 200), contentType: "text/html", body: html, finalUrl: page.url(), title: await page.title().catch(() => ""), blocked };
202
+ };
203
+ const raw = async (url) => {
204
+ let sameOrigin = false;
205
+ try { sameOrigin = new URL(url).origin === new URL(page.url()).origin; } catch {}
206
+ if (sameOrigin) {
207
+ const r = await page.evaluate(async (u) => {
208
+ const res = await fetch(u, { credentials: "include", redirect: "follow" });
209
+ return { status: res.status, contentType: res.headers.get("content-type") || "", body: await res.text(), finalUrl: res.url };
210
+ }, url);
211
+ return { ...r, title: "", blocked: r.status === 429 || looksBlocked(r.body) };
212
+ }
213
+ const res = await ctx.request.get(url, { timeout: 15000 });
214
+ const body = await res.text();
215
+ return { status: res.status(), contentType: res.headers()["content-type"] || "", body, finalUrl: res.url(), title: "", blocked: res.status() === 429 || looksBlocked(body) };
216
+ };
217
+ let total = 0;
218
+ const clip = (d) => {
219
+ const room = Math.max(0, Math.min(PER_DOC, TOTAL - total));
220
+ const body = String(d.body || "").slice(0, room);
221
+ total += body.length;
222
+ return { ...d, body };
223
+ };
224
+ let entry = null;
225
+ if (action.entry) {
226
+ try { entry = clip(await render(String(action.entry), solveMs)); } catch (e) { entry = { blocked: true, error: String(e && e.message || e).split("\n")[0].slice(0, 300) }; }
227
+ }
228
+ const docs = [];
229
+ const queue = items.slice();
230
+ const seen = new Set(queue.map((it) => String(it && it.url || "")));
231
+ const maxFollow = Math.max(0, Math.min(Number(action.followSitemaps) || 0, 20));
232
+ let followed = 0;
233
+ // Queue sitemaps named in robots.txt or a sitemap index, read in this same session.
234
+ const follow = (d) => {
235
+ if (!maxFollow || !d.body || d.blocked || (d.status || 200) >= 400) return;
236
+ const found = [];
237
+ if (/\/robots\.txt(\?|$)/i.test(d.url)) {
238
+ for (const m of String(d.body).matchAll(/^\s*Sitemap:\s*(\S+)/gim)) found.push(m[1].trim());
239
+ } else if (/<sitemapindex[\s>]/i.test(d.body)) {
240
+ for (const m of String(d.body).matchAll(/<loc>\s*(?:<!\[CDATA\[)?([^<\]]+?)(?:\]\]>)?\s*<\/loc>/gi)) found.push(m[1].trim().replace(/&amp;/g, "&"));
241
+ }
242
+ for (const u of found) {
243
+ if (followed >= maxFollow || seen.has(u)) continue;
244
+ seen.add(u); followed++;
245
+ queue.push({ url: u, raw: true });
246
+ }
247
+ };
248
+ while (queue.length) {
249
+ const it = queue.shift();
250
+ const url = String(it && it.url || "");
251
+ if (!url) continue;
252
+ if (Date.now() - started > budget) { docs.push({ url, ok: false, error: "Ran out of time" }); continue; }
253
+ try {
254
+ let d = it.raw ? await raw(url) : await render(url, Math.min(solveMs, 10000));
255
+ // A raw read that still hits the check gets one rendered attempt.
256
+ if (d.blocked && it.raw) d = await render(url, Math.min(solveMs, 10000));
257
+ const doc = { url, ok: true, ...clip(d) };
258
+ docs.push(doc);
259
+ follow(doc);
260
+ } catch (e) {
261
+ docs.push({ url, ok: false, error: String(e && e.message || e).split("\n")[0].slice(0, 300) });
262
+ }
263
+ }
264
+ data = { value: { entry, docs, solved: entry ? !entry.blocked : true, userAgent: ua } };
265
+ break;
266
+ }
267
+ default:
268
+ throw new Error("Unsupported action type " + JSON.stringify(action.type));
269
+ }
270
+
271
+ out({ ok: true, url: page.url(), title: await page.title().catch(() => ""), data });
272
+ } catch (err) {
273
+ const msg = err && err.message ? String(err.message).split("\n")[0] : String(err);
274
+ out({ ok: false, error: msg.slice(0, 500) });
275
+ } finally {
276
+ // Disconnect only; the browser keeps running for the next step.
277
+ if (browser) await browser.close().catch(() => {});
278
+ }
279
+ `;
280
+ /** Parses the WCRESULT line emitted by STEP_SCRIPT. */
281
+ export function parseStepOutput(stdout, stderr = "") {
282
+ const line = stdout
283
+ .split("\n")
284
+ .reverse()
285
+ .find((l) => l.startsWith("WCRESULT "));
286
+ if (!line) {
287
+ const tail = (stderr || stdout).trim().split("\n").slice(-3).join(" ").slice(0, 400);
288
+ return { ok: false, error: tail || "Step produced no result" };
289
+ }
290
+ try {
291
+ return JSON.parse(line.slice("WCRESULT ".length));
292
+ }
293
+ catch {
294
+ return { ok: false, error: "Step result was not valid JSON" };
295
+ }
296
+ }
@@ -0,0 +1,35 @@
1
+ export interface LocalEngineBackend {
2
+ readonly baseUrl: string;
3
+ search(p: Record<string, unknown>): Promise<Record<string, unknown>>;
4
+ scrape(p: {
5
+ url: string;
6
+ formats?: Array<"markdown" | "links" | "html">;
7
+ noCache?: boolean;
8
+ }): Promise<Record<string, unknown>>;
9
+ parse(p: Record<string, unknown>): Promise<Record<string, unknown>>;
10
+ run(id: string): Promise<Record<string, unknown>>;
11
+ map(p: {
12
+ url: string;
13
+ limit?: number;
14
+ search?: string;
15
+ }): Promise<Record<string, unknown>>;
16
+ crawl(p: {
17
+ url: string;
18
+ maxPages?: number;
19
+ extractStructured?: boolean;
20
+ }): Promise<Record<string, unknown>>;
21
+ extract(p: {
22
+ url: string;
23
+ prompt?: string;
24
+ schema?: Record<string, unknown>;
25
+ }): Promise<Record<string, unknown>>;
26
+ rerank(p: {
27
+ query: string;
28
+ documents: Array<string | Record<string, unknown>>;
29
+ topK?: number;
30
+ }): Promise<Record<string, unknown>>;
31
+ categories(): Promise<Record<string, unknown>>;
32
+ targets(): Promise<Record<string, unknown>>;
33
+ usage(): Promise<Record<string, unknown>>;
34
+ }
35
+ export declare function createLocalEngineClient(): LocalEngineBackend;