mcp-scraper 0.38.0 → 0.38.2
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/README.md +1 -1
- package/dist/bin/api-server.cjs +813 -338
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +25 -7
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +5 -5
- package/dist/bin/paa-harvest.cjs +65 -4
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +3 -3
- package/dist/{chunk-PWPUKR5U.js → chunk-AZRPG43B.js} +12 -3
- package/dist/chunk-AZRPG43B.js.map +1 -0
- package/dist/{chunk-MA5JBAUZ.js → chunk-EGJKUB4Q.js} +3 -3
- package/dist/{chunk-MA5JBAUZ.js.map → chunk-EGJKUB4Q.js.map} +1 -1
- package/dist/{chunk-PUJFYJXB.js → chunk-FRYT3ID4.js} +2 -2
- package/dist/{chunk-CSCD2HNS.js → chunk-N7KUTTCC.js} +312 -101
- package/dist/chunk-N7KUTTCC.js.map +1 -0
- package/dist/{chunk-LOPKN3YL.js → chunk-NGM237OO.js} +66 -5
- package/dist/chunk-NGM237OO.js.map +1 -0
- package/dist/{chunk-P64PZUCA.js → chunk-NKCCGADE.js} +23 -8
- package/dist/chunk-NKCCGADE.js.map +1 -0
- package/dist/{chunk-FSAXLDB3.js → chunk-SFRMFGQ6.js} +2 -2
- package/dist/{chunk-Q35WZJJK.js → chunk-YCI2PNCS.js} +3 -3
- package/dist/chunk-YODBNTTN.js +7 -0
- package/dist/chunk-YODBNTTN.js.map +1 -0
- package/dist/{db-N3YECFWR.js → db-C5KVCOYT.js} +10 -4
- package/dist/{extract-bundle-346R6MXD.js → extract-bundle-KUBX6N6Z.js} +4 -4
- package/dist/index.cjs +65 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{location-data-repository-O2VII3ON.js → location-data-repository-TTWF3OTM.js} +3 -3
- package/dist/{server-3Q6XIJPF.js → server-5EX6XBIA.js} +320 -152
- package/dist/server-5EX6XBIA.js.map +1 -0
- package/dist/{site-extract-repository-I3VM6WXN.js → site-extract-repository-XSPJTCIL.js} +4 -4
- package/dist/{worker-TTFXPPDK.js → worker-XCPU4YSN.js} +6 -6
- package/docs/mcp-tool-manifest.generated.json +66 -9
- package/package.json +4 -2
- package/dist/chunk-CSCD2HNS.js.map +0 -1
- package/dist/chunk-H3M3O457.js +0 -7
- package/dist/chunk-H3M3O457.js.map +0 -1
- package/dist/chunk-LOPKN3YL.js.map +0 -1
- package/dist/chunk-P64PZUCA.js.map +0 -1
- package/dist/chunk-PWPUKR5U.js.map +0 -1
- package/dist/server-3Q6XIJPF.js.map +0 -1
- /package/dist/{chunk-PUJFYJXB.js.map → chunk-FRYT3ID4.js.map} +0 -0
- /package/dist/{chunk-FSAXLDB3.js.map → chunk-SFRMFGQ6.js.map} +0 -0
- /package/dist/{chunk-Q35WZJJK.js.map → chunk-YCI2PNCS.js.map} +0 -0
- /package/dist/{db-N3YECFWR.js.map → db-C5KVCOYT.js.map} +0 -0
- /package/dist/{extract-bundle-346R6MXD.js.map → extract-bundle-KUBX6N6Z.js.map} +0 -0
- /package/dist/{location-data-repository-O2VII3ON.js.map → location-data-repository-TTWF3OTM.js.map} +0 -0
- /package/dist/{site-extract-repository-I3VM6WXN.js.map → site-extract-repository-XSPJTCIL.js.map} +0 -0
- /package/dist/{worker-TTFXPPDK.js.map → worker-XCPU4YSN.js.map} +0 -0
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
importHostedCensusPlacesCsv,
|
|
10
10
|
importHostedZipGroupsCsv,
|
|
11
11
|
listActiveHostedLocationDatasets
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-FRYT3ID4.js";
|
|
13
13
|
import {
|
|
14
14
|
classifyHarvestProblem,
|
|
15
15
|
createHarvestAttemptRecorder,
|
|
16
16
|
harvestProblemResponse,
|
|
17
17
|
serializeHarvestProblem
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-SFRMFGQ6.js";
|
|
19
19
|
import {
|
|
20
20
|
BACKUP_LAST_RESORT_ATTEMPTS,
|
|
21
21
|
BrowserDriver,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
resolveBackupKernelProxyId,
|
|
30
30
|
resolveKernelProxyId,
|
|
31
31
|
serpArrivalUrl
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-NGM237OO.js";
|
|
33
33
|
import {
|
|
34
34
|
HttpMcpToolExecutor,
|
|
35
35
|
WIZPER_LANGUAGES,
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
sanitizeAttempts,
|
|
57
57
|
sanitizeHarvestResult,
|
|
58
58
|
transcribeMediaUrl
|
|
59
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-NKCCGADE.js";
|
|
60
60
|
import {
|
|
61
61
|
auditImageUrls,
|
|
62
62
|
auditImages,
|
|
@@ -74,7 +74,7 @@ import {
|
|
|
74
74
|
recordKernelSession,
|
|
75
75
|
recordVendorUsage,
|
|
76
76
|
runWithCostContext
|
|
77
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-EGJKUB4Q.js";
|
|
78
78
|
import {
|
|
79
79
|
listWorkflowDefinitions,
|
|
80
80
|
runWorkflow,
|
|
@@ -101,7 +101,7 @@ import {
|
|
|
101
101
|
RawMapsOverviewSchema,
|
|
102
102
|
RawMapsReviewStatsSchema
|
|
103
103
|
} from "./chunk-CB5C3BPB.js";
|
|
104
|
-
import "./chunk-
|
|
104
|
+
import "./chunk-YODBNTTN.js";
|
|
105
105
|
import {
|
|
106
106
|
abandonExtractSettlement,
|
|
107
107
|
countSuccessfulPages,
|
|
@@ -124,7 +124,7 @@ import {
|
|
|
124
124
|
setExtractJobTotal,
|
|
125
125
|
settleExtractJob,
|
|
126
126
|
terminalExtractJobStatus
|
|
127
|
-
} from "./chunk-
|
|
127
|
+
} from "./chunk-YCI2PNCS.js";
|
|
128
128
|
import {
|
|
129
129
|
BROWSER_OPEN_MIN_BALANCE_MC,
|
|
130
130
|
CONCURRENCY_PRICE_ID,
|
|
@@ -169,10 +169,9 @@ import {
|
|
|
169
169
|
tierBillingInterval,
|
|
170
170
|
tierCreditsPerInterval,
|
|
171
171
|
vendorCostUsdToMc
|
|
172
|
-
} from "./chunk-
|
|
172
|
+
} from "./chunk-AZRPG43B.js";
|
|
173
173
|
import {
|
|
174
174
|
CaptchaError,
|
|
175
|
-
ExtractionError,
|
|
176
175
|
MapsSearchIncompletePageError,
|
|
177
176
|
RECAPTCHA_INSTRUCTIONS,
|
|
178
177
|
sanitizeVendorName
|
|
@@ -183,7 +182,9 @@ import {
|
|
|
183
182
|
appendWorkflowArtifacts,
|
|
184
183
|
cancelJob,
|
|
185
184
|
checkRateLimit,
|
|
185
|
+
claimStripeEvent,
|
|
186
186
|
completeJob,
|
|
187
|
+
completeStripeEvent,
|
|
187
188
|
completeWorkflowRunRecord,
|
|
188
189
|
consumeCode,
|
|
189
190
|
consumePasswordResetToken,
|
|
@@ -202,6 +203,7 @@ import {
|
|
|
202
203
|
deleteWorkflowSchedule,
|
|
203
204
|
expireOldLots,
|
|
204
205
|
failJob,
|
|
206
|
+
failStripeEvent,
|
|
205
207
|
failWorkflowRunRecord,
|
|
206
208
|
getBillingLedgerSummary,
|
|
207
209
|
getClient,
|
|
@@ -245,7 +247,6 @@ import {
|
|
|
245
247
|
putCode,
|
|
246
248
|
putRefresh,
|
|
247
249
|
reconcileBalanceMc,
|
|
248
|
-
recordStripeEvent,
|
|
249
250
|
recordWorkflowWebhookDelivery,
|
|
250
251
|
registerClient,
|
|
251
252
|
replaceWorkflowArtifacts,
|
|
@@ -265,7 +266,7 @@ import {
|
|
|
265
266
|
softDeleteInboxMessage,
|
|
266
267
|
upsertPageSnapshot,
|
|
267
268
|
verifyPassword
|
|
268
|
-
} from "./chunk-
|
|
269
|
+
} from "./chunk-N7KUTTCC.js";
|
|
269
270
|
|
|
270
271
|
// src/blog/registry.ts
|
|
271
272
|
var posts = [
|
|
@@ -4688,12 +4689,15 @@ async function spiderSite(opts) {
|
|
|
4688
4689
|
const discovered = { url, status, linkedFrom: from, via: "fetch" };
|
|
4689
4690
|
const children = [];
|
|
4690
4691
|
if (html) {
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
visited.
|
|
4695
|
-
|
|
4692
|
+
try {
|
|
4693
|
+
for (const link of extractLinks(html, url)) {
|
|
4694
|
+
if (baseDomain(link) !== startDomain) continue;
|
|
4695
|
+
if (!visited.has(link)) {
|
|
4696
|
+
visited.add(link);
|
|
4697
|
+
children.push({ url: link, from: url });
|
|
4698
|
+
}
|
|
4696
4699
|
}
|
|
4700
|
+
} catch {
|
|
4697
4701
|
}
|
|
4698
4702
|
}
|
|
4699
4703
|
return { discovered, children };
|
|
@@ -6100,67 +6104,78 @@ async function extractSite(opts) {
|
|
|
6100
6104
|
let truncated = false;
|
|
6101
6105
|
let spider;
|
|
6102
6106
|
let sitemapUrls = [];
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
const
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
if (seeded) {
|
|
6117
|
-
for (const u of opts.seedUrls) enqueue(u);
|
|
6118
|
-
} else {
|
|
6119
|
-
enqueue(opts.startUrl);
|
|
6120
|
-
sitemapUrls = await discoverSitemapUrls2(opts.startUrl, maxPages);
|
|
6121
|
-
for (const u of sitemapUrls) enqueue(u);
|
|
6122
|
-
}
|
|
6123
|
-
const waveCap = Math.max(browserConcurrency * urlsPerBrowser, 30);
|
|
6124
|
-
let done = 0;
|
|
6125
|
-
while (frontier.length > 0 && pages.length < maxPages) {
|
|
6126
|
-
const capacity = maxPages - pages.length;
|
|
6127
|
-
const wave = frontier.splice(0, Math.min(capacity, waveCap));
|
|
6128
|
-
const fetched = await crawlWithRotation(wave, {
|
|
6129
|
-
apiKey: opts.kernelApiKey,
|
|
6130
|
-
concurrency: browserConcurrency,
|
|
6131
|
-
urlsPerBrowser,
|
|
6132
|
-
onPage: (p) => {
|
|
6133
|
-
done++;
|
|
6134
|
-
opts.onProgress?.({ done, discovered: seen.size, frontier: frontier.length, lastOk: p.ok ? 1 : 0, lastFail: p.ok ? 0 : 1, phase: p.phase });
|
|
6107
|
+
let stoppedEarly = null;
|
|
6108
|
+
try {
|
|
6109
|
+
if (rotating) {
|
|
6110
|
+
const browserConcurrency = Math.max(1, Math.min(opts.parallelism ?? 1, 8));
|
|
6111
|
+
const urlsPerBrowser = opts.rotateProxyEvery ?? 10;
|
|
6112
|
+
const followLinks = !seeded;
|
|
6113
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6114
|
+
const frontier = [];
|
|
6115
|
+
const enqueue = (u) => {
|
|
6116
|
+
const n = normalizeUrl(u, opts.startUrl);
|
|
6117
|
+
if (n && !seen.has(n)) {
|
|
6118
|
+
seen.add(n);
|
|
6119
|
+
frontier.push(n);
|
|
6135
6120
|
}
|
|
6136
|
-
}
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6121
|
+
};
|
|
6122
|
+
if (seeded) {
|
|
6123
|
+
for (const u of opts.seedUrls) enqueue(u);
|
|
6124
|
+
} else {
|
|
6125
|
+
enqueue(opts.startUrl);
|
|
6126
|
+
sitemapUrls = await discoverSitemapUrls2(opts.startUrl, maxPages);
|
|
6127
|
+
for (const u of sitemapUrls) enqueue(u);
|
|
6128
|
+
}
|
|
6129
|
+
const waveCap = Math.max(browserConcurrency * urlsPerBrowser, 30);
|
|
6130
|
+
let done = 0;
|
|
6131
|
+
while (frontier.length > 0 && pages.length < maxPages) {
|
|
6132
|
+
const capacity = maxPages - pages.length;
|
|
6133
|
+
const wave = frontier.splice(0, Math.min(capacity, waveCap));
|
|
6134
|
+
const fetched = await crawlWithRotation(wave, {
|
|
6135
|
+
apiKey: opts.kernelApiKey,
|
|
6136
|
+
concurrency: browserConcurrency,
|
|
6137
|
+
urlsPerBrowser,
|
|
6138
|
+
onPage: (p) => {
|
|
6139
|
+
done++;
|
|
6140
|
+
opts.onProgress?.({ done, discovered: seen.size, frontier: frontier.length, lastOk: p.ok ? 1 : 0, lastFail: p.ok ? 0 : 1, phase: p.phase });
|
|
6141
|
+
}
|
|
6142
6142
|
});
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6143
|
+
for (const r of fetched) {
|
|
6144
|
+
const page = r.html && r.status >= 200 && r.status < 300 ? parsePageData(r.url, r.html, r.status, "browser", { headers: r.headers, responseTimeMs: r.responseTimeMs, redirectUrl: r.redirectUrl }, [successfulAttempt("browser", r.status, r.responseTimeMs ?? null)]) : emptyPageData(r.url, r.status || null, "browser", {
|
|
6145
|
+
code: r.errorCode ?? "browser_empty_response",
|
|
6146
|
+
message: r.error ?? "Browser fetch returned no usable response.",
|
|
6147
|
+
attempts: [failedAttempt("browser", r.status || null, r.responseTimeMs ?? null, r.errorCode ?? "browser_empty_response", r.error ?? "Browser fetch returned no usable response.")]
|
|
6148
|
+
});
|
|
6149
|
+
pages.push(page);
|
|
6150
|
+
browserRetries++;
|
|
6151
|
+
if (followLinks && isPageExtractionSuccessful(page)) {
|
|
6152
|
+
for (const href of page.discoveryLinks ?? page.outlinks.filter((link) => link.internal).map((link) => link.href)) enqueue(href);
|
|
6153
|
+
}
|
|
6147
6154
|
}
|
|
6148
6155
|
}
|
|
6156
|
+
truncated = frontier.length > 0;
|
|
6157
|
+
spider = makeSeedSpider(opts.startUrl, [...seen]);
|
|
6158
|
+
} else {
|
|
6159
|
+
spider = seeded ? makeSeedSpider(opts.startUrl, opts.seedUrls) : await spiderSite({ startUrl: opts.startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey: opts.kernelApiKey });
|
|
6160
|
+
sitemapUrls = spider.sitemapUrls;
|
|
6161
|
+
const urlsToExtract = spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
|
|
6162
|
+
browserRetries = spider.browserRetries;
|
|
6163
|
+
await runWithConcurrency(urlsToExtract, concurrency, async (url) => {
|
|
6164
|
+
const data = await fetchAndParse(url, opts.kernelApiKey);
|
|
6165
|
+
if (data.via === "browser") browserRetries++;
|
|
6166
|
+
pages.push(data);
|
|
6167
|
+
});
|
|
6168
|
+
truncated = spider.truncated || pages.length >= maxPages;
|
|
6149
6169
|
}
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
await runWithConcurrency(urlsToExtract, concurrency, async (url) => {
|
|
6158
|
-
const data = await fetchAndParse(url, opts.kernelApiKey);
|
|
6159
|
-
if (data.via === "browser") browserRetries++;
|
|
6160
|
-
pages.push(data);
|
|
6161
|
-
});
|
|
6162
|
-
truncated = spider.truncated || pages.length >= maxPages;
|
|
6170
|
+
} catch (err) {
|
|
6171
|
+
if (pages.length === 0) throw err;
|
|
6172
|
+
stoppedEarly = {
|
|
6173
|
+
code: "crawl_stopped_early",
|
|
6174
|
+
message: err instanceof Error ? err.message : String(err)
|
|
6175
|
+
};
|
|
6176
|
+
truncated = true;
|
|
6163
6177
|
}
|
|
6178
|
+
if (!spider) spider = makeSeedSpider(opts.startUrl, pages.map((p) => p.url));
|
|
6164
6179
|
const sitemapSet = sitemapUrls.length > 0 ? new Set(sitemapUrls.map((u) => normalizeUrl(u, opts.startUrl) ?? u)) : null;
|
|
6165
6180
|
for (const p of pages) p.inSitemap = sitemapSet ? sitemapSet.has(normalizeUrl(p.url, opts.startUrl) ?? p.url) : null;
|
|
6166
6181
|
let branding = null;
|
|
@@ -6170,10 +6185,11 @@ async function extractSite(opts) {
|
|
|
6170
6185
|
const seoAudit = opts.formats?.includes("issues") ? buildSeoAudit(pages, opts.startUrl) : null;
|
|
6171
6186
|
let imageAudit = null;
|
|
6172
6187
|
if (opts.formats?.includes("images")) {
|
|
6173
|
-
imageAudit = await auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
|
|
6188
|
+
imageAudit = await auditImages(pages, { concurrency: 12, timeoutMs: 12e3 }).catch(() => null);
|
|
6174
6189
|
}
|
|
6175
6190
|
return {
|
|
6176
6191
|
startUrl: opts.startUrl,
|
|
6192
|
+
stoppedEarly,
|
|
6177
6193
|
pages,
|
|
6178
6194
|
totalPages: pages.length,
|
|
6179
6195
|
spider,
|
|
@@ -8595,7 +8611,7 @@ var siteExtractFn = inngest.createFunction(
|
|
|
8595
8611
|
});
|
|
8596
8612
|
}) : null;
|
|
8597
8613
|
const finalized = await step.run("finalize", async () => {
|
|
8598
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
8614
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-KUBX6N6Z.js");
|
|
8599
8615
|
const current = await getExtractJob(jobId2);
|
|
8600
8616
|
if (!current) throw new Error("extract job disappeared before finalization");
|
|
8601
8617
|
const stored = await assembleExtractArtifacts(current, { branding, imageAudit });
|
|
@@ -8698,6 +8714,30 @@ function closeOpenBusinessPanelDialogInBrowser(expectedHeading) {
|
|
|
8698
8714
|
close?.click();
|
|
8699
8715
|
return Boolean(close);
|
|
8700
8716
|
}
|
|
8717
|
+
function readExpandedPanelRowInBrowser(input) {
|
|
8718
|
+
const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim();
|
|
8719
|
+
const expectedBusiness = normalize(input.businessName).toLowerCase();
|
|
8720
|
+
const visible = (element) => element.getAttribute("aria-hidden") !== "true" && (element.offsetParent !== null || element.getClientRects().length > 0);
|
|
8721
|
+
const readText = (element) => normalize(element.innerText || element.textContent);
|
|
8722
|
+
const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).filter((dialog) => visible(dialog) && readText(dialog).toLowerCase().includes(expectedBusiness)).reverse();
|
|
8723
|
+
for (const dialog of dialogs) {
|
|
8724
|
+
const row = Array.from(
|
|
8725
|
+
dialog.querySelectorAll('button, a, [role="button"]')
|
|
8726
|
+
).find((element) => visible(element) && readText(element).startsWith(input.prefix));
|
|
8727
|
+
if (!row) continue;
|
|
8728
|
+
const body = readText(row).slice(input.prefix.length);
|
|
8729
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8730
|
+
const items = [];
|
|
8731
|
+
for (const part of body.split(/\s*[,\u00b7]\s*/)) {
|
|
8732
|
+
const value = part.trim();
|
|
8733
|
+
if (!value || seen.has(value)) continue;
|
|
8734
|
+
seen.add(value);
|
|
8735
|
+
items.push(value);
|
|
8736
|
+
}
|
|
8737
|
+
if (items.length > 0) return items;
|
|
8738
|
+
}
|
|
8739
|
+
return [];
|
|
8740
|
+
}
|
|
8701
8741
|
async function clickPanelRowWithRetry(page, businessName, prefix) {
|
|
8702
8742
|
const input = { businessName, prefix };
|
|
8703
8743
|
if (await page.evaluate(clickBusinessPanelRowInBrowser, input)) return true;
|
|
@@ -8711,9 +8751,12 @@ async function extractExpandedList(page, businessName, rowPrefix, dialogHeading)
|
|
|
8711
8751
|
extractOpenBusinessPanelDialogItemsInBrowser,
|
|
8712
8752
|
dialogHeading
|
|
8713
8753
|
);
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8754
|
+
if (items.length > 0) {
|
|
8755
|
+
await page.evaluate(closeOpenBusinessPanelDialogInBrowser, dialogHeading);
|
|
8756
|
+
await page.waitForTimeout(250);
|
|
8757
|
+
return items;
|
|
8758
|
+
}
|
|
8759
|
+
return page.evaluate(readExpandedPanelRowInBrowser, { businessName, prefix: rowPrefix });
|
|
8717
8760
|
}
|
|
8718
8761
|
async function extractExpandedGoogleBusinessPanelServices(page, businessName) {
|
|
8719
8762
|
const services = await extractExpandedList(page, businessName, "Services:", "Services");
|
|
@@ -8778,7 +8821,7 @@ var MapsSearchExtractor = class {
|
|
|
8778
8821
|
await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
|
|
8779
8822
|
if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
8780
8823
|
const reachedFinder = await this.clickIntoLocalFinder(page);
|
|
8781
|
-
if (!reachedFinder) throw new
|
|
8824
|
+
if (!reachedFinder) throw new MapsSearchIncompletePageError('Google "More businesses" control was not found on the organic results page');
|
|
8782
8825
|
await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
|
|
8783
8826
|
if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
8784
8827
|
const results = await this.collectResults(page, options);
|
|
@@ -9043,6 +9086,7 @@ var MapsSearchExtractor = class {
|
|
|
9043
9086
|
};
|
|
9044
9087
|
|
|
9045
9088
|
// src/maps-search-rotation.ts
|
|
9089
|
+
var UNCONFIRMED_PAGE_MAX_RETRIES = 5;
|
|
9046
9090
|
var MAPS_SEARCH_MAX_ATTEMPTS = 3;
|
|
9047
9091
|
var MAPS_SEARCH_NONE_MAX_ATTEMPTS = 7;
|
|
9048
9092
|
var MAPS_SEARCH_LOCATION_PROXY_MAX_ATTEMPTS = 5;
|
|
@@ -9110,7 +9154,8 @@ function shouldRetryMapsSearchError(input) {
|
|
|
9110
9154
|
if (input.attemptIndex >= input.maxAttempts - 1) return false;
|
|
9111
9155
|
if (!retryableMapsSearchError(input.err, input.proxyMode)) return false;
|
|
9112
9156
|
if (input.err instanceof MapsSearchIncompletePageError) {
|
|
9113
|
-
|
|
9157
|
+
const unconfirmed = input.priorAttempts.filter((attempt) => attempt.outcome === "results_unconfirmed").length;
|
|
9158
|
+
return unconfirmed < UNCONFIRMED_PAGE_MAX_RETRIES;
|
|
9114
9159
|
}
|
|
9115
9160
|
return true;
|
|
9116
9161
|
}
|
|
@@ -9508,7 +9553,7 @@ async function resolveUsZipsPath(requestedPath) {
|
|
|
9508
9553
|
async function loadZipGroups(stateAbbr, requestedPath, warnings, sourceMode) {
|
|
9509
9554
|
const mode = resolvedLocationDataMode({ zipSource: sourceMode });
|
|
9510
9555
|
if (mode === "hosted") {
|
|
9511
|
-
const { getHostedZipGroups } = await import("./location-data-repository-
|
|
9556
|
+
const { getHostedZipGroups } = await import("./location-data-repository-TTWF3OTM.js");
|
|
9512
9557
|
const hosted = await getHostedZipGroups(stateAbbr);
|
|
9513
9558
|
if (!hosted.dataset) {
|
|
9514
9559
|
throw new DirectoryLocationError(
|
|
@@ -9571,7 +9616,7 @@ async function resolveDirectoryMarkets(options, config = {}) {
|
|
|
9571
9616
|
const sourceUrl = censusStateUrl(state.fips);
|
|
9572
9617
|
const warnings = [];
|
|
9573
9618
|
if (resolvedLocationDataMode(config) === "hosted") {
|
|
9574
|
-
const { queryHostedLocationMarkets } = await import("./location-data-repository-
|
|
9619
|
+
const { queryHostedLocationMarkets } = await import("./location-data-repository-TTWF3OTM.js");
|
|
9575
9620
|
const hosted = await queryHostedLocationMarkets({
|
|
9576
9621
|
state: state.abbr,
|
|
9577
9622
|
minPopulation: options.minPopulation,
|
|
@@ -13718,6 +13763,7 @@ async function resolveYouTubeAudio(videoId) {
|
|
|
13718
13763
|
if (!base || !secret2) return null;
|
|
13719
13764
|
const controller = new AbortController();
|
|
13720
13765
|
const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
|
|
13766
|
+
const startedMs = Date.now();
|
|
13721
13767
|
try {
|
|
13722
13768
|
const resp = await fetch(`${base.replace(/\/+$/, "")}/resolve-audio`, {
|
|
13723
13769
|
method: "POST",
|
|
@@ -13735,6 +13781,12 @@ async function resolveYouTubeAudio(videoId) {
|
|
|
13735
13781
|
console.warn(`[audio-resolver] ${videoId}: response missing mediaUrl`);
|
|
13736
13782
|
return null;
|
|
13737
13783
|
}
|
|
13784
|
+
void recordVendorUsage({
|
|
13785
|
+
vendor: "frames_audio_resolve",
|
|
13786
|
+
model: null,
|
|
13787
|
+
units: (Date.now() - startedMs) / 1e3,
|
|
13788
|
+
unitType: "compute_sec"
|
|
13789
|
+
});
|
|
13738
13790
|
return {
|
|
13739
13791
|
mediaUrl: data.mediaUrl,
|
|
13740
13792
|
durationS: Number(data.durationS) || 0,
|
|
@@ -15232,10 +15284,23 @@ function isAllowedFacebookOrganicUrl(url) {
|
|
|
15232
15284
|
async function transcribeFacebookVideoUrl(videoUrl, markdownTitle = "# Facebook Video Transcript") {
|
|
15233
15285
|
return transcribeMediaUrl(videoUrl, markdownTitle);
|
|
15234
15286
|
}
|
|
15287
|
+
var AD_LIBRARY_APP_CHROME = /Ad Library|Search by keyword|Branded Content/i;
|
|
15288
|
+
var AUTH_WALL_URL = /facebook\.com\/(login|checkpoint|authentication)/i;
|
|
15289
|
+
var MIN_RENDERED_BODY_CHARS = 200;
|
|
15290
|
+
function classifyAdLibraryPage(bodyText, finalUrl) {
|
|
15291
|
+
if (AUTH_WALL_URL.test(finalUrl)) return "blocked";
|
|
15292
|
+
if (bodyText.length < MIN_RENDERED_BODY_CHARS) return "blocked";
|
|
15293
|
+
return AD_LIBRARY_APP_CHROME.test(bodyText) ? "reached_app" : "blocked";
|
|
15294
|
+
}
|
|
15295
|
+
async function refundEmptyPageIntel(c, userId, subject, blocked) {
|
|
15296
|
+
await creditMc(userId, MC_COSTS.fb_page_intel, LedgerOperation.FB_PAGE_INTEL_REFUND, blocked ? "soft-block empty result" : "empty result");
|
|
15297
|
+
await logRequestEvent({ userId, source: "facebook_page_intel", status: "failed", query: subject, error: blocked ? "soft-block: empty result refunded" : "empty result refunded" });
|
|
15298
|
+
return blocked ? c.json({ error: "soft-block: no ads returned (refunded)" }, 503) : c.json({ error: `No ads in Meta's Ad Library match "${subject}" (refunded). This is a real empty result, not a block.` }, 404);
|
|
15299
|
+
}
|
|
15235
15300
|
async function detectSoftBlock(driver) {
|
|
15236
15301
|
const page = driver.getPage();
|
|
15237
15302
|
const bodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
|
|
15238
|
-
return bodyText.
|
|
15303
|
+
return classifyAdLibraryPage(bodyText, page.url()) === "blocked";
|
|
15239
15304
|
}
|
|
15240
15305
|
function buildPageIntelUrl(body, country) {
|
|
15241
15306
|
if (body.libraryId?.trim()) return `https://www.facebook.com/ads/library/?id=${FacebookAdExtractor.resolveLibraryId(body.libraryId.trim()) ?? body.libraryId.trim()}`;
|
|
@@ -15371,26 +15436,27 @@ facebookAdApp.post("/page-intel", createApiKeyAuth(), async (c) => {
|
|
|
15371
15436
|
const driver = new BrowserDriver();
|
|
15372
15437
|
let refunded = false, debited = false;
|
|
15373
15438
|
try {
|
|
15374
|
-
const { ok: fbOk, balance_mc: fbBal } = await debitMc(fbUser.id, MC_COSTS.
|
|
15375
|
-
if (!fbOk) return c.json(insufficientBalanceResponse(fbBal, MC_COSTS.
|
|
15439
|
+
const { ok: fbOk, balance_mc: fbBal } = await debitMc(fbUser.id, MC_COSTS.fb_page_intel, LedgerOperation.FB_PAGE_INTEL, subject);
|
|
15440
|
+
if (!fbOk) return c.json(insufficientBalanceResponse(fbBal, MC_COSTS.fb_page_intel), 402);
|
|
15376
15441
|
debited = true;
|
|
15377
15442
|
await driver.launch(await kernelLaunchOptsResidential());
|
|
15378
15443
|
await driver.navigateTo(listingUrl);
|
|
15379
15444
|
const extractor = new FacebookAdExtractor(driver);
|
|
15380
15445
|
const result = await extractor.extractPageIntel(listingUrl, maxAds);
|
|
15381
|
-
if (result.ads.length === 0
|
|
15382
|
-
await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "soft-block empty result");
|
|
15446
|
+
if (result.ads.length === 0) {
|
|
15383
15447
|
refunded = true;
|
|
15384
|
-
await
|
|
15385
|
-
return c.json({ error: "soft-block: no ads returned (refunded)" }, 503);
|
|
15448
|
+
return await refundEmptyPageIntel(c, fbUser.id, subject, await detectSoftBlock(driver));
|
|
15386
15449
|
}
|
|
15450
|
+
const perAdMc = result.ads.length * MC_COSTS.fb_page_intel_ad;
|
|
15451
|
+
const adDebit = await debitMc(fbUser.id, perAdMc, LedgerOperation.FB_PAGE_INTEL_AD, subject);
|
|
15452
|
+
if (!adDebit.ok) return c.json(insufficientBalanceResponse(adDebit.balance_mc, perAdMc), 402);
|
|
15387
15453
|
const match = assessPageIntelMatch(body, result);
|
|
15388
15454
|
const output = { ...result, ...match };
|
|
15389
15455
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: subject, resultCount: result.ads.length, result: output });
|
|
15390
15456
|
return c.json(output);
|
|
15391
15457
|
} catch (err) {
|
|
15392
15458
|
const msg = err instanceof Error ? err.message : String(err);
|
|
15393
|
-
if (debited && !refunded) await creditMc(fbUser.id, MC_COSTS.
|
|
15459
|
+
if (debited && !refunded) await creditMc(fbUser.id, MC_COSTS.fb_page_intel, LedgerOperation.FB_PAGE_INTEL_REFUND, "failed call");
|
|
15394
15460
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: subject, error: msg });
|
|
15395
15461
|
if (msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha")) {
|
|
15396
15462
|
return c.json({ error: msg }, 503);
|
|
@@ -15531,10 +15597,16 @@ facebookAdApp.post("/search", createApiKeyAuth(), async (c) => {
|
|
|
15531
15597
|
debited = true;
|
|
15532
15598
|
await driver.launch(await kernelLaunchOptsResidential());
|
|
15533
15599
|
const page = driver.getPage();
|
|
15534
|
-
|
|
15600
|
+
let collated = await collectAdLibraryResults(page, searchUrl, Math.max(maxResults * 4, 40));
|
|
15601
|
+
if (collated.length === 0) {
|
|
15602
|
+
collated = await collectAdLibraryResults(page, searchUrl, Math.max(maxResults * 4, 40));
|
|
15603
|
+
}
|
|
15535
15604
|
const gqlAdvertisers = advertisersFromResults(collated, maxResults);
|
|
15536
15605
|
if (gqlAdvertisers.length > 0) {
|
|
15537
15606
|
const results2 = gqlAdvertisers.map((a) => ({ name: a.pageName, pageName: a.pageName, pageId: a.pageId, libraryId: a.sampleLibraryId, sampleLibraryId: a.sampleLibraryId, adCount: a.adCount }));
|
|
15607
|
+
const gqlPerResultMc = results2.length * MC_COSTS.fb_search_result;
|
|
15608
|
+
const gqlResultDebit = await debitMc(fbUser.id, gqlPerResultMc, LedgerOperation.FB_SEARCH_RESULT, body.query.trim());
|
|
15609
|
+
if (!gqlResultDebit.ok) return c.json(insufficientBalanceResponse(gqlResultDebit.balance_mc, gqlPerResultMc), 402);
|
|
15538
15610
|
const searchResult2 = { query: body.query.trim(), searchUrl, results: results2, via: "graphql" };
|
|
15539
15611
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "done", query: body.query.trim(), resultCount: results2.length, result: searchResult2 });
|
|
15540
15612
|
return c.json(searchResult2);
|
|
@@ -15547,7 +15619,7 @@ facebookAdApp.post("/search", createApiKeyAuth(), async (c) => {
|
|
|
15547
15619
|
await page.waitForTimeout(1e3);
|
|
15548
15620
|
}
|
|
15549
15621
|
const rawBodyText = await page.evaluate(() => document.body?.innerText ?? "");
|
|
15550
|
-
if (rawBodyText.
|
|
15622
|
+
if (classifyAdLibraryPage(rawBodyText, page.url()) === "blocked") {
|
|
15551
15623
|
await creditMc(fbUser.id, MC_COSTS.fb_search, LedgerOperation.FB_SEARCH_REFUND, "soft-block empty result");
|
|
15552
15624
|
searchRefunded = true;
|
|
15553
15625
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "failed", query: body.query.trim(), error: "soft-block: empty result refunded" });
|
|
@@ -15588,8 +15660,11 @@ facebookAdApp.post("/search", createApiKeyAuth(), async (c) => {
|
|
|
15588
15660
|
await creditMc(fbUser.id, MC_COSTS.fb_search, LedgerOperation.FB_SEARCH_REFUND, "empty result");
|
|
15589
15661
|
searchRefunded = true;
|
|
15590
15662
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
|
|
15591
|
-
return c.json({ error:
|
|
15663
|
+
return c.json({ error: `No advertisers in Meta's Ad Library match "${body.query.trim()}" (refunded). This is a real empty result, not a block \u2014 try a shorter or broader query, or a brand/page name.` }, 404);
|
|
15592
15664
|
}
|
|
15665
|
+
const perResultMc = results.length * MC_COSTS.fb_search_result;
|
|
15666
|
+
const resultDebit = await debitMc(fbUser.id, perResultMc, LedgerOperation.FB_SEARCH_RESULT, body.query.trim());
|
|
15667
|
+
if (!resultDebit.ok) return c.json(insufficientBalanceResponse(resultDebit.balance_mc, perResultMc), 402);
|
|
15593
15668
|
const searchResult = { query: body.query.trim(), searchUrl, results };
|
|
15594
15669
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "done", query: body.query.trim(), resultCount: results.length, result: searchResult });
|
|
15595
15670
|
return c.json(searchResult);
|
|
@@ -15658,9 +15733,6 @@ import { z as z18 } from "zod";
|
|
|
15658
15733
|
|
|
15659
15734
|
// src/extractor/GoogleAdsExtractor.ts
|
|
15660
15735
|
var ORIGIN = "https://adstransparency.google.com";
|
|
15661
|
-
function looksLikeDomain(q) {
|
|
15662
|
-
return /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(q.trim()) && !q.includes(" ");
|
|
15663
|
-
}
|
|
15664
15736
|
var GoogleAdsExtractor = class {
|
|
15665
15737
|
constructor(driver) {
|
|
15666
15738
|
this.driver = driver;
|
|
@@ -15671,50 +15743,108 @@ var GoogleAdsExtractor = class {
|
|
|
15671
15743
|
}
|
|
15672
15744
|
async search(query, region, maxResults) {
|
|
15673
15745
|
const q = query.trim();
|
|
15674
|
-
|
|
15675
|
-
await this.driver.navigateTo(url);
|
|
15746
|
+
await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}`);
|
|
15676
15747
|
const page = this.page();
|
|
15677
|
-
await page.waitForTimeout(
|
|
15678
|
-
await
|
|
15679
|
-
|
|
15680
|
-
|
|
15681
|
-
|
|
15682
|
-
|
|
15683
|
-
|
|
15684
|
-
|
|
15685
|
-
|
|
15686
|
-
const
|
|
15687
|
-
const
|
|
15688
|
-
|
|
15689
|
-
|
|
15690
|
-
|
|
15691
|
-
|
|
15692
|
-
|
|
15693
|
-
|
|
15694
|
-
|
|
15695
|
-
|
|
15696
|
-
|
|
15697
|
-
|
|
15698
|
-
|
|
15748
|
+
await page.waitForTimeout(2500);
|
|
15749
|
+
const box = await page.waitForSelector('input[type="text"], input[aria-label*="Search" i], input', { timeout: 2e4 });
|
|
15750
|
+
await box.click();
|
|
15751
|
+
await box.fill("");
|
|
15752
|
+
await box.type(q, { delay: 110 });
|
|
15753
|
+
await page.waitForSelector('[role="option"]', { timeout: 2e4 }).catch(() => null);
|
|
15754
|
+
await page.waitForTimeout(2200);
|
|
15755
|
+
const raw = await page.evaluate(() => {
|
|
15756
|
+
const rows = Array.from(document.querySelectorAll('[role="option"]')).map((el2) => {
|
|
15757
|
+
const parts = Array.from(el2.querySelectorAll("*")).filter((n) => n.children.length === 0).map((n) => (n.textContent ?? "").replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
15758
|
+
const flat = el2.innerText.replace(/\s+/g, " ").trim();
|
|
15759
|
+
return { parts: parts.length ? parts : [flat], flat };
|
|
15760
|
+
}).filter((r) => r.flat);
|
|
15761
|
+
return { rows };
|
|
15762
|
+
});
|
|
15763
|
+
const domainRe = /^[a-z0-9][a-z0-9-]*(\.[a-z0-9-]+)+$/i;
|
|
15764
|
+
const websiteRows = raw.rows.filter((r) => domainRe.test(r.flat));
|
|
15765
|
+
const advertiserRows = raw.rows.filter((r) => !domainRe.test(r.flat));
|
|
15766
|
+
const advertisers = advertiserRows.slice(0, maxResults).map((row) => {
|
|
15767
|
+
const segments = row.parts.filter((p) => !/^Verified$/i.test(p));
|
|
15768
|
+
const adSeg = segments.find((p) => /~\s*[\d,]+\s*ads?/i.test(p));
|
|
15769
|
+
const adCount = adSeg?.match(/~\s*([\d,]+)\s*ads?/i);
|
|
15770
|
+
const rest = segments.filter((p) => p !== adSeg);
|
|
15771
|
+
return {
|
|
15772
|
+
advertiserId: null,
|
|
15773
|
+
name: rest[0] ?? null,
|
|
15774
|
+
basedIn: rest.length > 1 ? rest[rest.length - 1] : null,
|
|
15775
|
+
domain: null,
|
|
15776
|
+
approxAdCount: adCount ? Number(adCount[1].replace(/,/g, "")) : null,
|
|
15777
|
+
detailUrl: null
|
|
15778
|
+
};
|
|
15779
|
+
});
|
|
15780
|
+
const websites = websiteRows.slice(0, maxResults).map((row) => ({
|
|
15781
|
+
domain: row.flat,
|
|
15782
|
+
detailUrl: `${ORIGIN}/?region=${encodeURIComponent(region)}&domain=${encodeURIComponent(row.flat)}`
|
|
15783
|
+
}));
|
|
15699
15784
|
return {
|
|
15700
15785
|
query: q,
|
|
15701
15786
|
region,
|
|
15702
15787
|
advertiserCount: advertisers.length,
|
|
15703
|
-
advertisers
|
|
15704
|
-
|
|
15705
|
-
|
|
15706
|
-
domain: looksLikeDomain(q) ? q : null,
|
|
15707
|
-
approxAdCount: a.approxAdCount,
|
|
15708
|
-
detailUrl: `${ORIGIN}/advertiser/${a.advertiserId}?region=${region}`
|
|
15709
|
-
}))
|
|
15788
|
+
advertisers,
|
|
15789
|
+
websiteCount: websites.length,
|
|
15790
|
+
websites
|
|
15710
15791
|
};
|
|
15711
15792
|
}
|
|
15793
|
+
async advertiserIdOnPage() {
|
|
15794
|
+
const page = this.page();
|
|
15795
|
+
const fromUrl = page.url().match(/\/advertiser\/(AR[0-9]+)/);
|
|
15796
|
+
if (fromUrl) return fromUrl[1];
|
|
15797
|
+
return await page.evaluate(() => {
|
|
15798
|
+
for (const a of Array.from(document.querySelectorAll('a[href*="/advertiser/"]'))) {
|
|
15799
|
+
const m = a.getAttribute("href")?.match(/\/advertiser\/(AR[0-9]+)/);
|
|
15800
|
+
if (m) return m[1];
|
|
15801
|
+
}
|
|
15802
|
+
return null;
|
|
15803
|
+
});
|
|
15804
|
+
}
|
|
15805
|
+
async resolveAdvertiserIdByDomain(domain, region) {
|
|
15806
|
+
await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}&domain=${encodeURIComponent(domain.trim())}`);
|
|
15807
|
+
await this.page().waitForTimeout(3500);
|
|
15808
|
+
return await this.advertiserIdOnPage();
|
|
15809
|
+
}
|
|
15810
|
+
async resolveAdvertiserIdByTypeahead(term, region) {
|
|
15811
|
+
const page = this.page();
|
|
15812
|
+
await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}`);
|
|
15813
|
+
await page.waitForTimeout(2500);
|
|
15814
|
+
const box = await page.waitForSelector('input[type="text"], input[aria-label*="Search" i], input', { timeout: 2e4 });
|
|
15815
|
+
await box.click();
|
|
15816
|
+
await box.fill("");
|
|
15817
|
+
await box.type(term.trim(), { delay: 110 });
|
|
15818
|
+
await page.waitForSelector('[role="option"]', { timeout: 2e4 }).catch(() => null);
|
|
15819
|
+
await page.waitForTimeout(2e3);
|
|
15820
|
+
const optionCount = (await page.$$('[role="option"]')).length;
|
|
15821
|
+
for (let i = 0; i < Math.min(optionCount, 3); i++) {
|
|
15822
|
+
const options = await page.$$('[role="option"]');
|
|
15823
|
+
if (!options[i]) break;
|
|
15824
|
+
await options[i].click().catch(() => null);
|
|
15825
|
+
await page.waitForTimeout(3e3);
|
|
15826
|
+
const found = await this.advertiserIdOnPage();
|
|
15827
|
+
if (found) return found;
|
|
15828
|
+
await this.driver.navigateTo(`${ORIGIN}/?region=${encodeURIComponent(region)}`);
|
|
15829
|
+
await page.waitForTimeout(1500);
|
|
15830
|
+
const retryBox = await page.waitForSelector('input[type="text"], input', { timeout: 15e3 }).catch(() => null);
|
|
15831
|
+
if (!retryBox) break;
|
|
15832
|
+
await retryBox.click();
|
|
15833
|
+
await retryBox.fill("");
|
|
15834
|
+
await retryBox.type(term.trim(), { delay: 110 });
|
|
15835
|
+
await page.waitForSelector('[role="option"]', { timeout: 15e3 }).catch(() => null);
|
|
15836
|
+
await page.waitForTimeout(1800);
|
|
15837
|
+
}
|
|
15838
|
+
return null;
|
|
15839
|
+
}
|
|
15712
15840
|
async pageIntel(opts) {
|
|
15713
15841
|
const { region, maxAds } = opts;
|
|
15714
15842
|
let advertiserId = opts.advertiserId?.trim() || null;
|
|
15715
|
-
if (!advertiserId && opts.domain) {
|
|
15716
|
-
|
|
15717
|
-
|
|
15843
|
+
if (!advertiserId && opts.domain?.trim()) {
|
|
15844
|
+
advertiserId = await this.resolveAdvertiserIdByDomain(opts.domain, region);
|
|
15845
|
+
}
|
|
15846
|
+
if (!advertiserId && opts.advertiserName?.trim()) {
|
|
15847
|
+
advertiserId = await this.resolveAdvertiserIdByTypeahead(opts.advertiserName, region);
|
|
15718
15848
|
}
|
|
15719
15849
|
if (!advertiserId) {
|
|
15720
15850
|
return { advertiserId: null, advertiserName: null, domain: opts.domain ?? null, region, totalCreatives: 0, videoCount: 0, imageCount: 0, textCount: 0, ads: [] };
|
|
@@ -15831,10 +15961,11 @@ var GoogleAdsSearchBodySchema = z18.object({
|
|
|
15831
15961
|
});
|
|
15832
15962
|
var GoogleAdsPageIntelBodySchema = z18.object({
|
|
15833
15963
|
advertiserId: z18.string().trim().optional(),
|
|
15964
|
+
advertiserName: z18.string().trim().optional(),
|
|
15834
15965
|
domain: z18.string().trim().optional(),
|
|
15835
15966
|
region: z18.string().trim().toUpperCase().length(2).optional(),
|
|
15836
15967
|
maxAds: z18.number().int().min(1).max(200).optional()
|
|
15837
|
-
}).refine((d) => !!d.advertiserId || !!d.domain, { message: "advertiserId or domain is required" });
|
|
15968
|
+
}).refine((d) => !!d.advertiserId || !!d.domain || !!d.advertiserName, { message: "advertiserId, advertiserName, or domain is required (advertiserName and domain both come straight from google_ads_search)" });
|
|
15838
15969
|
var GoogleAdsTranscribeBodySchema = z18.object({
|
|
15839
15970
|
videoUrl: z18.string().trim().min(1, "videoUrl is required")
|
|
15840
15971
|
});
|
|
@@ -15884,13 +16015,13 @@ googleAdsApp.post("/search", createApiKeyAuth(), async (c) => {
|
|
|
15884
16015
|
const extractor = new GoogleAdsExtractor(driver);
|
|
15885
16016
|
return extractor.search(body.query.trim(), region, maxResults);
|
|
15886
16017
|
});
|
|
15887
|
-
if (result.advertisers.length === 0) {
|
|
16018
|
+
if (result.advertisers.length === 0 && result.websites.length === 0) {
|
|
15888
16019
|
await creditMc(user.id, MC_COSTS.google_ads_search, LedgerOperation.GOOGLE_ADS_SEARCH_REFUND, "empty result");
|
|
15889
16020
|
refunded = true;
|
|
15890
16021
|
await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
|
|
15891
16022
|
return c.json({ error: "no advertisers found (refunded)" }, 503);
|
|
15892
16023
|
}
|
|
15893
|
-
await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "done", query: body.query.trim(), resultCount: result.advertisers.length, result });
|
|
16024
|
+
await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "done", query: body.query.trim(), resultCount: result.advertisers.length + result.websites.length, result });
|
|
15894
16025
|
return c.json(result);
|
|
15895
16026
|
} catch (err) {
|
|
15896
16027
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -15909,7 +16040,7 @@ googleAdsApp.post("/page-intel", createApiKeyAuth(), async (c) => {
|
|
|
15909
16040
|
const body = parsed.data;
|
|
15910
16041
|
const region = body.region ?? "US";
|
|
15911
16042
|
const maxAds = Math.min(200, Math.max(1, body.maxAds ?? 50));
|
|
15912
|
-
const queryLabel = body.advertiserId ?? body.domain ?? "";
|
|
16043
|
+
const queryLabel = body.advertiserId ?? body.domain ?? body.advertiserName ?? "";
|
|
15913
16044
|
const user = c.get("user");
|
|
15914
16045
|
const gate = await acquireConcurrencyGate(user, "google_ads_page_intel", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { advertiserId: body.advertiserId, domain: body.domain, region } });
|
|
15915
16046
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
@@ -15922,7 +16053,7 @@ googleAdsApp.post("/page-intel", createApiKeyAuth(), async (c) => {
|
|
|
15922
16053
|
const result = await runWithCostContext({ ...currentCostContext(), op: "google_ads_intel", userId: user.id }, async () => {
|
|
15923
16054
|
await driver.launch(await kernelLaunchOptsResidential2());
|
|
15924
16055
|
const extractor = new GoogleAdsExtractor(driver);
|
|
15925
|
-
return extractor.pageIntel({ advertiserId: body.advertiserId, domain: body.domain, region, maxAds });
|
|
16056
|
+
return extractor.pageIntel({ advertiserId: body.advertiserId, advertiserName: body.advertiserName, domain: body.domain, region, maxAds });
|
|
15926
16057
|
});
|
|
15927
16058
|
if (result.ads.length === 0) {
|
|
15928
16059
|
await creditMc(user.id, MC_COSTS.google_ads_intel, LedgerOperation.GOOGLE_ADS_INTEL_REFUND, "empty result");
|
|
@@ -16893,6 +17024,12 @@ instagramApp.post("/profile-content", createApiKeyAuth(), async (c) => {
|
|
|
16893
17024
|
scrollDelayMs: body.scrollDelayMs,
|
|
16894
17025
|
stableScrollLimit: body.stableScrollLimit
|
|
16895
17026
|
});
|
|
17027
|
+
if (result.items.length === 0) {
|
|
17028
|
+
await creditMc(user.id, MC_COSTS.instagram_profile, LedgerOperation.INSTAGRAM_PROFILE_REFUND, "empty result");
|
|
17029
|
+
debited = false;
|
|
17030
|
+
await logRequestEvent({ userId: user.id, source: "instagram_profile_content", status: "failed", query: target.profileUrl, error: "empty result refunded" });
|
|
17031
|
+
return c.json({ error: `No public posts were readable on ${target.profileUrl} (refunded). The profile may be private, empty, or require a logged-in session.` }, 404);
|
|
17032
|
+
}
|
|
16896
17033
|
const response = { ...result, browser: launch.browser };
|
|
16897
17034
|
await logRequestEvent({ userId: user.id, source: "instagram_profile_content", status: "done", query: target.profileUrl, resultCount: result.items.length, result: response });
|
|
16898
17035
|
return c.json(response);
|
|
@@ -17016,7 +17153,10 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
|
|
|
17016
17153
|
const tx = await debitMc(user.id, holdMc, LedgerOperation.INSTAGRAM_TRANSCRIBE, sourceUrl.href);
|
|
17017
17154
|
if (!tx.ok) return c.json(insufficientBalanceResponse(tx.balance_mc, holdMc), 402);
|
|
17018
17155
|
transcriptDebited = true;
|
|
17019
|
-
transcript = await
|
|
17156
|
+
transcript = await runWithCostContext(
|
|
17157
|
+
{ ...currentCostContext(), op: "instagram_transcribe", userId: user.id },
|
|
17158
|
+
() => transcribeMediaUrl(audioUrl, "# Instagram Media Transcript")
|
|
17159
|
+
);
|
|
17020
17160
|
const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
|
|
17021
17161
|
const diff = holdMc - settleMc;
|
|
17022
17162
|
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.INSTAGRAM_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
@@ -18454,6 +18594,7 @@ trustpilotApp.post("/reviews", createApiKeyAuth(), async (c) => {
|
|
|
18454
18594
|
const driver = new BrowserDriver();
|
|
18455
18595
|
const allReviews = [];
|
|
18456
18596
|
let pagesFetched = 0;
|
|
18597
|
+
let stoppedEarly = null;
|
|
18457
18598
|
try {
|
|
18458
18599
|
await driver.launch({
|
|
18459
18600
|
headless: false,
|
|
@@ -18471,6 +18612,12 @@ trustpilotApp.post("/reviews", createApiKeyAuth(), async (c) => {
|
|
|
18471
18612
|
if (cards.length === 0) break;
|
|
18472
18613
|
allReviews.push(...cards);
|
|
18473
18614
|
}
|
|
18615
|
+
} catch (err) {
|
|
18616
|
+
if (allReviews.length === 0) throw err;
|
|
18617
|
+
stoppedEarly = {
|
|
18618
|
+
code: "review_scrape_stopped_early",
|
|
18619
|
+
message: err instanceof Error ? err.message : String(err)
|
|
18620
|
+
};
|
|
18474
18621
|
} finally {
|
|
18475
18622
|
await driver.close();
|
|
18476
18623
|
}
|
|
@@ -18496,6 +18643,7 @@ trustpilotApp.post("/reviews", createApiKeyAuth(), async (c) => {
|
|
|
18496
18643
|
pagesFetched,
|
|
18497
18644
|
reviewCount: allReviews.length,
|
|
18498
18645
|
reviews: allReviews,
|
|
18646
|
+
stoppedEarly,
|
|
18499
18647
|
durationMs: Date.now() - startMs
|
|
18500
18648
|
};
|
|
18501
18649
|
await logRequestEvent({ userId: user.id, source: "trustpilot_reviews", status: "done", query: domain, resultCount: allReviews.length, result });
|
|
@@ -23981,6 +24129,13 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
|
|
|
23981
24129
|
// src/api/stripe-routes.ts
|
|
23982
24130
|
import Stripe from "stripe";
|
|
23983
24131
|
import { Hono as Hono22 } from "hono";
|
|
24132
|
+
|
|
24133
|
+
// src/api/stripe-credit-policy.ts
|
|
24134
|
+
function shouldGrantScraperSubscriptionCredits(invoiceHasTierLine, invoiceTotal) {
|
|
24135
|
+
return invoiceHasTierLine && (invoiceTotal ?? 0) > 0;
|
|
24136
|
+
}
|
|
24137
|
+
|
|
24138
|
+
// src/api/stripe-routes.ts
|
|
23984
24139
|
var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
|
|
23985
24140
|
async function cancelStandaloneMemorySub(memSubId, baseSubId) {
|
|
23986
24141
|
if (!memSubId || memSubId === baseSubId) return;
|
|
@@ -24005,17 +24160,30 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
24005
24160
|
} catch {
|
|
24006
24161
|
return c.json({ error: "Invalid signature" }, 400);
|
|
24007
24162
|
}
|
|
24008
|
-
const
|
|
24009
|
-
if (
|
|
24163
|
+
const claim = await claimStripeEvent(event.id);
|
|
24164
|
+
if (claim.state !== "claimed") return c.json({ received: true });
|
|
24165
|
+
try {
|
|
24166
|
+
await processStripeEvent(event);
|
|
24167
|
+
const completed = await completeStripeEvent(event.id, claim.token);
|
|
24168
|
+
if (!completed) throw new Error("Stripe event claim was superseded before completion");
|
|
24169
|
+
return c.json({ received: true });
|
|
24170
|
+
} catch (error) {
|
|
24171
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24172
|
+
await failStripeEvent(event.id, claim.token, message).catch(() => false);
|
|
24173
|
+
console.error("[stripe] webhook processing failed", event.id, message);
|
|
24174
|
+
return c.json({ error: "Webhook processing failed" }, 500);
|
|
24175
|
+
}
|
|
24176
|
+
});
|
|
24177
|
+
async function processStripeEvent(event) {
|
|
24010
24178
|
if (event.type === "invoice.paid" || event.type === "invoice.payment_succeeded") {
|
|
24011
24179
|
const invoice = event.data.object;
|
|
24012
24180
|
const memLineId = invoice.lines.data.map(linePriceId).find((id) => id && id in MEMORY_PLANS);
|
|
24013
|
-
if (memLineId) return
|
|
24181
|
+
if (memLineId) return;
|
|
24014
24182
|
const invoiceHasTierLine = invoice.lines.data.some((l) => {
|
|
24015
24183
|
const id = linePriceId(l);
|
|
24016
24184
|
return id && id in SUBSCRIPTION_TIERS;
|
|
24017
24185
|
});
|
|
24018
|
-
if (invoiceHasTierLine) {
|
|
24186
|
+
if (shouldGrantScraperSubscriptionCredits(invoiceHasTierLine, invoice.total)) {
|
|
24019
24187
|
const subId = invoice.subscription;
|
|
24020
24188
|
const liveBasePlanPriceId = subId ? findBasePlanItem(await stripe.subscriptions.retrieve(subId))?.price?.id : void 0;
|
|
24021
24189
|
const tierPriceId = liveBasePlanPriceId ?? invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
|
|
@@ -24048,10 +24216,10 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
24048
24216
|
await setMemoryPlan(user2.id, newMemoryPlan, live ? sub.id : null);
|
|
24049
24217
|
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: newMemoryPlan }));
|
|
24050
24218
|
}
|
|
24051
|
-
return
|
|
24219
|
+
return;
|
|
24052
24220
|
}
|
|
24053
24221
|
const user = await getUserByStripeCustomerId(sub.customer);
|
|
24054
|
-
if (!user) return
|
|
24222
|
+
if (!user) return;
|
|
24055
24223
|
if (priceId && priceId in SUBSCRIPTION_TIERS) {
|
|
24056
24224
|
const tier = SUBSCRIPTION_TIERS[priceId];
|
|
24057
24225
|
const live = sub.status === "active" || sub.status === "trialing";
|
|
@@ -24071,7 +24239,7 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
24071
24239
|
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
24072
24240
|
}
|
|
24073
24241
|
} else if (priceId === CONCURRENCY_PRICE_ID && event.type === "customer.subscription.created") {
|
|
24074
|
-
await setExtraConcurrencySlots(user.id,
|
|
24242
|
+
await setExtraConcurrencySlots(user.id, 1);
|
|
24075
24243
|
await setConcurrencySubId(user.id, sub.id);
|
|
24076
24244
|
}
|
|
24077
24245
|
}
|
|
@@ -24088,7 +24256,7 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
24088
24256
|
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
|
|
24089
24257
|
}
|
|
24090
24258
|
}
|
|
24091
|
-
return
|
|
24259
|
+
return;
|
|
24092
24260
|
}
|
|
24093
24261
|
if (priceId && priceId in MEMORY_PLANS || await resolveUserByMemorySubscription(sub.id)) {
|
|
24094
24262
|
const user2 = await resolveUserByMemorySubscription(sub.id) ?? await getUserByStripeCustomerId(sub.customer);
|
|
@@ -24096,17 +24264,16 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
24096
24264
|
await setMemoryPlan(user2.id, "free", null);
|
|
24097
24265
|
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
|
|
24098
24266
|
}
|
|
24099
|
-
return
|
|
24267
|
+
return;
|
|
24100
24268
|
}
|
|
24101
24269
|
const user = await getUserByStripeCustomerId(sub.customer);
|
|
24102
|
-
if (!user) return
|
|
24270
|
+
if (!user) return;
|
|
24103
24271
|
if (priceId === CONCURRENCY_PRICE_ID) {
|
|
24104
|
-
await setExtraConcurrencySlots(user.id,
|
|
24272
|
+
await setExtraConcurrencySlots(user.id, 0);
|
|
24105
24273
|
await setConcurrencySubId(user.id, null);
|
|
24106
24274
|
}
|
|
24107
24275
|
}
|
|
24108
|
-
|
|
24109
|
-
});
|
|
24276
|
+
}
|
|
24110
24277
|
function linePriceId(line) {
|
|
24111
24278
|
const l = line;
|
|
24112
24279
|
return l?.price?.id ?? l?.pricing?.price_details?.price ?? l?.plan?.id;
|
|
@@ -30511,6 +30678,7 @@ function opForCostPath(p) {
|
|
|
30511
30678
|
if (p.startsWith("/facebook/")) {
|
|
30512
30679
|
if (p.endsWith("transcribe")) return "fb_transcribe";
|
|
30513
30680
|
if (p.endsWith("/search")) return "fb_search";
|
|
30681
|
+
if (p.endsWith("/page-intel")) return "fb_page_intel";
|
|
30514
30682
|
return "fb_ad";
|
|
30515
30683
|
}
|
|
30516
30684
|
if (p.startsWith("/instagram/")) return "instagram";
|
|
@@ -33177,7 +33345,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
33177
33345
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
33178
33346
|
return c.json({ error: "Unauthorized" }, 401);
|
|
33179
33347
|
}
|
|
33180
|
-
const { drainQueue } = await import("./worker-
|
|
33348
|
+
const { drainQueue } = await import("./worker-XCPU4YSN.js");
|
|
33181
33349
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
33182
33350
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
33183
33351
|
const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation] = await Promise.all([
|
|
@@ -33226,8 +33394,8 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
33226
33394
|
finishExtractJob: finishExtractJob2,
|
|
33227
33395
|
terminalExtractJobStatus: terminalExtractJobStatus2,
|
|
33228
33396
|
extractJobLimitInfo: extractJobLimitInfo2
|
|
33229
|
-
} = await import("./site-extract-repository-
|
|
33230
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
33397
|
+
} = await import("./site-extract-repository-XSPJTCIL.js");
|
|
33398
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-KUBX6N6Z.js");
|
|
33231
33399
|
const existing = await getExtractJob2(jobId2);
|
|
33232
33400
|
if (!existing) return c.json({ error: "job not found" }, 404);
|
|
33233
33401
|
if (existing.status !== "failed") {
|
|
@@ -33254,7 +33422,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
33254
33422
|
if (!updated) return c.json({ error: "job state changed during re-finalization" }, 409);
|
|
33255
33423
|
let settlement = "already_settled_or_refunded";
|
|
33256
33424
|
if (job.billedMc == null && job.userId != null) {
|
|
33257
|
-
const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-
|
|
33425
|
+
const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-XSPJTCIL.js");
|
|
33258
33426
|
const heldMc = Number(job.options.heldMc ?? 0);
|
|
33259
33427
|
const successful = await countSuccessfulPages2(jobId2);
|
|
33260
33428
|
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
@@ -33425,4 +33593,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
33425
33593
|
export {
|
|
33426
33594
|
app
|
|
33427
33595
|
};
|
|
33428
|
-
//# sourceMappingURL=server-
|
|
33596
|
+
//# sourceMappingURL=server-5EX6XBIA.js.map
|