mcp-scraper 0.69.1 → 0.72.1
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/CHANGELOG.md +63 -2
- package/README.md +6 -2
- package/dist/{analytics-repository-62CAZAIP.js → analytics-repository-6YLRSWYO.js} +3 -2
- package/dist/bin/api-server.cjs +18989 -13102
- package/dist/bin/api-server.js +4 -4
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +3 -3
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +10493 -9135
- package/dist/bin/mcp-stdio-server.js +9 -7
- package/dist/bin/paa-harvest.cjs +308 -41
- package/dist/bin/paa-harvest.js +4 -3
- package/dist/{chunk-RODSHBN7.js → chunk-57F5PKS5.js} +1 -1
- package/dist/chunk-7RQULQF3.js +263 -0
- package/dist/{chunk-UL4ZKAWZ.js → chunk-A66DGFOU.js} +1 -1
- package/dist/{chunk-ES25GP6C.js → chunk-CXY5WV45.js} +24 -34
- package/dist/{chunk-Y46YNQMM.js → chunk-F5TK4KMT.js} +1 -1
- package/dist/{chunk-62LEXS5O.js → chunk-F6MUGMRN.js} +306 -44
- package/dist/{chunk-5RFQGIYC.js → chunk-FVL4GUTP.js} +2 -2
- package/dist/{chunk-OZIUFN6B.js → chunk-I45JV4EU.js} +2 -2
- package/dist/{chunk-2LXTOAKS.js → chunk-KIPIBPUB.js} +9 -1
- package/dist/chunk-MB72PA6S.js +2003 -0
- package/dist/chunk-OM7HVEJ3.js +26 -0
- package/dist/chunk-PJEEKOUM.js +404 -0
- package/dist/{chunk-RX2QAHML.js → chunk-RAFQEPJ4.js} +2 -2
- package/dist/{chunk-KO5CK5ZT.js → chunk-SR7GSLEA.js} +202 -441
- package/dist/chunk-T3MZISOF.js +240 -0
- package/dist/{chunk-65FTMB7G.js → chunk-WEFPBAAG.js} +180 -9
- package/dist/{chunk-LN6N3YLK.js → chunk-WFQ2E4WE.js} +1 -1
- package/dist/{chunk-2LMMZUU5.js → chunk-XYRD4KLT.js} +3173 -2019
- package/dist/{db-ZGZAKYBW.js → db-566MOHHD.js} +7 -1
- package/dist/{extract-bundle-SXSJCPQI.js → extract-bundle-337ZDIIQ.js} +4 -4
- package/dist/gmail-service-E276SBJU.js +25 -0
- package/dist/index.cjs +322 -49
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +4 -3
- package/dist/{lead-list-enrichment-repository-CXLVIV3R.js → lead-list-enrichment-repository-WWEMJ7EN.js} +2 -2
- package/dist/{location-data-repository-G72M7DTN.js → location-data-repository-H4B2BWGY.js} +2 -2
- package/dist/{server-VDJBV4V4.js → server-QY2AZ2OW.js} +8377 -6637
- package/dist/{site-extract-repository-RLB3TFJ3.js → site-extract-repository-ACNAVNDN.js} +3 -3
- package/dist/{worker-KHSJG365.js → worker-JQORPCFQ.js} +9 -7
- package/package.json +1 -1
- package/dist/chunk-2L4C4DAZ.js +0 -103
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// src/api/connected-data-artifacts.ts
|
|
2
|
+
import { createHash, randomUUID } from "crypto";
|
|
3
|
+
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
var CONNECTED_DATA_ARTIFACT_PREFIX = "connected-data-exports/";
|
|
7
|
+
var CONNECTED_DATA_ARTIFACT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8
|
+
var CONNECTED_DATA_DOWNLOAD_TTL_MS = 15 * 60 * 1e3;
|
|
9
|
+
function privateBlobToken() {
|
|
10
|
+
return process.env.CONNECTED_DATA_READ_WRITE_TOKEN?.trim() || process.env.CONNECTED_DATA_BLOB_READ_WRITE_TOKEN?.trim() || null;
|
|
11
|
+
}
|
|
12
|
+
function localBaseDir() {
|
|
13
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join(homedir(), "Downloads", "mcp-scraper");
|
|
14
|
+
}
|
|
15
|
+
function isHosted() {
|
|
16
|
+
return process.env.VERCEL === "1" || process.env.NODE_ENV === "production";
|
|
17
|
+
}
|
|
18
|
+
function safeFilename(value) {
|
|
19
|
+
const safe = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
20
|
+
return (safe || "connected-data-export").slice(0, 120);
|
|
21
|
+
}
|
|
22
|
+
function artifactTimestamp(artifactId) {
|
|
23
|
+
if (!artifactId.startsWith(CONNECTED_DATA_ARTIFACT_PREFIX)) return null;
|
|
24
|
+
const filename = artifactId.split("/").at(-1) ?? "";
|
|
25
|
+
const match = filename.match(/^(\d{13})-/);
|
|
26
|
+
if (!match) return null;
|
|
27
|
+
const timestamp = Number(match[1]);
|
|
28
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
29
|
+
}
|
|
30
|
+
function connectedDataArtifactOwnerId(artifactId) {
|
|
31
|
+
if (!artifactId.startsWith(CONNECTED_DATA_ARTIFACT_PREFIX)) return null;
|
|
32
|
+
const rest = artifactId.slice(CONNECTED_DATA_ARTIFACT_PREFIX.length);
|
|
33
|
+
const owner = rest.split("/")[0];
|
|
34
|
+
return owner || null;
|
|
35
|
+
}
|
|
36
|
+
function connectedDataArtifactExpiresAt(artifactId) {
|
|
37
|
+
const timestamp = artifactTimestamp(artifactId);
|
|
38
|
+
return timestamp === null ? null : new Date(timestamp + CONNECTED_DATA_ARTIFACT_TTL_MS);
|
|
39
|
+
}
|
|
40
|
+
function connectedDataArtifactIsExpired(artifactId, now = Date.now()) {
|
|
41
|
+
const timestamp = artifactTimestamp(artifactId);
|
|
42
|
+
return timestamp === null || timestamp + CONNECTED_DATA_ARTIFACT_TTL_MS <= now;
|
|
43
|
+
}
|
|
44
|
+
async function signedDownloadUrl(pathname, expiresAt) {
|
|
45
|
+
const token = privateBlobToken();
|
|
46
|
+
if (!token) return null;
|
|
47
|
+
const validUntil = Math.min(Date.now() + CONNECTED_DATA_DOWNLOAD_TTL_MS, expiresAt.getTime());
|
|
48
|
+
if (validUntil <= Date.now()) return null;
|
|
49
|
+
const { issueSignedToken, presignUrl } = await import("@vercel/blob");
|
|
50
|
+
const signedToken = await issueSignedToken({
|
|
51
|
+
token,
|
|
52
|
+
pathname,
|
|
53
|
+
operations: ["get"],
|
|
54
|
+
validUntil
|
|
55
|
+
});
|
|
56
|
+
const { presignedUrl } = await presignUrl(signedToken, {
|
|
57
|
+
access: "private",
|
|
58
|
+
operation: "get",
|
|
59
|
+
pathname,
|
|
60
|
+
validUntil
|
|
61
|
+
});
|
|
62
|
+
return { url: presignedUrl, expiresAt: new Date(validUntil).toISOString() };
|
|
63
|
+
}
|
|
64
|
+
async function createConnectedDataArtifact(args) {
|
|
65
|
+
return createConnectedDataBinaryArtifact({
|
|
66
|
+
...args,
|
|
67
|
+
contentType: "application/x-ndjson"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function createConnectedDataBinaryArtifact(args) {
|
|
71
|
+
const requestedCreatedAt = args.stableCreatedAt ? Date.parse(args.stableCreatedAt) : Number.NaN;
|
|
72
|
+
const createdAt = Number.isFinite(requestedCreatedAt) ? requestedCreatedAt : Date.now();
|
|
73
|
+
const filename = safeFilename(args.filename);
|
|
74
|
+
const extension = filename.includes(".") ? `.${filename.split(".").at(-1)}` : ".bin";
|
|
75
|
+
const stableSuffix = args.stableKey ? safeFilename(args.stableKey) : randomUUID();
|
|
76
|
+
const requestedPathname = `${CONNECTED_DATA_ARTIFACT_PREFIX}${args.ownerId}/${createdAt}-${safeFilename(args.exportId)}-${stableSuffix}--${filename}`;
|
|
77
|
+
const content = typeof args.content === "string" ? Buffer.from(args.content) : Buffer.from(args.content);
|
|
78
|
+
const bytes = content.byteLength;
|
|
79
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
80
|
+
const expiresAt = new Date(createdAt + CONNECTED_DATA_ARTIFACT_TTL_MS);
|
|
81
|
+
const token = privateBlobToken();
|
|
82
|
+
let artifactId = requestedPathname;
|
|
83
|
+
if (token) {
|
|
84
|
+
const { put } = await import("@vercel/blob");
|
|
85
|
+
const stored = await put(requestedPathname, content, {
|
|
86
|
+
access: "private",
|
|
87
|
+
token,
|
|
88
|
+
contentType: args.contentType,
|
|
89
|
+
addRandomSuffix: !args.stableKey,
|
|
90
|
+
...args.stableKey ? { allowOverwrite: true } : {},
|
|
91
|
+
cacheControlMaxAge: 60,
|
|
92
|
+
multipart: bytes > 100 * 1024 * 1024
|
|
93
|
+
});
|
|
94
|
+
artifactId = stored.pathname;
|
|
95
|
+
} else {
|
|
96
|
+
if (isHosted()) throw new Error("connected_data_private_blob_not_configured");
|
|
97
|
+
const path = join(localBaseDir(), "blobs", requestedPathname);
|
|
98
|
+
await mkdir(dirname(path), { recursive: true });
|
|
99
|
+
await writeFile(path, content);
|
|
100
|
+
}
|
|
101
|
+
const download = await signedDownloadUrl(artifactId, expiresAt);
|
|
102
|
+
return {
|
|
103
|
+
artifactId,
|
|
104
|
+
filename,
|
|
105
|
+
contentType: args.contentType,
|
|
106
|
+
bytes,
|
|
107
|
+
sha256,
|
|
108
|
+
createdAt: new Date(createdAt).toISOString(),
|
|
109
|
+
expiresAt: expiresAt.toISOString(),
|
|
110
|
+
downloadUrl: download?.url ?? null,
|
|
111
|
+
downloadUrlExpiresAt: download?.expiresAt ?? null,
|
|
112
|
+
readback: {
|
|
113
|
+
tool: "report_artifact_read",
|
|
114
|
+
arguments: { artifactId, offset: 0, maxBytes: 2e4 },
|
|
115
|
+
continuation: "Repeat with offset set to the previous result nextOffset until nextOffset is null."
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function renewConnectedDataArtifactDownload(args) {
|
|
120
|
+
if (connectedDataArtifactOwnerId(args.artifactId) !== args.ownerId) return null;
|
|
121
|
+
const expiresAt = connectedDataArtifactExpiresAt(args.artifactId);
|
|
122
|
+
if (!expiresAt || expiresAt.getTime() <= Date.now()) return null;
|
|
123
|
+
const download = await signedDownloadUrl(args.artifactId, expiresAt);
|
|
124
|
+
if (!download) return null;
|
|
125
|
+
return {
|
|
126
|
+
downloadUrl: download.url,
|
|
127
|
+
downloadUrlExpiresAt: download.expiresAt,
|
|
128
|
+
expiresAt: expiresAt.toISOString()
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function streamToBuffer(stream, maxBytes = Number.POSITIVE_INFINITY) {
|
|
132
|
+
const reader = stream.getReader();
|
|
133
|
+
const chunks = [];
|
|
134
|
+
let totalBytes = 0;
|
|
135
|
+
try {
|
|
136
|
+
for (; ; ) {
|
|
137
|
+
const { done, value } = await reader.read();
|
|
138
|
+
if (done) break;
|
|
139
|
+
if (value) {
|
|
140
|
+
const chunk = Buffer.from(value);
|
|
141
|
+
totalBytes += chunk.byteLength;
|
|
142
|
+
if (totalBytes > maxBytes) {
|
|
143
|
+
await reader.cancel("artifact_too_large").catch(() => void 0);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
chunks.push(chunk);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} finally {
|
|
150
|
+
reader.releaseLock();
|
|
151
|
+
}
|
|
152
|
+
return Buffer.concat(chunks);
|
|
153
|
+
}
|
|
154
|
+
async function readConnectedDataArtifactWindow(artifactId, offset, maxBytes) {
|
|
155
|
+
if (connectedDataArtifactIsExpired(artifactId)) return null;
|
|
156
|
+
const token = privateBlobToken();
|
|
157
|
+
let buffer;
|
|
158
|
+
if (token) {
|
|
159
|
+
const { get } = await import("@vercel/blob");
|
|
160
|
+
const result = await get(artifactId, { access: "private", token, useCache: false });
|
|
161
|
+
if (!result || result.statusCode !== 200) return null;
|
|
162
|
+
const streamed = await streamToBuffer(result.stream);
|
|
163
|
+
if (!streamed) return null;
|
|
164
|
+
buffer = streamed;
|
|
165
|
+
} else {
|
|
166
|
+
if (isHosted()) return null;
|
|
167
|
+
try {
|
|
168
|
+
buffer = await readFile(join(localBaseDir(), "blobs", artifactId));
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const totalBytes = buffer.length;
|
|
174
|
+
const start = Math.max(0, offset);
|
|
175
|
+
const end = Math.min(totalBytes, start + maxBytes);
|
|
176
|
+
return {
|
|
177
|
+
text: buffer.subarray(start, end).toString("utf8"),
|
|
178
|
+
totalBytes,
|
|
179
|
+
nextOffset: end < totalBytes ? end : null
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function readConnectedDataArtifactBytes(args) {
|
|
183
|
+
if (connectedDataArtifactOwnerId(args.artifactId) !== args.ownerId || connectedDataArtifactIsExpired(args.artifactId)) return null;
|
|
184
|
+
const token = privateBlobToken();
|
|
185
|
+
let buffer;
|
|
186
|
+
if (token) {
|
|
187
|
+
const { get } = await import("@vercel/blob");
|
|
188
|
+
const result = await get(args.artifactId, { access: "private", token, useCache: false });
|
|
189
|
+
if (!result || result.statusCode !== 200) return null;
|
|
190
|
+
const streamed = await streamToBuffer(result.stream, args.maxBytes ?? 25 * 1024 * 1024);
|
|
191
|
+
if (!streamed) return null;
|
|
192
|
+
buffer = streamed;
|
|
193
|
+
} else {
|
|
194
|
+
if (isHosted()) return null;
|
|
195
|
+
try {
|
|
196
|
+
const path = join(localBaseDir(), "blobs", args.artifactId);
|
|
197
|
+
const info = await stat(path);
|
|
198
|
+
if (info.size > (args.maxBytes ?? 25 * 1024 * 1024)) return null;
|
|
199
|
+
buffer = await readFile(path);
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (buffer.byteLength > (args.maxBytes ?? 25 * 1024 * 1024)) return null;
|
|
205
|
+
return buffer;
|
|
206
|
+
}
|
|
207
|
+
async function cleanupExpiredConnectedDataArtifacts(args = {}) {
|
|
208
|
+
const now = args.now ?? /* @__PURE__ */ new Date();
|
|
209
|
+
const token = privateBlobToken();
|
|
210
|
+
if (!token) return { deleted: 0, store: isHosted() ? "none" : "local" };
|
|
211
|
+
if (!args.force && !(now.getUTCHours() === 3 && now.getUTCMinutes() === 17)) {
|
|
212
|
+
return { deleted: 0, store: "private-vercel-blob", skipped: true };
|
|
213
|
+
}
|
|
214
|
+
const cutoff = now.getTime() - CONNECTED_DATA_ARTIFACT_TTL_MS;
|
|
215
|
+
const { list, del } = await import("@vercel/blob");
|
|
216
|
+
let cursor;
|
|
217
|
+
let deleted = 0;
|
|
218
|
+
for (let page = 0; page < 20; page++) {
|
|
219
|
+
const result = await list({ prefix: CONNECTED_DATA_ARTIFACT_PREFIX, token, limit: 1e3, cursor });
|
|
220
|
+
const expired = result.blobs.filter((blob) => new Date(blob.uploadedAt).getTime() <= cutoff);
|
|
221
|
+
if (expired.length > 0) {
|
|
222
|
+
await del(expired.map((blob) => blob.pathname), { token });
|
|
223
|
+
deleted += expired.length;
|
|
224
|
+
}
|
|
225
|
+
if (!result.hasMore || !result.cursor) break;
|
|
226
|
+
cursor = result.cursor;
|
|
227
|
+
}
|
|
228
|
+
return { deleted, store: "private-vercel-blob" };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export {
|
|
232
|
+
CONNECTED_DATA_ARTIFACT_PREFIX,
|
|
233
|
+
connectedDataArtifactOwnerId,
|
|
234
|
+
createConnectedDataArtifact,
|
|
235
|
+
createConnectedDataBinaryArtifact,
|
|
236
|
+
renewConnectedDataArtifactDownload,
|
|
237
|
+
readConnectedDataArtifactWindow,
|
|
238
|
+
readConnectedDataArtifactBytes,
|
|
239
|
+
cleanupExpiredConnectedDataArtifacts
|
|
240
|
+
};
|
|
@@ -379,7 +379,7 @@ var DB_URL = process.env.TURSO_DATABASE_URL ?? "file:./paa-api.db";
|
|
|
379
379
|
var DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
|
|
380
380
|
var _db = null;
|
|
381
381
|
var _rateLimitSchemaReady = false;
|
|
382
|
-
var CORE_SCHEMA_VERSION = "2026-08-
|
|
382
|
+
var CORE_SCHEMA_VERSION = "2026-08-27.1";
|
|
383
383
|
var SITE_EXTRACT_COUNTER_REPAIR_VERSION = "data-2026-07-28-site-extract-counters.1";
|
|
384
384
|
var CORE_SCHEMA_TABLES = [
|
|
385
385
|
"admin_credit_adjustment_events",
|
|
@@ -437,6 +437,7 @@ var CORE_SCHEMA_INDEXES = [
|
|
|
437
437
|
"credit_lots_user_active",
|
|
438
438
|
"credit_lot_repairs_user_applied",
|
|
439
439
|
"harvest_attempts_job_id",
|
|
440
|
+
"harvest_attempts_provider_lookup",
|
|
440
441
|
"harvest_attempts_user_created_at",
|
|
441
442
|
"inbox_messages_user_sent_at",
|
|
442
443
|
"jobs_created_at_status_user",
|
|
@@ -480,7 +481,23 @@ var CORE_SCHEMA_TRIGGERS = [
|
|
|
480
481
|
];
|
|
481
482
|
var CORE_SCHEMA_COLUMNS = {
|
|
482
483
|
credit_lots: ["never_expires"],
|
|
483
|
-
harvest_attempts: [
|
|
484
|
+
harvest_attempts: [
|
|
485
|
+
"debug_json",
|
|
486
|
+
"browser_provider",
|
|
487
|
+
"provider_session_id",
|
|
488
|
+
"provider_lookup_status",
|
|
489
|
+
"provider_status",
|
|
490
|
+
"provider_error_code",
|
|
491
|
+
"provider_error_message",
|
|
492
|
+
"provider_reason_source",
|
|
493
|
+
"provider_duration_ms",
|
|
494
|
+
"provider_bandwidth_bytes",
|
|
495
|
+
"provider_captcha",
|
|
496
|
+
"provider_checked_at",
|
|
497
|
+
"provider_detail_json",
|
|
498
|
+
"provider_disconnect_observed",
|
|
499
|
+
"provider_disconnect_message"
|
|
500
|
+
],
|
|
484
501
|
jobs: ["public_error_json"],
|
|
485
502
|
oauth_tokens: ["replaced_by", "rotated_at"],
|
|
486
503
|
site_extract_jobs: [
|
|
@@ -689,6 +706,20 @@ async function migrate(options = {}) {
|
|
|
689
706
|
browser_close_succeeded INTEGER,
|
|
690
707
|
browser_close_error TEXT,
|
|
691
708
|
debug_json TEXT,
|
|
709
|
+
browser_provider TEXT,
|
|
710
|
+
provider_session_id TEXT,
|
|
711
|
+
provider_lookup_status TEXT,
|
|
712
|
+
provider_status TEXT,
|
|
713
|
+
provider_error_code TEXT,
|
|
714
|
+
provider_error_message TEXT,
|
|
715
|
+
provider_reason_source TEXT,
|
|
716
|
+
provider_duration_ms INTEGER,
|
|
717
|
+
provider_bandwidth_bytes INTEGER,
|
|
718
|
+
provider_captcha INTEGER,
|
|
719
|
+
provider_checked_at TEXT,
|
|
720
|
+
provider_detail_json TEXT,
|
|
721
|
+
provider_disconnect_observed INTEGER,
|
|
722
|
+
provider_disconnect_message TEXT,
|
|
692
723
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
693
724
|
completed_at TEXT,
|
|
694
725
|
UNIQUE(job_id, attempt_number)
|
|
@@ -700,6 +731,28 @@ async function migrate(options = {}) {
|
|
|
700
731
|
await db.execute(`ALTER TABLE harvest_attempts ADD COLUMN debug_json TEXT`);
|
|
701
732
|
} catch {
|
|
702
733
|
}
|
|
734
|
+
for (const column of [
|
|
735
|
+
"browser_provider TEXT",
|
|
736
|
+
"provider_session_id TEXT",
|
|
737
|
+
"provider_lookup_status TEXT",
|
|
738
|
+
"provider_status TEXT",
|
|
739
|
+
"provider_error_code TEXT",
|
|
740
|
+
"provider_error_message TEXT",
|
|
741
|
+
"provider_reason_source TEXT",
|
|
742
|
+
"provider_duration_ms INTEGER",
|
|
743
|
+
"provider_bandwidth_bytes INTEGER",
|
|
744
|
+
"provider_captcha INTEGER",
|
|
745
|
+
"provider_checked_at TEXT",
|
|
746
|
+
"provider_detail_json TEXT",
|
|
747
|
+
"provider_disconnect_observed INTEGER",
|
|
748
|
+
"provider_disconnect_message TEXT"
|
|
749
|
+
]) {
|
|
750
|
+
try {
|
|
751
|
+
await db.execute(`ALTER TABLE harvest_attempts ADD COLUMN ${column}`);
|
|
752
|
+
} catch {
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS harvest_attempts_provider_lookup ON harvest_attempts(browser_provider, provider_lookup_status, provider_checked_at)`);
|
|
703
756
|
try {
|
|
704
757
|
await db.execute(`ALTER TABLE users ADD COLUMN password_hash TEXT`);
|
|
705
758
|
} catch {
|
|
@@ -1854,6 +1907,20 @@ function rowToHarvestAttemptLog(row) {
|
|
|
1854
1907
|
kernel_delete_error: row.kernel_delete_error != null ? String(row.kernel_delete_error) : null,
|
|
1855
1908
|
browser_close_succeeded: nullableBoolean(row.browser_close_succeeded),
|
|
1856
1909
|
browser_close_error: row.browser_close_error != null ? String(row.browser_close_error) : null,
|
|
1910
|
+
browser_provider: row.browser_provider != null ? String(row.browser_provider) : null,
|
|
1911
|
+
provider_session_id: row.provider_session_id != null ? String(row.provider_session_id) : null,
|
|
1912
|
+
provider_lookup_status: row.provider_lookup_status != null ? String(row.provider_lookup_status) : null,
|
|
1913
|
+
provider_status: row.provider_status != null ? String(row.provider_status) : null,
|
|
1914
|
+
provider_error_code: row.provider_error_code != null ? String(row.provider_error_code) : null,
|
|
1915
|
+
provider_error_message: row.provider_error_message != null ? String(row.provider_error_message) : null,
|
|
1916
|
+
provider_reason_source: row.provider_reason_source != null ? String(row.provider_reason_source) : null,
|
|
1917
|
+
provider_duration_ms: row.provider_duration_ms != null ? Number(row.provider_duration_ms) : null,
|
|
1918
|
+
provider_bandwidth_bytes: row.provider_bandwidth_bytes != null ? Number(row.provider_bandwidth_bytes) : null,
|
|
1919
|
+
provider_captcha: nullableBoolean(row.provider_captcha),
|
|
1920
|
+
provider_checked_at: row.provider_checked_at != null ? String(row.provider_checked_at) : null,
|
|
1921
|
+
provider_detail: parseNullableJson(row.provider_detail_json),
|
|
1922
|
+
provider_disconnect_observed: nullableBoolean(row.provider_disconnect_observed),
|
|
1923
|
+
provider_disconnect_message: row.provider_disconnect_message != null ? String(row.provider_disconnect_message) : null,
|
|
1857
1924
|
debug: parseNullableJson(row.debug_json),
|
|
1858
1925
|
created_at: String(row.created_at),
|
|
1859
1926
|
completed_at: row.completed_at != null ? String(row.completed_at) : null
|
|
@@ -1906,13 +1973,7 @@ async function startHarvestAttempt(input) {
|
|
|
1906
1973
|
id, job_id, user_id, attempt_number, max_attempts, query, location, max_questions, status, created_at
|
|
1907
1974
|
)
|
|
1908
1975
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?)
|
|
1909
|
-
ON CONFLICT(job_id, attempt_number) DO
|
|
1910
|
-
status = 'running',
|
|
1911
|
-
created_at = excluded.created_at,
|
|
1912
|
-
completed_at = NULL,
|
|
1913
|
-
outcome = NULL,
|
|
1914
|
-
error = NULL,
|
|
1915
|
-
debug_json = NULL
|
|
1976
|
+
ON CONFLICT(job_id, attempt_number) DO NOTHING
|
|
1916
1977
|
`,
|
|
1917
1978
|
args: [
|
|
1918
1979
|
randomUUID(),
|
|
@@ -1943,6 +2004,32 @@ async function finishHarvestAttempt(input) {
|
|
|
1943
2004
|
kernel_delete_error = ?,
|
|
1944
2005
|
browser_close_succeeded = ?,
|
|
1945
2006
|
browser_close_error = ?,
|
|
2007
|
+
browser_provider = COALESCE(browser_provider, ?),
|
|
2008
|
+
provider_session_id = COALESCE(provider_session_id, ?),
|
|
2009
|
+
provider_lookup_status = CASE
|
|
2010
|
+
WHEN provider_lookup_status = 'complete' THEN provider_lookup_status
|
|
2011
|
+
ELSE COALESCE(?, provider_lookup_status)
|
|
2012
|
+
END,
|
|
2013
|
+
provider_status = COALESCE(provider_status, ?),
|
|
2014
|
+
provider_error_code = CASE
|
|
2015
|
+
WHEN provider_reason_source = 'provider_session_api' THEN provider_error_code
|
|
2016
|
+
ELSE COALESCE(?, provider_error_code)
|
|
2017
|
+
END,
|
|
2018
|
+
provider_error_message = CASE
|
|
2019
|
+
WHEN provider_reason_source = 'provider_session_api' THEN provider_error_message
|
|
2020
|
+
ELSE COALESCE(?, provider_error_message)
|
|
2021
|
+
END,
|
|
2022
|
+
provider_reason_source = CASE
|
|
2023
|
+
WHEN provider_reason_source = 'provider_session_api' THEN provider_reason_source
|
|
2024
|
+
ELSE COALESCE(?, provider_reason_source)
|
|
2025
|
+
END,
|
|
2026
|
+
provider_duration_ms = COALESCE(provider_duration_ms, ?),
|
|
2027
|
+
provider_bandwidth_bytes = COALESCE(provider_bandwidth_bytes, ?),
|
|
2028
|
+
provider_captcha = COALESCE(provider_captcha, ?),
|
|
2029
|
+
provider_checked_at = COALESCE(provider_checked_at, ?),
|
|
2030
|
+
provider_detail_json = COALESCE(provider_detail_json, ?),
|
|
2031
|
+
provider_disconnect_observed = COALESCE(provider_disconnect_observed, ?),
|
|
2032
|
+
provider_disconnect_message = COALESCE(provider_disconnect_message, ?),
|
|
1946
2033
|
debug_json = ?,
|
|
1947
2034
|
completed_at = ?
|
|
1948
2035
|
WHERE job_id = ? AND attempt_number = ?
|
|
@@ -1959,6 +2046,20 @@ async function finishHarvestAttempt(input) {
|
|
|
1959
2046
|
input.kernelDeleteError,
|
|
1960
2047
|
input.browserCloseSucceeded == null ? null : input.browserCloseSucceeded ? 1 : 0,
|
|
1961
2048
|
input.browserCloseError,
|
|
2049
|
+
input.browserProvider ?? null,
|
|
2050
|
+
input.providerSessionId ?? null,
|
|
2051
|
+
input.providerLookupStatus ?? null,
|
|
2052
|
+
input.providerStatus ?? null,
|
|
2053
|
+
input.providerErrorCode ?? null,
|
|
2054
|
+
input.providerErrorMessage ?? null,
|
|
2055
|
+
input.providerReasonSource ?? null,
|
|
2056
|
+
input.providerDurationMs ?? null,
|
|
2057
|
+
input.providerBandwidthBytes ?? null,
|
|
2058
|
+
input.providerCaptcha == null ? null : input.providerCaptcha ? 1 : 0,
|
|
2059
|
+
input.providerCheckedAt ?? null,
|
|
2060
|
+
input.providerDetail == null ? null : boundedJson(input.providerDetail),
|
|
2061
|
+
input.providerDisconnectObserved == null ? null : input.providerDisconnectObserved ? 1 : 0,
|
|
2062
|
+
input.providerDisconnectMessage ?? null,
|
|
1962
2063
|
input.debug == null ? null : JSON.stringify(input.debug),
|
|
1963
2064
|
input.completedAt,
|
|
1964
2065
|
input.jobId,
|
|
@@ -1966,6 +2067,65 @@ async function finishHarvestAttempt(input) {
|
|
|
1966
2067
|
]
|
|
1967
2068
|
});
|
|
1968
2069
|
}
|
|
2070
|
+
function boundedJson(value, maxBytes = 4096) {
|
|
2071
|
+
const serialized = JSON.stringify(value);
|
|
2072
|
+
return serialized.length <= maxBytes ? serialized : JSON.stringify({ truncated: true });
|
|
2073
|
+
}
|
|
2074
|
+
async function updateHarvestAttemptProviderTelemetry(input) {
|
|
2075
|
+
const result = await getDb().execute({
|
|
2076
|
+
sql: `
|
|
2077
|
+
UPDATE harvest_attempts SET
|
|
2078
|
+
provider_lookup_status = ?,
|
|
2079
|
+
provider_status = ?,
|
|
2080
|
+
provider_error_code = ?,
|
|
2081
|
+
provider_error_message = ?,
|
|
2082
|
+
provider_reason_source = ?,
|
|
2083
|
+
provider_duration_ms = ?,
|
|
2084
|
+
provider_bandwidth_bytes = ?,
|
|
2085
|
+
provider_captcha = ?,
|
|
2086
|
+
provider_checked_at = ?,
|
|
2087
|
+
provider_detail_json = ?
|
|
2088
|
+
WHERE id = ?
|
|
2089
|
+
AND job_id = ?
|
|
2090
|
+
AND user_id = ?
|
|
2091
|
+
AND browser_provider = 'bright_data_browser_api'
|
|
2092
|
+
AND provider_session_id = ?
|
|
2093
|
+
AND COALESCE(provider_lookup_status, '') <> 'complete'
|
|
2094
|
+
`,
|
|
2095
|
+
args: [
|
|
2096
|
+
input.lookupStatus,
|
|
2097
|
+
input.providerStatus ?? null,
|
|
2098
|
+
input.errorCode ?? null,
|
|
2099
|
+
input.errorMessage ?? null,
|
|
2100
|
+
input.reasonSource ?? null,
|
|
2101
|
+
input.durationMs ?? null,
|
|
2102
|
+
input.bandwidthBytes ?? null,
|
|
2103
|
+
input.captcha == null ? null : input.captcha ? 1 : 0,
|
|
2104
|
+
input.checkedAt,
|
|
2105
|
+
input.detail == null ? null : boundedJson(input.detail),
|
|
2106
|
+
input.attemptId,
|
|
2107
|
+
input.jobId,
|
|
2108
|
+
input.userId,
|
|
2109
|
+
input.providerSessionId
|
|
2110
|
+
]
|
|
2111
|
+
});
|
|
2112
|
+
return result.rowsAffected === 1;
|
|
2113
|
+
}
|
|
2114
|
+
async function listHarvestAttemptsPendingProviderTelemetry(limit = 25) {
|
|
2115
|
+
const boundedLimit = Math.max(1, Math.min(100, Math.trunc(limit)));
|
|
2116
|
+
const result = await getDb().execute({
|
|
2117
|
+
sql: `
|
|
2118
|
+
SELECT * FROM harvest_attempts
|
|
2119
|
+
WHERE browser_provider = 'bright_data_browser_api'
|
|
2120
|
+
AND provider_session_id IS NOT NULL
|
|
2121
|
+
AND provider_lookup_status IN ('pending', 'temporary_error')
|
|
2122
|
+
ORDER BY COALESCE(provider_checked_at, created_at) ASC
|
|
2123
|
+
LIMIT ?
|
|
2124
|
+
`,
|
|
2125
|
+
args: [boundedLimit]
|
|
2126
|
+
});
|
|
2127
|
+
return result.rows.map((row) => rowToHarvestAttemptLog(row));
|
|
2128
|
+
}
|
|
1969
2129
|
async function listHarvestAttempts(jobId, userId) {
|
|
1970
2130
|
const res = userId == null ? await getDb().execute({
|
|
1971
2131
|
sql: `SELECT * FROM harvest_attempts WHERE job_id = ? ORDER BY attempt_number ASC`,
|
|
@@ -2502,6 +2662,14 @@ async function completeJob(id, result) {
|
|
|
2502
2662
|
args: [JSON.stringify(result), id]
|
|
2503
2663
|
});
|
|
2504
2664
|
}
|
|
2665
|
+
async function completeRunningJob(id, result) {
|
|
2666
|
+
const updated = await getDb().execute({
|
|
2667
|
+
sql: `UPDATE jobs SET status = 'done', result = ?, error = NULL, public_error_json = NULL, completed_at = datetime('now')
|
|
2668
|
+
WHERE id = ? AND status = 'running'`,
|
|
2669
|
+
args: [JSON.stringify(result), id]
|
|
2670
|
+
});
|
|
2671
|
+
return updated.rowsAffected === 1;
|
|
2672
|
+
}
|
|
2505
2673
|
async function failJob(id, error, publicError) {
|
|
2506
2674
|
await getDb().execute({
|
|
2507
2675
|
sql: `UPDATE jobs SET status = 'failed', error = ?, public_error_json = ?, completed_at = datetime('now') WHERE id = ?`,
|
|
@@ -3574,6 +3742,8 @@ export {
|
|
|
3574
3742
|
getOrCreateRunningJob,
|
|
3575
3743
|
startHarvestAttempt,
|
|
3576
3744
|
finishHarvestAttempt,
|
|
3745
|
+
updateHarvestAttemptProviderTelemetry,
|
|
3746
|
+
listHarvestAttemptsPendingProviderTelemetry,
|
|
3577
3747
|
listHarvestAttempts,
|
|
3578
3748
|
getJob,
|
|
3579
3749
|
listJobs,
|
|
@@ -3611,6 +3781,7 @@ export {
|
|
|
3611
3781
|
listStaleRunningDurablePaaJobs,
|
|
3612
3782
|
markJobBillingSettled,
|
|
3613
3783
|
completeJob,
|
|
3784
|
+
completeRunningJob,
|
|
3614
3785
|
failJob,
|
|
3615
3786
|
failRunningJob,
|
|
3616
3787
|
cancelJob,
|