mcp-scraper 0.4.5 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/api-server.cjs +1866 -1376
- 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 +89 -1
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/bin/paa-harvest.cjs +13 -4
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +2 -2
- package/dist/{chunk-NJK5BTUK.js → chunk-2YY46QYT.js} +28 -11
- package/dist/chunk-2YY46QYT.js.map +1 -0
- package/dist/{chunk-WI4A3KFQ.js → chunk-6EXP6DQG.js} +90 -2
- package/dist/chunk-6EXP6DQG.js.map +1 -0
- package/dist/{chunk-5LO6EHEH.js → chunk-EMY7ELRU.js} +35 -7
- package/dist/chunk-EMY7ELRU.js.map +1 -0
- package/dist/{chunk-FUVQR6GK.js → chunk-GL4BW4CP.js} +49 -1
- package/dist/chunk-GL4BW4CP.js.map +1 -0
- package/dist/{chunk-3UH3BEIZ.js → chunk-HE2LQPJ2.js} +2 -2
- package/dist/{chunk-4ZZWFI6L.js → chunk-ONIOF5XW.js} +3 -3
- package/dist/chunk-SHXJQQOH.js +7 -0
- package/dist/chunk-SHXJQQOH.js.map +1 -0
- package/dist/{db-H3S3M6KK.js → db-LIOTIWVN.js} +6 -2
- package/dist/{extract-bundle-UKE273TV.js → extract-bundle-COS56ZDO.js} +2 -2
- package/dist/index.cjs +59 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/{server-XCDFHHLE.js → server-UNID3SJU.js} +570 -276
- package/dist/server-UNID3SJU.js.map +1 -0
- package/dist/{site-extract-repository-THBVEXMP.js → site-extract-repository-HTIY52MW.js} +4 -4
- package/dist/{worker-YHKUJR5P.js → worker-HTYZAYGB.js} +18 -14
- package/dist/worker-HTYZAYGB.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-5LO6EHEH.js.map +0 -1
- package/dist/chunk-FUVQR6GK.js.map +0 -1
- package/dist/chunk-NJK5BTUK.js.map +0 -1
- package/dist/chunk-UGQC2FOX.js +0 -7
- package/dist/chunk-UGQC2FOX.js.map +0 -1
- package/dist/chunk-WI4A3KFQ.js.map +0 -1
- package/dist/server-XCDFHHLE.js.map +0 -1
- package/dist/worker-YHKUJR5P.js.map +0 -1
- /package/dist/{chunk-3UH3BEIZ.js.map → chunk-HE2LQPJ2.js.map} +0 -0
- /package/dist/{chunk-4ZZWFI6L.js.map → chunk-ONIOF5XW.js.map} +0 -0
- /package/dist/{db-H3S3M6KK.js.map → db-LIOTIWVN.js.map} +0 -0
- /package/dist/{extract-bundle-UKE273TV.js.map → extract-bundle-COS56ZDO.js.map} +0 -0
- /package/dist/{site-extract-repository-THBVEXMP.js.map → site-extract-repository-HTIY52MW.js.map} +0 -0
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
createHarvestAttemptRecorder,
|
|
4
4
|
harvestProblemResponse,
|
|
5
5
|
serializeHarvestProblem
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-HE2LQPJ2.js";
|
|
7
7
|
import {
|
|
8
8
|
BrowserDriver,
|
|
9
9
|
MapsSelectors,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
recordVendorUsage,
|
|
17
17
|
resolveKernelProxyId,
|
|
18
18
|
runWithCostContext
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-2YY46QYT.js";
|
|
20
20
|
import {
|
|
21
21
|
HttpMcpToolExecutor,
|
|
22
22
|
MemoryMcpToolExecutor,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
registerSerpIntelligenceCaptureTools,
|
|
32
32
|
sanitizeAttempts,
|
|
33
33
|
sanitizeHarvestResult
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-6EXP6DQG.js";
|
|
35
35
|
import {
|
|
36
36
|
auditImages,
|
|
37
37
|
getBlobStore
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
RawMapsOverviewSchema,
|
|
66
66
|
RawMapsReviewStatsSchema
|
|
67
67
|
} from "./chunk-XGIPATLV.js";
|
|
68
|
-
import "./chunk-
|
|
68
|
+
import "./chunk-SHXJQQOH.js";
|
|
69
69
|
import {
|
|
70
70
|
completeExtractJob,
|
|
71
71
|
countSuccessfulPages,
|
|
@@ -76,7 +76,7 @@ import {
|
|
|
76
76
|
saveExtractPages,
|
|
77
77
|
setExtractJobTotal,
|
|
78
78
|
settleExtractJob
|
|
79
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-ONIOF5XW.js";
|
|
80
80
|
import {
|
|
81
81
|
BROWSER_OPEN_MIN_BALANCE_MC,
|
|
82
82
|
CONCURRENCY_PRICE_ID,
|
|
@@ -90,12 +90,14 @@ import {
|
|
|
90
90
|
MEMORY_PLANS,
|
|
91
91
|
MEMORY_PLAN_QUOTA,
|
|
92
92
|
SCHEDULING_PLANS,
|
|
93
|
+
SITE_AUDIT_LLM_MARGIN_MULTIPLE,
|
|
93
94
|
SUBSCRIPTION_TIERS,
|
|
94
95
|
SUBSCRIPTION_TIER_BY_KEY,
|
|
96
|
+
VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE,
|
|
95
97
|
browserActiveCostMc,
|
|
96
98
|
concurrencySlotBillingInfo,
|
|
97
99
|
insufficientBalanceResponse
|
|
98
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-EMY7ELRU.js";
|
|
99
101
|
import {
|
|
100
102
|
CaptchaError,
|
|
101
103
|
RECAPTCHA_INSTRUCTIONS,
|
|
@@ -131,6 +133,7 @@ import {
|
|
|
131
133
|
getLedger,
|
|
132
134
|
getMemoryPlan,
|
|
133
135
|
getMemoryProvisioned,
|
|
136
|
+
getPageSnapshot,
|
|
134
137
|
getRefresh,
|
|
135
138
|
getUserByApiKey,
|
|
136
139
|
getUserByEmail,
|
|
@@ -175,8 +178,9 @@ import {
|
|
|
175
178
|
setStripeCustomerId,
|
|
176
179
|
setSubscriptionTier,
|
|
177
180
|
setUserMemoryCreds,
|
|
181
|
+
upsertPageSnapshot,
|
|
178
182
|
verifyPassword
|
|
179
|
-
} from "./chunk-
|
|
183
|
+
} from "./chunk-GL4BW4CP.js";
|
|
180
184
|
|
|
181
185
|
// src/blog/registry.ts
|
|
182
186
|
var posts = [
|
|
@@ -5337,6 +5341,7 @@ function extractJson(raw) {
|
|
|
5337
5341
|
var DeepInfraLlmClient = class {
|
|
5338
5342
|
apiKey;
|
|
5339
5343
|
model;
|
|
5344
|
+
totalCostUsd = 0;
|
|
5340
5345
|
constructor(apiKey, model = "Qwen/Qwen3.6-35B-A3B") {
|
|
5341
5346
|
this.apiKey = apiKey;
|
|
5342
5347
|
this.model = model;
|
|
@@ -5369,6 +5374,7 @@ ${raw}`
|
|
|
5369
5374
|
var OpenRouterLlmClient = class {
|
|
5370
5375
|
apiKey;
|
|
5371
5376
|
model;
|
|
5377
|
+
totalCostUsd = 0;
|
|
5372
5378
|
constructor(apiKey, model = "qwen/qwen3-235b-a22b") {
|
|
5373
5379
|
this.apiKey = apiKey;
|
|
5374
5380
|
this.model = model;
|
|
@@ -5381,6 +5387,9 @@ var OpenRouterLlmClient = class {
|
|
|
5381
5387
|
});
|
|
5382
5388
|
if (!resp.ok) throw new Error(`OpenRouter error ${resp.status}: ${await resp.text()}`);
|
|
5383
5389
|
const data = await resp.json();
|
|
5390
|
+
const cost = data.usage?.cost ?? 0;
|
|
5391
|
+
this.totalCostUsd += cost;
|
|
5392
|
+
void recordVendorUsage({ vendor: "openrouter", model: this.model, units: cost, unitType: "usd" });
|
|
5384
5393
|
return data.choices[0].message.content;
|
|
5385
5394
|
}
|
|
5386
5395
|
async completeJson(prompt) {
|
|
@@ -5406,6 +5415,9 @@ var FallbackLlmClient = class {
|
|
|
5406
5415
|
secondary;
|
|
5407
5416
|
lastServedBy = null;
|
|
5408
5417
|
lastPrimaryError = null;
|
|
5418
|
+
get totalCostUsd() {
|
|
5419
|
+
return this.primary.totalCostUsd + this.secondary.totalCostUsd;
|
|
5420
|
+
}
|
|
5409
5421
|
async complete(prompt) {
|
|
5410
5422
|
try {
|
|
5411
5423
|
const result = await this.primary.complete(prompt);
|
|
@@ -6892,8 +6904,23 @@ var SiteAuditService = class {
|
|
|
6892
6904
|
async getJobsForUser(userId) {
|
|
6893
6905
|
return this.deps.repo.listSiteAuditJobs(userId);
|
|
6894
6906
|
}
|
|
6907
|
+
async runLlmBilled(userId, source, fn) {
|
|
6908
|
+
const before = this.deps.llm.totalCostUsd;
|
|
6909
|
+
const result = await runWithCostContext({ ...currentCostContext(), op: "audit_site", userId }, fn);
|
|
6910
|
+
const deltaCostUsd = this.deps.llm.totalCostUsd - before;
|
|
6911
|
+
if (deltaCostUsd <= 0) return result;
|
|
6912
|
+
const mc = Math.round(deltaCostUsd * SITE_AUDIT_LLM_MARGIN_MULTIPLE * MC_PER_USD);
|
|
6913
|
+
if (mc <= 0) return result;
|
|
6914
|
+
const { ok } = await debitMc(userId, mc, LedgerOperation.SITE_AUDIT_LLM, source);
|
|
6915
|
+
if (!ok) {
|
|
6916
|
+
console.warn(`[site-audit-service] SITE_AUDIT_LLM surcharge debit failed for user ${userId} (${source}): insufficient balance for ${mc}mc`);
|
|
6917
|
+
}
|
|
6918
|
+
return result;
|
|
6919
|
+
}
|
|
6895
6920
|
async runIngestPhase(jobId, payload) {
|
|
6896
6921
|
const { request } = payload;
|
|
6922
|
+
const job = await this.deps.repo.getSiteAuditJob(jobId);
|
|
6923
|
+
if (!job) throw new Error(`[site-audit-service] runIngestPhase: job ${jobId} not found`);
|
|
6897
6924
|
const input = {
|
|
6898
6925
|
clientDomain: request.clientDomain,
|
|
6899
6926
|
sfInternalPath: request.sfInternalPath,
|
|
@@ -6908,14 +6935,18 @@ var SiteAuditService = class {
|
|
|
6908
6935
|
businessContext: request.businessContext,
|
|
6909
6936
|
priorAuditMemoryEnabled: request.priorAuditMemoryEnabled
|
|
6910
6937
|
};
|
|
6911
|
-
const result = await
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6938
|
+
const result = await this.runLlmBilled(
|
|
6939
|
+
job.user_id,
|
|
6940
|
+
`${jobId}:phase1-ingest`,
|
|
6941
|
+
() => llmParseWithRetry(
|
|
6942
|
+
ValidatedFileManifestSchema,
|
|
6943
|
+
(attempt, error) => {
|
|
6944
|
+
void attempt;
|
|
6945
|
+
void error;
|
|
6946
|
+
return buildIngestValidatePrompt(input);
|
|
6947
|
+
},
|
|
6948
|
+
this.deps.llm
|
|
6949
|
+
)
|
|
6919
6950
|
);
|
|
6920
6951
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase1-ingest", result);
|
|
6921
6952
|
}
|
|
@@ -6930,23 +6961,27 @@ var SiteAuditService = class {
|
|
|
6930
6961
|
const competitorDomainsCsvPath = request.competitorDomainsCsvPath;
|
|
6931
6962
|
void competitorDomainsCsvPath;
|
|
6932
6963
|
const competitors = await this._readCompetitorDomains(request);
|
|
6933
|
-
const results = await
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
(
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6964
|
+
const results = await this.runLlmBilled(
|
|
6965
|
+
job.user_id,
|
|
6966
|
+
`${jobId}:phase1-enrich`,
|
|
6967
|
+
() => Promise.all(
|
|
6968
|
+
competitors.map(
|
|
6969
|
+
(domain, idx) => limit(async () => {
|
|
6970
|
+
const competitorIndex = idx + 1;
|
|
6971
|
+
const rawPrompt = buildEnrichCompetitorPrompt({ domain, sessionPath });
|
|
6972
|
+
const prompt = rawPrompt.replace("<competitor-index>", String(competitorIndex));
|
|
6973
|
+
const result = await llmParseWithRetry(
|
|
6974
|
+
EnrichCompetitorOutputSchema,
|
|
6975
|
+
(attempt, error) => {
|
|
6976
|
+
void attempt;
|
|
6977
|
+
void error;
|
|
6978
|
+
return prompt;
|
|
6979
|
+
},
|
|
6980
|
+
this.deps.llm
|
|
6981
|
+
);
|
|
6982
|
+
return result;
|
|
6983
|
+
})
|
|
6984
|
+
)
|
|
6950
6985
|
)
|
|
6951
6986
|
);
|
|
6952
6987
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase1-ingest", { competitors: results });
|
|
@@ -6960,19 +6995,23 @@ var SiteAuditService = class {
|
|
|
6960
6995
|
const clientDomain = request.clientDomain;
|
|
6961
6996
|
const urlInventory = [];
|
|
6962
6997
|
const directedEdgeCount = 0;
|
|
6963
|
-
const orphanAnnotation = await
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6998
|
+
const orphanAnnotation = await this.runLlmBilled(
|
|
6999
|
+
job.user_id,
|
|
7000
|
+
`${jobId}:phase2-build-graph`,
|
|
7001
|
+
() => llmParseWithRetry(
|
|
7002
|
+
OrphanAnnotationOutputSchema,
|
|
7003
|
+
(attempt, error) => {
|
|
7004
|
+
void attempt;
|
|
7005
|
+
void error;
|
|
7006
|
+
return buildGraphPerSitePrompt({
|
|
7007
|
+
siteId: "client",
|
|
7008
|
+
domain: clientDomain,
|
|
7009
|
+
urlInventory,
|
|
7010
|
+
directedEdgeCount
|
|
7011
|
+
});
|
|
7012
|
+
},
|
|
7013
|
+
this.deps.llm
|
|
7014
|
+
)
|
|
6976
7015
|
);
|
|
6977
7016
|
const graphMetrics = await this.deps.python.runGraphMetrics({
|
|
6978
7017
|
directedLinkGraphPath: `${sessionPath}/phase1/directed_link_graph.json`,
|
|
@@ -7017,50 +7056,52 @@ var SiteAuditService = class {
|
|
|
7017
7056
|
}
|
|
7018
7057
|
if (currentBatch.length > 0) batches.push(currentBatch);
|
|
7019
7058
|
const usZipsCsvPath = process.env["US_ZIPS_CSV_PATH"] ?? "/Users/vilovieta/Downloads/sales-magician-api-leads-magician-01c6cff78e31/tools/analytics/data/uszips.csv";
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7059
|
+
await this.runLlmBilled(job.user_id, `${jobId}:phase3-classify`, async () => {
|
|
7060
|
+
for (const batch of batches) {
|
|
7061
|
+
let classified = [];
|
|
7062
|
+
let unclassifiedUrls = batch.map((u) => u.url);
|
|
7063
|
+
try {
|
|
7064
|
+
const pythonResult = await this.deps.python.runLocationClassifier({
|
|
7065
|
+
urls: batch.map((u) => u.url),
|
|
7066
|
+
usZipsCsvPath
|
|
7067
|
+
});
|
|
7068
|
+
classified = pythonResult.classified;
|
|
7069
|
+
unclassifiedUrls = pythonResult.unclassified;
|
|
7070
|
+
} catch (err) {
|
|
7071
|
+
console.warn(
|
|
7072
|
+
"[site-audit-service] runClassifyPhase: python.runLocationClassifier failed (falling back to LLM):",
|
|
7073
|
+
err instanceof Error ? err.message : String(err)
|
|
7074
|
+
);
|
|
7075
|
+
unclassifiedUrls = batch.map((u) => u.url);
|
|
7076
|
+
}
|
|
7077
|
+
for (const c of classified) {
|
|
7078
|
+
allClassifications.push({
|
|
7079
|
+
url: c.url,
|
|
7080
|
+
page_type: c.pageType,
|
|
7081
|
+
confidence: c.confidence > 0.6 ? "high" : c.confidence > 0.4 ? "medium" : "low",
|
|
7082
|
+
_classification_label: "[COMPUTED:python/location-classifier]",
|
|
7083
|
+
topic_cluster: null,
|
|
7084
|
+
location_signals: c.locationSignals
|
|
7085
|
+
});
|
|
7086
|
+
}
|
|
7087
|
+
if (unclassifiedUrls.length > 0) {
|
|
7088
|
+
const unclassifiedBatch = batch.filter((u) => unclassifiedUrls.includes(u.url));
|
|
7089
|
+
const llmResults = await llmParseWithRetry(
|
|
7090
|
+
z9.array(PageTypeClassificationRowSchema),
|
|
7091
|
+
(attempt, error) => {
|
|
7092
|
+
void attempt;
|
|
7093
|
+
void error;
|
|
7094
|
+
return buildMetricsClassifyPrompt({
|
|
7095
|
+
urls: unclassifiedBatch,
|
|
7096
|
+
siteDomain: clientDomain
|
|
7097
|
+
});
|
|
7098
|
+
},
|
|
7099
|
+
this.deps.llm
|
|
7100
|
+
);
|
|
7101
|
+
allClassifications.push(...llmResults);
|
|
7102
|
+
}
|
|
7062
7103
|
}
|
|
7063
|
-
}
|
|
7104
|
+
});
|
|
7064
7105
|
void sessionPath;
|
|
7065
7106
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase3-classify", {
|
|
7066
7107
|
classifications: allClassifications,
|
|
@@ -7073,20 +7114,24 @@ var SiteAuditService = class {
|
|
|
7073
7114
|
if (!job) throw new Error(`[site-audit-service] runComparePhase: job ${jobId} not found`);
|
|
7074
7115
|
const request = JSON.parse(job.request);
|
|
7075
7116
|
const clientDomain = request.clientDomain;
|
|
7076
|
-
const result = await
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7117
|
+
const result = await this.runLlmBilled(
|
|
7118
|
+
job.user_id,
|
|
7119
|
+
`${jobId}:phase4-compare`,
|
|
7120
|
+
() => llmParseWithRetry(
|
|
7121
|
+
CompareRecommendOutputSchema,
|
|
7122
|
+
(attempt, error) => {
|
|
7123
|
+
void attempt;
|
|
7124
|
+
void error;
|
|
7125
|
+
return buildCompareRecommendPrompt({
|
|
7126
|
+
clientDomain,
|
|
7127
|
+
clientMoneyPages: [],
|
|
7128
|
+
competitorSites: [],
|
|
7129
|
+
clientUrlMetrics: [],
|
|
7130
|
+
clientPageTypes: []
|
|
7131
|
+
});
|
|
7132
|
+
},
|
|
7133
|
+
this.deps.llm
|
|
7134
|
+
)
|
|
7090
7135
|
);
|
|
7091
7136
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase4-compare", result);
|
|
7092
7137
|
return result;
|
|
@@ -7116,20 +7161,24 @@ var SiteAuditService = class {
|
|
|
7116
7161
|
total: ilesTotal,
|
|
7117
7162
|
components: ilesComponents
|
|
7118
7163
|
};
|
|
7119
|
-
const reportMarkdown = await
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7164
|
+
const reportMarkdown = await this.runLlmBilled(
|
|
7165
|
+
job.user_id,
|
|
7166
|
+
`${jobId}:phase5-synthesize`,
|
|
7167
|
+
() => llmParseWithRetry(
|
|
7168
|
+
z9.string(),
|
|
7169
|
+
(attempt, error) => {
|
|
7170
|
+
void attempt;
|
|
7171
|
+
void error;
|
|
7172
|
+
return buildScoreSynthesizePrompt({
|
|
7173
|
+
preComputedArchitectureScore,
|
|
7174
|
+
preComputedIlesScore,
|
|
7175
|
+
rankedBacklog: [],
|
|
7176
|
+
clientDomain,
|
|
7177
|
+
sessionPath
|
|
7178
|
+
});
|
|
7179
|
+
},
|
|
7180
|
+
this.deps.llm
|
|
7181
|
+
)
|
|
7133
7182
|
);
|
|
7134
7183
|
const auditResult = SiteAuditResultSchema.parse({
|
|
7135
7184
|
jobId,
|
|
@@ -7529,7 +7578,7 @@ var siteExtractFn = inngest.createFunction(
|
|
|
7529
7578
|
return auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
|
|
7530
7579
|
}) : null;
|
|
7531
7580
|
const artifacts = await step.run("finalize", async () => {
|
|
7532
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
7581
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-COS56ZDO.js");
|
|
7533
7582
|
const stored = await assembleExtractArtifacts(job, { branding, imageAudit });
|
|
7534
7583
|
await completeExtractJob(jobId, stored);
|
|
7535
7584
|
return stored;
|
|
@@ -8138,6 +8187,11 @@ internalMemoryApp.post("/ai-debit", async (c) => {
|
|
|
8138
8187
|
}
|
|
8139
8188
|
const user = await getUserByEmail(body.identity.trim().toLowerCase());
|
|
8140
8189
|
if (!user) return c.json({ ok: false, reason: "no_user" }, 200);
|
|
8190
|
+
if (body.costUsd > 0) {
|
|
8191
|
+
await runWithCostContext({ ...currentCostContext(), op: "memory_ai", userId: user.id }, async () => {
|
|
8192
|
+
void recordVendorUsage({ vendor: "mcp_memory_ai", model: null, units: body.costUsd, unitType: "usd", error: null });
|
|
8193
|
+
});
|
|
8194
|
+
}
|
|
8141
8195
|
const mc = Math.round(body.costUsd * MEMORY_AI_MARGIN_MULTIPLE * MC_PER_USD);
|
|
8142
8196
|
if (mc <= 0) return c.json({ ok: true, mc: 0, balance_mc: null }, 200);
|
|
8143
8197
|
const { ok, balance_mc } = await debitMc(user.id, mc, LedgerOperation.MEMORY_AI, `${body.source ?? "ai"}:${body.op ?? ""}`);
|
|
@@ -8658,6 +8712,11 @@ async function fetchViaYoutubeTranscript(videoId) {
|
|
|
8658
8712
|
}));
|
|
8659
8713
|
const text = chunks.map((c) => c.text).join(" ");
|
|
8660
8714
|
const last = chunks[chunks.length - 1];
|
|
8715
|
+
const now = Date.now();
|
|
8716
|
+
await runWithCostContext(
|
|
8717
|
+
{ ...currentCostContext(), op: "yt_transcription", subOp: "youtube-transcript" },
|
|
8718
|
+
() => recordKernelSession({ kernelSessionId: null, source: "yt_caption_tier", headlessSent: null, openedAtMs: now, closedAtMs: now })
|
|
8719
|
+
);
|
|
8661
8720
|
return { videoId, text, chunks, durationMs: last ? last.timestamp[1] * 1e3 : 0, method: "youtube-transcript" };
|
|
8662
8721
|
} catch {
|
|
8663
8722
|
return null;
|
|
@@ -8709,163 +8768,221 @@ function parseTimedtextXml(xml) {
|
|
|
8709
8768
|
}
|
|
8710
8769
|
return results;
|
|
8711
8770
|
}
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8771
|
+
var INNERTUBE_CLIENTS = [
|
|
8772
|
+
{ name: "ANDROID", version: "20.10.38", userAgent: "com.google.android.youtube/20.10.38 (Linux; U; Android 14)" },
|
|
8773
|
+
{ name: "IOS", version: "20.10.4", userAgent: "com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)" },
|
|
8774
|
+
{ name: "ANDROID_VR", version: "1.62.27", userAgent: "com.google.android.apps.youtube.vr.oculus/1.62.27 (Linux; U; Android 12L) gzip" }
|
|
8775
|
+
];
|
|
8776
|
+
function isBotBlockStatus(status) {
|
|
8777
|
+
return status === "LOGIN_REQUIRED" || status === "AGE_VERIFICATION_REQUIRED";
|
|
8778
|
+
}
|
|
8779
|
+
async function probePlayer(page, videoId) {
|
|
8780
|
+
return page.evaluate(
|
|
8781
|
+
async ({ vid, clients }) => {
|
|
8782
|
+
let lastStatus = null;
|
|
8783
|
+
for (const client2 of clients) {
|
|
8784
|
+
const resp = await fetch("/youtubei/v1/player?prettyPrint=false", {
|
|
8785
|
+
method: "POST",
|
|
8786
|
+
headers: { "Content-Type": "application/json", "User-Agent": client2.userAgent },
|
|
8787
|
+
body: JSON.stringify({
|
|
8788
|
+
context: { client: { clientName: client2.name, clientVersion: client2.version } },
|
|
8789
|
+
videoId: vid
|
|
8790
|
+
})
|
|
8791
|
+
}).catch(() => null);
|
|
8792
|
+
if (!resp?.ok) {
|
|
8793
|
+
lastStatus = resp ? `player_http_${resp.status}` : "player_fetch_failed";
|
|
8794
|
+
continue;
|
|
8795
|
+
}
|
|
8796
|
+
const data = await resp.json();
|
|
8797
|
+
lastStatus = data?.playabilityStatus?.status ?? null;
|
|
8798
|
+
const audioFormats = (data?.streamingData?.adaptiveFormats ?? []).filter((f) => typeof f.url === "string" && f.url && typeof f.mimeType === "string" && f.mimeType.startsWith("audio/")).sort((a, b) => (Number(a.contentLength) || Infinity) - (Number(b.contentLength) || Infinity));
|
|
8799
|
+
const pick = audioFormats[0] ?? null;
|
|
8800
|
+
let captions = null;
|
|
8801
|
+
const tracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
|
|
8802
|
+
if (tracks.length > 0) {
|
|
8803
|
+
const track = tracks.find((t) => t.languageCode === "en") ?? tracks[0];
|
|
8804
|
+
const sep = track.baseUrl.includes("?") ? "&" : "?";
|
|
8805
|
+
const xmlResp = await fetch(`${track.baseUrl}${sep}fmt=json3`).catch(() => null);
|
|
8806
|
+
if (xmlResp?.ok) captions = await xmlResp.text();
|
|
8807
|
+
}
|
|
8808
|
+
if (!captions && !pick) continue;
|
|
8809
|
+
return {
|
|
8810
|
+
captions,
|
|
8811
|
+
status: lastStatus,
|
|
8812
|
+
audio: pick ? {
|
|
8813
|
+
url: pick.url,
|
|
8814
|
+
mimeType: pick.mimeType,
|
|
8815
|
+
bitrate: pick.bitrate ?? null,
|
|
8816
|
+
contentLength: pick.contentLength ? Number(pick.contentLength) || null : null,
|
|
8817
|
+
approxDurationMs: pick.approxDurationMs ? Number(pick.approxDurationMs) || null : null
|
|
8818
|
+
} : null
|
|
8819
|
+
};
|
|
8820
|
+
}
|
|
8821
|
+
return { captions: null, audio: null, status: lastStatus };
|
|
8822
|
+
},
|
|
8823
|
+
{ vid: videoId, clients: INNERTUBE_CLIENTS }
|
|
8824
|
+
).catch(() => null);
|
|
8825
|
+
}
|
|
8826
|
+
var MAX_AUDIO_BYTES = 96 * 1024 * 1024;
|
|
8827
|
+
async function downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, audio) {
|
|
8828
|
+
if (audio.contentLength && audio.contentLength > MAX_AUDIO_BYTES) {
|
|
8829
|
+
console.warn(`[CaptionFetcher] audio stream ${audio.contentLength} bytes exceeds ${MAX_AUDIO_BYTES} byte cap`);
|
|
8830
|
+
return null;
|
|
8831
|
+
}
|
|
8832
|
+
const sep = audio.url.includes("?") ? "&" : "?";
|
|
8833
|
+
const url = audio.contentLength ? `${audio.url}${sep}range=0-${audio.contentLength - 1}` : audio.url;
|
|
8834
|
+
const result = await page.evaluate(async (u) => {
|
|
8835
|
+
try {
|
|
8836
|
+
const resp = await fetch(u);
|
|
8837
|
+
if (!resp.ok) return { ok: false, error: `http_${resp.status}` };
|
|
8838
|
+
const blob = await resp.blob();
|
|
8839
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
8840
|
+
const reader = new FileReader();
|
|
8841
|
+
reader.onload = () => resolve(reader.result);
|
|
8842
|
+
reader.onerror = () => reject(reader.error);
|
|
8843
|
+
reader.readAsDataURL(blob);
|
|
8738
8844
|
});
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
return xmlResp.ok ? xmlResp.text() : null;
|
|
8747
|
-
}, videoId);
|
|
8748
|
-
if (!xml) return null;
|
|
8749
|
-
let entries = parseTimedtextJson3(xml);
|
|
8750
|
-
if (entries.length === 0) entries = parseTimedtextXml(xml);
|
|
8751
|
-
if (entries.length === 0) return null;
|
|
8752
|
-
const chunks = entries.map((e) => ({
|
|
8753
|
-
timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
|
|
8754
|
-
text: e.text
|
|
8755
|
-
}));
|
|
8756
|
-
const text = chunks.map((c) => c.text).join(" ");
|
|
8757
|
-
const last = chunks[chunks.length - 1];
|
|
8758
|
-
return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
|
|
8759
|
-
} catch {
|
|
8845
|
+
return { ok: true, base64: dataUrl.slice(dataUrl.indexOf(",") + 1) };
|
|
8846
|
+
} catch (e) {
|
|
8847
|
+
return { ok: false, error: String(e) };
|
|
8848
|
+
}
|
|
8849
|
+
}, url).catch((e) => ({ ok: false, error: String(e) }));
|
|
8850
|
+
if (!result.ok || !result.base64) {
|
|
8851
|
+
console.warn(`[CaptionFetcher] audio fetch failed (${result.error ?? "unknown"}) for a ${audio.contentLength ?? 0} byte stream`);
|
|
8760
8852
|
return null;
|
|
8761
|
-
} finally {
|
|
8762
|
-
await driver.close();
|
|
8763
8853
|
}
|
|
8854
|
+
const bytes = Buffer.from(result.base64, "base64");
|
|
8855
|
+
if (audio.contentLength && bytes.length < audio.contentLength) {
|
|
8856
|
+
console.warn(`[CaptionFetcher] partial audio: ${bytes.length} of ${audio.contentLength} bytes`);
|
|
8857
|
+
return null;
|
|
8858
|
+
}
|
|
8859
|
+
return { bytes, mimeType: audio.mimeType.split(";")[0].trim() };
|
|
8764
8860
|
}
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
return new Promise((resolve) => {
|
|
8769
|
-
const video = document.querySelector("video");
|
|
8770
|
-
if (!video) {
|
|
8771
|
-
resolve({ ok: false, error: "no video element" });
|
|
8772
|
-
return;
|
|
8773
|
-
}
|
|
8774
|
-
video.muted = false;
|
|
8775
|
-
video.volume = 1;
|
|
8776
|
-
try {
|
|
8777
|
-
const ctx = new AudioContext();
|
|
8778
|
-
const src = ctx.createMediaElementSource(video);
|
|
8779
|
-
const dest = ctx.createMediaStreamDestination();
|
|
8780
|
-
src.connect(dest);
|
|
8781
|
-
src.connect(ctx.destination);
|
|
8782
|
-
const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/ogg";
|
|
8783
|
-
const recorder = new MediaRecorder(dest.stream, { mimeType, audioBitsPerSecond: 48e3 });
|
|
8784
|
-
const chunks = [];
|
|
8785
|
-
recorder.ondataavailable = (e) => {
|
|
8786
|
-
if (e.data.size > 0) chunks.push(e.data);
|
|
8787
|
-
};
|
|
8788
|
-
recorder.onstop = async () => {
|
|
8789
|
-
const blob = new Blob(chunks, { type: mimeType });
|
|
8790
|
-
const ab = await blob.arrayBuffer();
|
|
8791
|
-
const arr = new Uint8Array(ab);
|
|
8792
|
-
let binary = "";
|
|
8793
|
-
const chunkSize = 8192;
|
|
8794
|
-
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
8795
|
-
binary += String.fromCharCode(...arr.slice(i, Math.min(i + chunkSize, arr.length)));
|
|
8796
|
-
}
|
|
8797
|
-
resolve({ ok: true, base64: btoa(binary), size: ab.byteLength, mimeType });
|
|
8798
|
-
};
|
|
8799
|
-
recorder.start(1e3);
|
|
8800
|
-
video.playbackRate = 2;
|
|
8801
|
-
video.play().catch(() => void 0);
|
|
8802
|
-
setTimeout(() => {
|
|
8803
|
-
recorder.stop();
|
|
8804
|
-
ctx.close();
|
|
8805
|
-
}, recordSecs * 1e3);
|
|
8806
|
-
} catch (e) {
|
|
8807
|
-
resolve({ ok: false, error: String(e) });
|
|
8808
|
-
}
|
|
8809
|
-
});
|
|
8810
|
-
}, WHISPER_RECORD_SECONDS);
|
|
8811
|
-
if (!result.ok || !result.base64 || !result.mimeType) return null;
|
|
8812
|
-
return { bytes: Buffer.from(result.base64, "base64"), mimeType: result.mimeType };
|
|
8813
|
-
}
|
|
8814
|
-
async function attemptKernelWhisper(videoId, kernelApiKey, kernelProxyId, falKey, start) {
|
|
8815
|
-
const driver = new BrowserDriver();
|
|
8861
|
+
async function transcribeWithWizper(videoId, audio, startMs) {
|
|
8862
|
+
const falKey = process.env.FAL_KEY;
|
|
8863
|
+
if (!falKey) return null;
|
|
8816
8864
|
try {
|
|
8817
|
-
await driver.launch({ kernelApiKey, kernelProxyId, headless: true, viewport: { width: 1280, height: 800 }, locale: "en-US" });
|
|
8818
|
-
const page = driver.getPage();
|
|
8819
|
-
await driver.navigateTo(`https://www.youtube.com/watch?v=${videoId}`);
|
|
8820
|
-
await page.waitForFunction(
|
|
8821
|
-
() => !!window.ytcfg,
|
|
8822
|
-
{ timeout: 2e4 }
|
|
8823
|
-
);
|
|
8824
|
-
const playStatus = await page.evaluate(() => {
|
|
8825
|
-
const w = window;
|
|
8826
|
-
return w.ytInitialPlayerResponse?.playabilityStatus?.status;
|
|
8827
|
-
});
|
|
8828
|
-
if (playStatus !== "OK") {
|
|
8829
|
-
await driver.close();
|
|
8830
|
-
return null;
|
|
8831
|
-
}
|
|
8832
|
-
const audioInfo = await captureAudioViaMediaRecorder(page);
|
|
8833
|
-
await driver.close();
|
|
8834
|
-
if (!audioInfo || audioInfo.bytes.length < 1e4) return null;
|
|
8835
8865
|
fal.config({ credentials: falKey });
|
|
8836
|
-
const ext =
|
|
8837
|
-
const audioFile = new File([new Uint8Array(
|
|
8866
|
+
const ext = audio.mimeType.includes("webm") ? "webm" : audio.mimeType.includes("mp4") ? "m4a" : "audio";
|
|
8867
|
+
const audioFile = new File([new Uint8Array(audio.bytes)], `audio.${ext}`, { type: audio.mimeType });
|
|
8838
8868
|
const uploadedUrl = await fal.storage.upload(audioFile);
|
|
8869
|
+
const subscribeStartMs = Date.now();
|
|
8839
8870
|
const result = await fal.subscribe("fal-ai/wizper", {
|
|
8840
|
-
input: { audio_url: uploadedUrl, task: "transcribe", language: "en" },
|
|
8871
|
+
input: { audio_url: uploadedUrl, task: "transcribe", language: "en", chunk_level: "segment" },
|
|
8841
8872
|
logs: false,
|
|
8842
8873
|
pollInterval: 3e3
|
|
8843
8874
|
});
|
|
8875
|
+
const subscribeDurationMs = Date.now() - subscribeStartMs;
|
|
8844
8876
|
const data = result.data;
|
|
8845
8877
|
const chunks = (data.chunks ?? []).map((c) => ({
|
|
8846
8878
|
timestamp: c.timestamp,
|
|
8847
8879
|
text: c.text.trim()
|
|
8848
8880
|
}));
|
|
8849
|
-
|
|
8850
|
-
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: audioSec / 60, unitType: "audio_min" });
|
|
8881
|
+
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: subscribeDurationMs / 1e3, unitType: "compute_sec" });
|
|
8851
8882
|
const text = data.text ?? chunks.map((c) => c.text).join(" ");
|
|
8852
8883
|
if (!text) return null;
|
|
8853
|
-
return { videoId, text, chunks, durationMs: Date.now() -
|
|
8854
|
-
} catch {
|
|
8855
|
-
|
|
8884
|
+
return { videoId, text, chunks, durationMs: Date.now() - startMs, method: "browser-whisper" };
|
|
8885
|
+
} catch (err) {
|
|
8886
|
+
console.warn(`[CaptionFetcher] wizper transcription failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8856
8887
|
return null;
|
|
8857
8888
|
}
|
|
8858
8889
|
}
|
|
8859
|
-
|
|
8890
|
+
var YT_RESIDENTIAL_LOCATION = process.env.YOUTUBE_PROXY_LOCATION ?? "Austin, Texas";
|
|
8891
|
+
async function resolveResidentialProxy(kernelApiKey) {
|
|
8892
|
+
try {
|
|
8893
|
+
const resolved = await resolveKernelProxyId({
|
|
8894
|
+
kernelApiKey,
|
|
8895
|
+
proxyMode: "location",
|
|
8896
|
+
location: YT_RESIDENTIAL_LOCATION,
|
|
8897
|
+
gl: "us",
|
|
8898
|
+
fresh: true
|
|
8899
|
+
});
|
|
8900
|
+
return { kernelProxyId: resolved.kernelProxyId, disposableProxyId: resolved.disposableProxyId };
|
|
8901
|
+
} catch (err) {
|
|
8902
|
+
console.warn(`[CaptionFetcher] residential proxy resolution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8903
|
+
return {};
|
|
8904
|
+
}
|
|
8905
|
+
}
|
|
8906
|
+
async function probeAndExtract(videoId, opts) {
|
|
8860
8907
|
const kernelApiKey = browserServiceApiKey();
|
|
8861
|
-
|
|
8862
|
-
const
|
|
8863
|
-
|
|
8908
|
+
if (!kernelApiKey) return null;
|
|
8909
|
+
const kernelProxyId = opts?.kernelProxyId ?? browserServiceProxyId();
|
|
8910
|
+
const driver = new BrowserDriver();
|
|
8911
|
+
return runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: "browser-innertube" }, async () => {
|
|
8912
|
+
let entries = [];
|
|
8913
|
+
let audio = null;
|
|
8914
|
+
let retryWithFreshIp = false;
|
|
8915
|
+
try {
|
|
8916
|
+
await driver.launch({ kernelApiKey, kernelProxyId, headless: true, headlessMode: "headless", viewport: { width: 1280, height: 800 }, locale: "en-US" });
|
|
8917
|
+
const page = driver.getPage();
|
|
8918
|
+
await driver.navigateTo(`https://www.youtube.com/watch?v=${videoId}`);
|
|
8919
|
+
await page.waitForFunction(
|
|
8920
|
+
() => !!window.ytcfg,
|
|
8921
|
+
{ timeout: 2e4 }
|
|
8922
|
+
);
|
|
8923
|
+
const probe = await probePlayer(page, videoId);
|
|
8924
|
+
if (!probe) return null;
|
|
8925
|
+
if (probe.captions && !opts?.skipCaptions) {
|
|
8926
|
+
entries = parseTimedtextJson3(probe.captions);
|
|
8927
|
+
if (entries.length === 0) entries = parseTimedtextXml(probe.captions);
|
|
8928
|
+
}
|
|
8929
|
+
if (entries.length === 0) {
|
|
8930
|
+
if (!probe.audio) {
|
|
8931
|
+
retryWithFreshIp = isBotBlockStatus(probe.status);
|
|
8932
|
+
console.warn(`[CaptionFetcher] no captions and no audio stream for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)})`);
|
|
8933
|
+
return { entries, audio: null, retryWithFreshIp };
|
|
8934
|
+
}
|
|
8935
|
+
audio = await downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, probe.audio);
|
|
8936
|
+
if (!audio) return { entries, audio: null, retryWithFreshIp: true };
|
|
8937
|
+
}
|
|
8938
|
+
} catch (err) {
|
|
8939
|
+
console.warn(`[CaptionFetcher] kernel session failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8940
|
+
return null;
|
|
8941
|
+
} finally {
|
|
8942
|
+
const servedSubOp = audio ? "browser-whisper" : "browser-innertube";
|
|
8943
|
+
await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: servedSubOp }, () => driver.close());
|
|
8944
|
+
}
|
|
8945
|
+
return { entries, audio, retryWithFreshIp };
|
|
8946
|
+
});
|
|
8947
|
+
}
|
|
8948
|
+
async function extractWithProxyEscalation(videoId, opts) {
|
|
8949
|
+
const direct = await probeAndExtract(videoId, opts);
|
|
8950
|
+
if (direct && (direct.entries.length > 0 || direct.audio)) return direct;
|
|
8951
|
+
const shouldEscalate = direct === null || direct.retryWithFreshIp;
|
|
8952
|
+
if (!shouldEscalate) return direct;
|
|
8953
|
+
const kernelApiKey = browserServiceApiKey();
|
|
8954
|
+
if (!kernelApiKey) return direct;
|
|
8955
|
+
const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
|
|
8956
|
+
if (!kernelProxyId) return direct;
|
|
8957
|
+
try {
|
|
8958
|
+
console.warn(`[CaptionFetcher] ${videoId}: retrying through residential proxy after bot block`);
|
|
8959
|
+
const viaProxy = await probeAndExtract(videoId, { ...opts, kernelProxyId });
|
|
8960
|
+
return viaProxy ?? direct;
|
|
8961
|
+
} finally {
|
|
8962
|
+
if (disposableProxyId) {
|
|
8963
|
+
await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
|
|
8964
|
+
(err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
|
|
8965
|
+
);
|
|
8966
|
+
}
|
|
8967
|
+
}
|
|
8968
|
+
}
|
|
8969
|
+
async function fetchViaKernelSession(videoId) {
|
|
8864
8970
|
const start = Date.now();
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8971
|
+
const outcome = await extractWithProxyEscalation(videoId);
|
|
8972
|
+
if (!outcome) return null;
|
|
8973
|
+
if (outcome.entries.length > 0) {
|
|
8974
|
+
const chunks = outcome.entries.map((e) => ({
|
|
8975
|
+
timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
|
|
8976
|
+
text: e.text
|
|
8977
|
+
}));
|
|
8978
|
+
const text = chunks.map((c) => c.text).join(" ");
|
|
8979
|
+
return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
|
|
8980
|
+
}
|
|
8981
|
+
if (outcome.audio) {
|
|
8982
|
+
return runWithCostContext(
|
|
8983
|
+
{ ...currentCostContext(), op: "yt_transcription", subOp: "browser-whisper" },
|
|
8984
|
+
() => transcribeWithWizper(videoId, outcome.audio, start)
|
|
8985
|
+
);
|
|
8869
8986
|
}
|
|
8870
8987
|
return null;
|
|
8871
8988
|
}
|
|
@@ -8887,11 +9004,11 @@ function finalizeCaptions(result) {
|
|
|
8887
9004
|
async function fetchCaptions(videoId) {
|
|
8888
9005
|
const primary = await fetchViaYoutubeTranscript(videoId);
|
|
8889
9006
|
if (primary) return finalizeCaptions(primary);
|
|
8890
|
-
const
|
|
8891
|
-
if (
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
9007
|
+
const result = await fetchViaKernelSession(videoId);
|
|
9008
|
+
if (result) return finalizeCaptions(result);
|
|
9009
|
+
throw new Error(
|
|
9010
|
+
`Could not transcribe ${videoId}. It has no caption track, and its audio stream could not be downloaded \u2014 YouTube permits a single media request per IP, so streams that do not fit in one response are unreachable. Captioned videos of any length still work.`
|
|
9011
|
+
);
|
|
8895
9012
|
}
|
|
8896
9013
|
|
|
8897
9014
|
// src/api/api-auth.ts
|
|
@@ -8935,6 +9052,11 @@ var ExtractUrlBodySchema = z11.object({
|
|
|
8935
9052
|
depositToVault: z11.boolean().optional(),
|
|
8936
9053
|
vaultName: z11.string().trim().min(1).max(120).optional()
|
|
8937
9054
|
});
|
|
9055
|
+
var DiffPageBodySchema = z11.object({
|
|
9056
|
+
url: z11.string().min(1, "url is required"),
|
|
9057
|
+
allowLocal: z11.boolean().optional(),
|
|
9058
|
+
resetBaseline: z11.boolean().optional()
|
|
9059
|
+
});
|
|
8938
9060
|
var MapUrlsBodySchema = z11.object({
|
|
8939
9061
|
url: z11.string().min(1, "url is required"),
|
|
8940
9062
|
maxUrls: z11.number().int().min(1).max(2e3).optional(),
|
|
@@ -9214,7 +9336,7 @@ youtubeApp.post("/transcribe", createApiKeyAuth(), async (c) => {
|
|
|
9214
9336
|
const { ok: trOk, balance_mc: trBal } = await debitMc(user.id, holdMc, LedgerOperation.TRANSCRIPTION_HOLD, id);
|
|
9215
9337
|
if (!trOk) return c.json(insufficientBalanceResponse(trBal, holdMc), 402);
|
|
9216
9338
|
debited = true;
|
|
9217
|
-
const result = await fetchCaptions(id);
|
|
9339
|
+
const result = await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", userId: user.id }, () => fetchCaptions(id));
|
|
9218
9340
|
const lastChunk = result.chunks?.[result.chunks.length - 1];
|
|
9219
9341
|
const chunkSecs = lastChunk && Number.isFinite(lastChunk.timestamp[1]) ? lastChunk.timestamp[1] : 0;
|
|
9220
9342
|
const durationSecs = Number.isFinite(result.durationMs) ? result.durationMs / 1e3 : 0;
|
|
@@ -10277,8 +10399,7 @@ async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript"
|
|
|
10277
10399
|
const text = data.text ?? "";
|
|
10278
10400
|
const chunks = data.chunks ?? [];
|
|
10279
10401
|
const durationMs = Date.now() - startMs;
|
|
10280
|
-
|
|
10281
|
-
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: audioSec / 60, unitType: "audio_min" });
|
|
10402
|
+
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: durationMs / 1e3, unitType: "compute_sec" });
|
|
10282
10403
|
return {
|
|
10283
10404
|
text,
|
|
10284
10405
|
chunks,
|
|
@@ -11753,6 +11874,7 @@ redditApp.post("/thread", createApiKeyAuth(), async (c) => {
|
|
|
11753
11874
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
11754
11875
|
let debited = false;
|
|
11755
11876
|
let refunded = false;
|
|
11877
|
+
let commentDebitMc = 0;
|
|
11756
11878
|
try {
|
|
11757
11879
|
const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD, body.url);
|
|
11758
11880
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.reddit_thread), 402);
|
|
@@ -12070,6 +12192,21 @@ videoApp.post("/status", createApiKeyAuth(), async (c) => {
|
|
|
12070
12192
|
effectiveFrames: st.frameCount ?? null
|
|
12071
12193
|
};
|
|
12072
12194
|
}
|
|
12195
|
+
if ((st.status === "done" || st.status === "failed") && typeof st.costUsd === "number" && st.costUsd > 0) {
|
|
12196
|
+
const llmOp = `${LedgerOperation.VIDEO_ANALYSIS_LLM}:${runId}`;
|
|
12197
|
+
if (!await ledgerExistsForOperation(user.id, llmOp)) {
|
|
12198
|
+
await runWithCostContext({ ...currentCostContext(), op: "video_analysis", userId: user.id }, async () => {
|
|
12199
|
+
void recordVendorUsage({ vendor: "mcp_memory_video", model: null, units: st.costUsd, unitType: "usd", error: null });
|
|
12200
|
+
});
|
|
12201
|
+
const surchargeMc = Math.round(st.costUsd * VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE * MC_PER_USD);
|
|
12202
|
+
if (surchargeMc > 0) {
|
|
12203
|
+
const { ok } = await debitMc(user.id, surchargeMc, llmOp, `real-cost-surcharge:${runId}`);
|
|
12204
|
+
if (!ok) {
|
|
12205
|
+
console.warn(`[video-routes] VIDEO_ANALYSIS_LLM surcharge debit failed for user ${user.id} (run ${runId}): insufficient balance for ${surchargeMc}mc`);
|
|
12206
|
+
}
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12209
|
+
}
|
|
12073
12210
|
return c.json({
|
|
12074
12211
|
ok: true,
|
|
12075
12212
|
runId,
|
|
@@ -13240,7 +13377,7 @@ trustpilotApp.post("/reviews", createApiKeyAuth(), async (c) => {
|
|
|
13240
13377
|
let pagesFetched = 0;
|
|
13241
13378
|
try {
|
|
13242
13379
|
await driver.launch({
|
|
13243
|
-
headless:
|
|
13380
|
+
headless: false,
|
|
13244
13381
|
kernelApiKey: browserServiceApiKey(),
|
|
13245
13382
|
viewport: { width: 1280, height: 900 },
|
|
13246
13383
|
locale: "en-US"
|
|
@@ -13433,7 +13570,7 @@ g2App.post("/reviews", createApiKeyAuth(), async (c) => {
|
|
|
13433
13570
|
const driver = new BrowserDriver();
|
|
13434
13571
|
try {
|
|
13435
13572
|
await driver.launch({
|
|
13436
|
-
headless:
|
|
13573
|
+
headless: false,
|
|
13437
13574
|
kernelApiKey: browserServiceApiKey(),
|
|
13438
13575
|
kernelProxyId: proxyId,
|
|
13439
13576
|
viewport: { width: 1280, height: 900 },
|
|
@@ -19769,6 +19906,71 @@ import { createMiddleware as createMiddleware3 } from "hono/factory";
|
|
|
19769
19906
|
import { getCookie as getCookie2, setCookie as setCookie2, deleteCookie } from "hono/cookie";
|
|
19770
19907
|
import Stripe2 from "stripe";
|
|
19771
19908
|
|
|
19909
|
+
// src/api/page-diff.ts
|
|
19910
|
+
import { createHash as createHash4 } from "crypto";
|
|
19911
|
+
import { diffLines } from "diff";
|
|
19912
|
+
var MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
|
|
19913
|
+
var MAX_DIFF_HUNKS = 200;
|
|
19914
|
+
var MAX_DIFF_LINES_PER_RESPONSE = 2e3;
|
|
19915
|
+
function sha256Hex(value) {
|
|
19916
|
+
return createHash4("sha256").update(value).digest("hex");
|
|
19917
|
+
}
|
|
19918
|
+
function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
|
|
19919
|
+
if (value.length <= maxChars) return { value, truncated: false };
|
|
19920
|
+
return { value: value.slice(0, maxChars), truncated: true };
|
|
19921
|
+
}
|
|
19922
|
+
function changeLines(change) {
|
|
19923
|
+
const lines = change.value.split("\n");
|
|
19924
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
19925
|
+
return lines;
|
|
19926
|
+
}
|
|
19927
|
+
function diffPageContent(oldContent, newContent) {
|
|
19928
|
+
const changes = diffLines(oldContent, newContent);
|
|
19929
|
+
const rawHunks = [];
|
|
19930
|
+
let linesAdded = 0;
|
|
19931
|
+
let linesRemoved = 0;
|
|
19932
|
+
let totalChangedLineCount = 0;
|
|
19933
|
+
for (const change of changes) {
|
|
19934
|
+
if (!change.added && !change.removed) continue;
|
|
19935
|
+
const lines = changeLines(change);
|
|
19936
|
+
totalChangedLineCount += lines.length;
|
|
19937
|
+
if (change.added) linesAdded += lines.length;
|
|
19938
|
+
if (change.removed) linesRemoved += lines.length;
|
|
19939
|
+
rawHunks.push({ type: change.added ? "added" : "removed", lines });
|
|
19940
|
+
}
|
|
19941
|
+
let hunksTruncated = false;
|
|
19942
|
+
const hunksWithinCount = rawHunks.length > MAX_DIFF_HUNKS ? rawHunks.slice(0, MAX_DIFF_HUNKS) : rawHunks;
|
|
19943
|
+
if (rawHunks.length > MAX_DIFF_HUNKS) hunksTruncated = true;
|
|
19944
|
+
const hunks = [];
|
|
19945
|
+
let emittedLines = 0;
|
|
19946
|
+
for (const hunk of hunksWithinCount) {
|
|
19947
|
+
if (emittedLines >= MAX_DIFF_LINES_PER_RESPONSE) {
|
|
19948
|
+
hunksTruncated = true;
|
|
19949
|
+
break;
|
|
19950
|
+
}
|
|
19951
|
+
const remaining = MAX_DIFF_LINES_PER_RESPONSE - emittedLines;
|
|
19952
|
+
if (hunk.lines.length > remaining) {
|
|
19953
|
+
hunks.push({ type: hunk.type, lines: hunk.lines.slice(0, remaining) });
|
|
19954
|
+
emittedLines += remaining;
|
|
19955
|
+
hunksTruncated = true;
|
|
19956
|
+
break;
|
|
19957
|
+
}
|
|
19958
|
+
hunks.push(hunk);
|
|
19959
|
+
emittedLines += hunk.lines.length;
|
|
19960
|
+
}
|
|
19961
|
+
const denominator = Math.max(oldContent.split("\n").length, newContent.split("\n").length, 1);
|
|
19962
|
+
const percentChanged = totalChangedLineCount > 0 ? Math.min(100, Math.round(totalChangedLineCount / denominator * 1e3) / 10) : 0;
|
|
19963
|
+
return {
|
|
19964
|
+
hunks,
|
|
19965
|
+
linesAdded,
|
|
19966
|
+
linesRemoved,
|
|
19967
|
+
percentChanged,
|
|
19968
|
+
hunksTruncated,
|
|
19969
|
+
hunksTruncatedReason: hunksTruncated ? `Showing ${emittedLines} of ${totalChangedLineCount} changed lines (capped at ${MAX_DIFF_HUNKS} hunks / ${MAX_DIFF_LINES_PER_RESPONSE} lines). Use contentHash/previousContentHash to confirm the full scope of change.` : null,
|
|
19970
|
+
totalChangedLineCount
|
|
19971
|
+
};
|
|
19972
|
+
}
|
|
19973
|
+
|
|
19772
19974
|
// src/api/scrape-vault-sink.ts
|
|
19773
19975
|
var MAX_DEPOSIT_CHARS = 5e6;
|
|
19774
19976
|
var SCRAPE_FALLBACK_PREFIX = "scrape-fallback/";
|
|
@@ -20207,9 +20409,12 @@ function opForCostPath(p) {
|
|
|
20207
20409
|
}
|
|
20208
20410
|
if (p.startsWith("/instagram/")) return "instagram";
|
|
20209
20411
|
if (p === "/reddit/thread") return "reddit_thread";
|
|
20412
|
+
if (p.startsWith("/trustpilot/")) return "trustpilot_reviews";
|
|
20413
|
+
if (p.startsWith("/g2/")) return "g2_reviews";
|
|
20210
20414
|
if (p.startsWith("/api/internal/site-architecture-auditor")) return "audit_site";
|
|
20211
20415
|
if (p.startsWith("/api/internal/memory")) return "memory_ai";
|
|
20212
20416
|
if (p.startsWith("/agent/")) return "browser_session";
|
|
20417
|
+
if (p.startsWith("/video/")) return "video_analysis";
|
|
20213
20418
|
return null;
|
|
20214
20419
|
}
|
|
20215
20420
|
app.use("*", (c, next) => {
|
|
@@ -20921,16 +21126,20 @@ app.post("/harvest/sync", auth2, async (c) => {
|
|
|
20921
21126
|
AbortSignal.timeout(syncTimeoutMs)
|
|
20922
21127
|
]);
|
|
20923
21128
|
try {
|
|
20924
|
-
const
|
|
20925
|
-
|
|
20926
|
-
|
|
20927
|
-
|
|
20928
|
-
|
|
20929
|
-
|
|
20930
|
-
|
|
20931
|
-
|
|
20932
|
-
|
|
20933
|
-
|
|
21129
|
+
const harvestCtx = currentCostContext();
|
|
21130
|
+
const result = await runWithCostContext(
|
|
21131
|
+
{ ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id },
|
|
21132
|
+
() => harvest({
|
|
21133
|
+
...options,
|
|
21134
|
+
kernelApiKey: browserServiceApiKey(),
|
|
21135
|
+
headless: true,
|
|
21136
|
+
format: "json",
|
|
21137
|
+
outputDir: "/tmp/paa-output-api",
|
|
21138
|
+
signal: syncSignal,
|
|
21139
|
+
softDeadlineMs: Date.now() + Math.floor(syncTimeoutMs * 0.8),
|
|
21140
|
+
onAttemptEvent: recordAttempt
|
|
21141
|
+
})
|
|
21142
|
+
);
|
|
20934
21143
|
await completeJob(jobId, result);
|
|
20935
21144
|
const attempts = await listHarvestAttempts(jobId, user.id);
|
|
20936
21145
|
if (!options.serpOnly) {
|
|
@@ -21098,6 +21307,91 @@ app.post("/extract-url", auth2, async (c) => {
|
|
|
21098
21307
|
await releaseConcurrencyGate(gate.lockId);
|
|
21099
21308
|
}
|
|
21100
21309
|
});
|
|
21310
|
+
app.post("/diff-page", auth2, async (c) => {
|
|
21311
|
+
const raw = await c.req.json().catch(() => ({}));
|
|
21312
|
+
const bodyResult = DiffPageBodySchema.safeParse(raw);
|
|
21313
|
+
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
21314
|
+
const { url, allowLocal, resetBaseline } = bodyResult.data;
|
|
21315
|
+
if (!allowLocal) {
|
|
21316
|
+
const checked = await validatePublicHttpUrl(url, { field: "URL" });
|
|
21317
|
+
if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
|
|
21318
|
+
} else {
|
|
21319
|
+
try {
|
|
21320
|
+
new URL(url);
|
|
21321
|
+
} catch {
|
|
21322
|
+
return c.json({ error: "Invalid URL" }, 400);
|
|
21323
|
+
}
|
|
21324
|
+
}
|
|
21325
|
+
const canonicalUrl = (() => {
|
|
21326
|
+
try {
|
|
21327
|
+
return new URL(url).href;
|
|
21328
|
+
} catch {
|
|
21329
|
+
return url;
|
|
21330
|
+
}
|
|
21331
|
+
})();
|
|
21332
|
+
const user = c.get("user");
|
|
21333
|
+
const gate = await acquireConcurrencyGate(user, "diff_page", {
|
|
21334
|
+
reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
|
|
21335
|
+
metadata: { url: canonicalUrl }
|
|
21336
|
+
});
|
|
21337
|
+
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
21338
|
+
let debited = false;
|
|
21339
|
+
try {
|
|
21340
|
+
const { ok: dpOk, balance_mc: dpBal } = await debitMc(user.id, MC_COSTS.diff_page, LedgerOperation.DIFF_PAGE, new URL(canonicalUrl).hostname);
|
|
21341
|
+
if (!dpOk) return c.json(insufficientBalanceResponse(dpBal, MC_COSTS.diff_page), 402);
|
|
21342
|
+
debited = true;
|
|
21343
|
+
const existingSnapshot = await getPageSnapshot(user.id, canonicalUrl);
|
|
21344
|
+
const previous = resetBaseline ? null : existingSnapshot;
|
|
21345
|
+
const kernelApiKey = browserServiceApiKey();
|
|
21346
|
+
const result = await extractKpo({ url: canonicalUrl, kernelApiKey });
|
|
21347
|
+
const currentContent = result.bodyMarkdown ?? "";
|
|
21348
|
+
const currentHash = sha256Hex(currentContent);
|
|
21349
|
+
const { value: storedContent, truncated: contentTruncated } = truncateForStorage(currentContent);
|
|
21350
|
+
const currentCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21351
|
+
await upsertPageSnapshot(user.id, canonicalUrl, {
|
|
21352
|
+
contentHash: currentHash,
|
|
21353
|
+
content: storedContent,
|
|
21354
|
+
title: result.title ?? null,
|
|
21355
|
+
contentBytes: Buffer.byteLength(currentContent, "utf8"),
|
|
21356
|
+
truncated: contentTruncated
|
|
21357
|
+
});
|
|
21358
|
+
let status;
|
|
21359
|
+
let diff = { hunks: [], linesAdded: 0, linesRemoved: 0, percentChanged: 0, hunksTruncated: false, hunksTruncatedReason: null, totalChangedLineCount: 0 };
|
|
21360
|
+
if (!previous) {
|
|
21361
|
+
status = "baseline";
|
|
21362
|
+
diff.percentChanged = null;
|
|
21363
|
+
} else if (previous.contentHash === currentHash) {
|
|
21364
|
+
status = "unchanged";
|
|
21365
|
+
} else {
|
|
21366
|
+
status = "changed";
|
|
21367
|
+
diff = diffPageContent(previous.content, storedContent);
|
|
21368
|
+
}
|
|
21369
|
+
await logRequestEvent({ userId: user.id, source: "diff_page", status: "done", query: canonicalUrl, result: { status } });
|
|
21370
|
+
return c.json({
|
|
21371
|
+
url: canonicalUrl,
|
|
21372
|
+
title: result.title ?? null,
|
|
21373
|
+
status,
|
|
21374
|
+
isReset: !!resetBaseline && !!existingSnapshot,
|
|
21375
|
+
previousCheckedAt: previous?.checkedAt ?? null,
|
|
21376
|
+
currentCheckedAt,
|
|
21377
|
+
contentHash: currentHash,
|
|
21378
|
+
previousContentHash: previous?.contentHash ?? null,
|
|
21379
|
+
summary: { linesAdded: diff.linesAdded, linesRemoved: diff.linesRemoved, percentChanged: diff.percentChanged },
|
|
21380
|
+
hunks: diff.hunks,
|
|
21381
|
+
contentTruncated,
|
|
21382
|
+
hunksTruncated: diff.hunksTruncated,
|
|
21383
|
+
hunksTruncatedReason: diff.hunksTruncatedReason,
|
|
21384
|
+
totalChangedLineCount: diff.totalChangedLineCount
|
|
21385
|
+
});
|
|
21386
|
+
} catch (err) {
|
|
21387
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
21388
|
+
if (debited) await creditMc(user.id, MC_COSTS.diff_page, LedgerOperation.DIFF_PAGE, "failed call");
|
|
21389
|
+
await logRequestEvent({ userId: user.id, source: "diff_page", status: "failed", query: canonicalUrl, error: msg });
|
|
21390
|
+
return c.json({ error: msg }, 500);
|
|
21391
|
+
} finally {
|
|
21392
|
+
await releaseConcurrencyGate(gate.lockId);
|
|
21393
|
+
}
|
|
21394
|
+
});
|
|
21101
21395
|
app.post("/map-urls", auth2, async (c) => {
|
|
21102
21396
|
const raw = await c.req.json().catch(() => ({}));
|
|
21103
21397
|
const bodyResult = MapUrlsBodySchema.safeParse(raw);
|
|
@@ -21524,7 +21818,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
21524
21818
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
21525
21819
|
return c.json({ error: "Unauthorized" }, 401);
|
|
21526
21820
|
}
|
|
21527
|
-
const { drainQueue } = await import("./worker-
|
|
21821
|
+
const { drainQueue } = await import("./worker-HTYZAYGB.js");
|
|
21528
21822
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
21529
21823
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
21530
21824
|
const [results, sweepResult, reapResult, expiredResult, blobCleanup] = await Promise.all([
|
|
@@ -21542,15 +21836,15 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
21542
21836
|
return c.json({ error: "Unauthorized" }, 401);
|
|
21543
21837
|
}
|
|
21544
21838
|
const jobId = c.req.param("id");
|
|
21545
|
-
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-
|
|
21546
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
21839
|
+
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-HTIY52MW.js");
|
|
21840
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-COS56ZDO.js");
|
|
21547
21841
|
const job = await getExtractJob2(jobId);
|
|
21548
21842
|
if (!job) return c.json({ error: "job not found" }, 404);
|
|
21549
21843
|
const stored = await assembleExtractArtifacts(job, {});
|
|
21550
21844
|
await completeExtractJob2(jobId, stored);
|
|
21551
21845
|
let settlement = "already_settled_or_refunded";
|
|
21552
21846
|
if (job.billedMc == null && job.userId != null) {
|
|
21553
|
-
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-
|
|
21847
|
+
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-HTIY52MW.js");
|
|
21554
21848
|
const heldMc = Number(job.options.heldMc ?? 0);
|
|
21555
21849
|
const successful = await countSuccessfulPages2(jobId);
|
|
21556
21850
|
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
@@ -21714,4 +22008,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
21714
22008
|
export {
|
|
21715
22009
|
app
|
|
21716
22010
|
};
|
|
21717
|
-
//# sourceMappingURL=server-
|
|
22011
|
+
//# sourceMappingURL=server-UNID3SJU.js.map
|