@webcrawl/mcp 1.4.1 → 1.4.3
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.js +5 -7
- package/dist/core/engine/awswaf.d.ts +59 -0
- package/dist/core/engine/awswaf.js +164 -0
- package/dist/core/engine/cache.d.ts +70 -0
- package/dist/core/engine/cache.js +297 -0
- package/dist/core/engine/chrome.d.ts +14 -0
- package/dist/core/engine/chrome.js +140 -0
- package/dist/core/engine/crawl-policy.d.ts +36 -0
- package/dist/core/engine/crawl-policy.js +134 -0
- package/dist/core/engine/crawler.d.ts +152 -0
- package/dist/core/engine/crawler.js +918 -0
- package/dist/core/engine/fetcher.d.ts +92 -0
- package/dist/core/engine/fetcher.js +494 -0
- package/dist/core/engine/index.d.ts +7 -0
- package/dist/core/engine/index.js +7 -0
- package/dist/core/engine/markdown.d.ts +33 -0
- package/dist/core/engine/markdown.js +334 -0
- package/dist/core/engine/path-scope.d.ts +21 -0
- package/dist/core/engine/path-scope.js +88 -0
- package/dist/core/engine/render.d.ts +41 -0
- package/dist/core/engine/render.js +138 -0
- package/dist/core/engine/safe-url.d.ts +29 -0
- package/dist/core/engine/safe-url.js +109 -0
- package/dist/core/engine/session.d.ts +93 -0
- package/dist/core/engine/session.js +250 -0
- package/dist/core/engine/stealth.d.ts +83 -0
- package/dist/core/engine/stealth.js +222 -0
- package/dist/core/engine.d.ts +1 -0
- package/dist/core/engine.js +1 -0
- package/dist/core/extract/business.d.ts +28 -0
- package/dist/core/extract/business.js +344 -0
- package/dist/core/extract/currency.d.ts +15 -0
- package/dist/core/extract/currency.js +259 -0
- package/dist/core/extract/cve-cache.d.ts +62 -0
- package/dist/core/extract/cve-cache.js +277 -0
- package/dist/core/extract/cve.d.ts +57 -0
- package/dist/core/extract/cve.js +397 -0
- package/dist/core/extract/dedupe-key.d.ts +12 -0
- package/dist/core/extract/dedupe-key.js +36 -0
- package/dist/core/extract/flights.d.ts +37 -0
- package/dist/core/extract/flights.js +373 -0
- package/dist/core/extract/hotels.d.ts +35 -0
- package/dist/core/extract/hotels.js +473 -0
- package/dist/core/extract/index.d.ts +9 -0
- package/dist/core/extract/index.js +9 -0
- package/dist/core/extract/real-estate.d.ts +53 -0
- package/dist/core/extract/real-estate.js +835 -0
- package/dist/core/extract/scaledown.d.ts +14 -0
- package/dist/core/extract/scaledown.js +484 -0
- package/dist/core/extract/shopping.d.ts +44 -0
- package/dist/core/extract/shopping.js +393 -0
- package/dist/core/extract/structured.d.ts +14 -0
- package/dist/core/extract/structured.js +144 -0
- package/dist/core/image-url.d.ts +5 -0
- package/dist/core/image-url.js +13 -0
- package/dist/core/interact/driver.d.ts +72 -0
- package/dist/core/interact/driver.js +384 -0
- package/dist/core/interact/remote.d.ts +41 -0
- package/dist/core/interact/remote.js +133 -0
- package/dist/core/interact/spans.d.ts +46 -0
- package/dist/core/interact/spans.js +32 -0
- package/dist/core/interact/validate.d.ts +34 -0
- package/dist/core/interact/validate.js +135 -0
- package/dist/core/interact/vm.d.ts +43 -0
- package/dist/core/interact/vm.js +338 -0
- package/dist/core/local-engine.d.ts +56 -0
- package/dist/core/local-engine.js +667 -0
- package/dist/core/parse/detect.d.ts +29 -0
- package/dist/core/parse/detect.js +152 -0
- package/dist/core/parse/fetch.d.ts +20 -0
- package/dist/core/parse/fetch.js +39 -0
- package/dist/core/parse/image.d.ts +13 -0
- package/dist/core/parse/image.js +100 -0
- package/dist/core/parse/index.d.ts +53 -0
- package/dist/core/parse/index.js +153 -0
- package/dist/core/parse/limits.d.ts +12 -0
- package/dist/core/parse/limits.js +29 -0
- package/dist/core/parse/media.d.ts +42 -0
- package/dist/core/parse/media.js +238 -0
- package/dist/core/parse/merge.d.ts +19 -0
- package/dist/core/parse/merge.js +48 -0
- package/dist/core/parse/ocr.d.ts +25 -0
- package/dist/core/parse/ocr.js +121 -0
- package/dist/core/parse/office.d.ts +28 -0
- package/dist/core/parse/office.js +83 -0
- package/dist/core/parse/pdf.d.ts +27 -0
- package/dist/core/parse/pdf.js +156 -0
- package/dist/core/parse/types.d.ts +38 -0
- package/dist/core/parse/types.js +1 -0
- package/dist/core/parse/youtube.d.ts +30 -0
- package/dist/core/parse/youtube.js +264 -0
- package/dist/core/ranker/decision.d.ts +28 -0
- package/dist/core/ranker/decision.js +151 -0
- package/dist/core/ranker/index.d.ts +5 -0
- package/dist/core/ranker/index.js +361 -0
- package/dist/core/ranker/lexical.d.ts +82 -0
- package/dist/core/ranker/lexical.js +643 -0
- package/dist/core/ranker/types.d.ts +90 -0
- package/dist/core/ranker/types.js +1 -0
- package/dist/core/ranker.d.ts +1 -0
- package/dist/core/ranker.js +1 -0
- package/dist/core/sandbox.d.ts +29 -0
- package/dist/core/sandbox.js +222 -0
- package/dist/core/search/catalog.d.ts +176 -0
- package/dist/core/search/catalog.js +488 -0
- package/dist/core/search/index.d.ts +3 -0
- package/dist/core/search/index.js +3 -0
- package/dist/core/search/pipeline.d.ts +7 -0
- package/dist/core/search/pipeline.js +719 -0
- package/dist/core/search/providers.d.ts +72 -0
- package/dist/core/search/providers.js +1270 -0
- package/dist/core/search.d.ts +1 -0
- package/dist/core/search.js +1 -0
- package/dist/core/targets/developer.d.ts +44 -0
- package/dist/core/targets/developer.js +338 -0
- package/dist/core/targets/index.d.ts +3 -0
- package/dist/core/targets/index.js +3 -0
- package/dist/core/targets/registry.d.ts +32 -0
- package/dist/core/targets/registry.js +411 -0
- package/dist/core/targets/types.d.ts +72 -0
- package/dist/core/targets/types.js +8 -0
- package/dist/core/targets.d.ts +1 -0
- package/dist/core/targets.js +1 -0
- package/dist/format.js +8 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/load-local-engine.d.ts +7 -0
- package/dist/load-local-engine.js +22 -0
- package/dist/server.js +7 -0
- package/package.json +11 -3
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BusinessListing } from "./business.js";
|
|
2
|
+
import { type FlightOffer } from "./flights.js";
|
|
3
|
+
import { type HotelListing } from "./hotels.js";
|
|
4
|
+
import { type PropertyListing } from "./real-estate.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tier-2 ultra-cheap RLCD-class structured extraction using `scaledown/scaledown-extract-1`
|
|
7
|
+
* via Vercel AI Gateway ($0.05 / 1M input tokens, $0.00 / 1M output tokens).
|
|
8
|
+
* Extracts structured `PropertyListing[]` from clean page markdown or SERP snippets when
|
|
9
|
+
* deterministic JSON-state / JSON-LD extraction yields insufficient results.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractPropertyListingsWithScaleDown(text: string, pageUrl: string, countryCode?: string, timeoutMs?: number): Promise<PropertyListing[]>;
|
|
12
|
+
export declare function extractFlightOffersWithScaleDown(text: string, pageUrl: string, countryCode?: string, timeoutMs?: number): Promise<FlightOffer[]>;
|
|
13
|
+
export declare function extractHotelListingsWithScaleDown(text: string, pageUrl: string, countryCode?: string, timeoutMs?: number): Promise<HotelListing[]>;
|
|
14
|
+
export declare function extractBusinessListingsWithScaleDown(text: string, pageUrl: string, countryCode?: string, timeoutMs?: number): Promise<BusinessListing[]>;
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { extractDomainHost } from "./currency.js";
|
|
2
|
+
import { parseFlightPrice } from "./flights.js";
|
|
3
|
+
import { parseHotelPrice } from "./hotels.js";
|
|
4
|
+
import { inferListingType, inferPricePeriod, parsePropertyPrice, resolveAbsoluteUrl, } from "./real-estate.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tier-2 ultra-cheap RLCD-class structured extraction using `scaledown/scaledown-extract-1`
|
|
7
|
+
* via Vercel AI Gateway ($0.05 / 1M input tokens, $0.00 / 1M output tokens).
|
|
8
|
+
* Extracts structured `PropertyListing[]` from clean page markdown or SERP snippets when
|
|
9
|
+
* deterministic JSON-state / JSON-LD extraction yields insufficient results.
|
|
10
|
+
*/
|
|
11
|
+
export async function extractPropertyListingsWithScaleDown(text, pageUrl, countryCode, timeoutMs = 3000) {
|
|
12
|
+
const apiKey = process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN || "";
|
|
13
|
+
const trimmed = text.trim().slice(0, 6_000);
|
|
14
|
+
if (!apiKey || trimmed.length < 40) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
19
|
+
const portal = extractDomainHost(pageUrl) || "unknown";
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch("https://ai-gateway.vercel.sh/v1/chat/completions", {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
Authorization: `Bearer ${apiKey}`,
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
model: "scaledown/scaledown-extract-1",
|
|
29
|
+
messages: [
|
|
30
|
+
{
|
|
31
|
+
role: "system",
|
|
32
|
+
content: "Extract distinct residential or commercial property listings with street or suburb addresses, prices, bedrooms, bathrooms, parking, and property types.",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
role: "user",
|
|
36
|
+
content: trimmed,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
response_format: {
|
|
40
|
+
type: "json_schema",
|
|
41
|
+
json_schema: {
|
|
42
|
+
name: "property_listings",
|
|
43
|
+
schema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
listings: {
|
|
47
|
+
type: "array",
|
|
48
|
+
items: {
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {
|
|
51
|
+
address: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "street address, suburb, city, state, and postcode of the property",
|
|
54
|
+
},
|
|
55
|
+
price: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "property price, price guide, or weekly/monthly rent amount",
|
|
58
|
+
},
|
|
59
|
+
bedrooms: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "number of bedrooms",
|
|
62
|
+
},
|
|
63
|
+
bathrooms: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "number of bathrooms",
|
|
66
|
+
},
|
|
67
|
+
parking: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "number of car spaces or garages",
|
|
70
|
+
},
|
|
71
|
+
property_type: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "property type such as House, Apartment, Townhouse, Unit, Villa, or Land",
|
|
74
|
+
},
|
|
75
|
+
listing_url: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "URL or path to the property listing if present",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
});
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const payload = (await res.json());
|
|
93
|
+
const rawContent = payload.choices?.[0]?.message?.content;
|
|
94
|
+
if (!rawContent) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const parsed = JSON.parse(rawContent);
|
|
98
|
+
const items = parsed.structured_result?.listings ??
|
|
99
|
+
(Array.isArray(parsed.listings) ? parsed.listings : []);
|
|
100
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
const address = typeof item.address === "string" ? item.address.trim() : "";
|
|
106
|
+
if (!address || address.length < 5) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const rawPrice = typeof item.price === "string" ? item.price.trim() : undefined;
|
|
110
|
+
const parsedPrice = parsePropertyPrice(rawPrice, countryCode);
|
|
111
|
+
const rawUrl = typeof item.listing_url === "string" && item.listing_url.trim()
|
|
112
|
+
? resolveAbsoluteUrl(item.listing_url.trim(), pageUrl)
|
|
113
|
+
: pageUrl;
|
|
114
|
+
const listingType = inferListingType(rawUrl, undefined, parsedPrice.priceText);
|
|
115
|
+
const bedsMatch = typeof item.bedrooms === "string" || typeof item.bedrooms === "number"
|
|
116
|
+
? String(item.bedrooms).match(/(\d+)/)
|
|
117
|
+
: null;
|
|
118
|
+
const bathsMatch = typeof item.bathrooms === "string" || typeof item.bathrooms === "number"
|
|
119
|
+
? String(item.bathrooms).match(/(\d+)/)
|
|
120
|
+
: null;
|
|
121
|
+
const carsMatch = typeof item.parking === "string" || typeof item.parking === "number"
|
|
122
|
+
? String(item.parking).match(/(\d+)/)
|
|
123
|
+
: null;
|
|
124
|
+
const propType = typeof item.property_type === "string" && item.property_type.trim()
|
|
125
|
+
? item.property_type.trim()
|
|
126
|
+
: undefined;
|
|
127
|
+
out.push({
|
|
128
|
+
title: `${bedsMatch ? `${bedsMatch[1]} bed ` : ""}${propType ? `${propType} ` : ""}at ${address}`
|
|
129
|
+
.replace(/\s+/g, " ")
|
|
130
|
+
.trim(),
|
|
131
|
+
address,
|
|
132
|
+
country: countryCode,
|
|
133
|
+
price: parsedPrice.price,
|
|
134
|
+
priceText: parsedPrice.priceText,
|
|
135
|
+
currency: parsedPrice.currency,
|
|
136
|
+
pricePeriod: inferPricePeriod(parsedPrice.priceText, listingType),
|
|
137
|
+
listingType,
|
|
138
|
+
propertyType: propType,
|
|
139
|
+
bedrooms: bedsMatch ? Number.parseInt(bedsMatch[1], 10) : undefined,
|
|
140
|
+
bathrooms: bathsMatch ? Number.parseInt(bathsMatch[1], 10) : undefined,
|
|
141
|
+
parking: carsMatch ? Number.parseInt(carsMatch[1], 10) : undefined,
|
|
142
|
+
url: rawUrl,
|
|
143
|
+
portal: extractDomainHost(rawUrl) || portal,
|
|
144
|
+
extractedVia: "scaledown-extract-1",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export async function extractFlightOffersWithScaleDown(text, pageUrl, countryCode, timeoutMs = 3000) {
|
|
157
|
+
const apiKey = process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN || "";
|
|
158
|
+
const trimmed = text.trim().slice(0, 6_000);
|
|
159
|
+
if (!apiKey || trimmed.length < 40) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
164
|
+
const provider = extractDomainHost(pageUrl) || "unknown";
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch("https://ai-gateway.vercel.sh/v1/chat/completions", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: {
|
|
169
|
+
"Content-Type": "application/json",
|
|
170
|
+
Authorization: `Bearer ${apiKey}`,
|
|
171
|
+
},
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
model: "scaledown/scaledown-extract-1",
|
|
174
|
+
messages: [
|
|
175
|
+
{
|
|
176
|
+
role: "system",
|
|
177
|
+
content: "Extract distinct flight offers with airline, origin, destination, duration, stops, cabin class, and fare price.",
|
|
178
|
+
},
|
|
179
|
+
{ role: "user", content: trimmed },
|
|
180
|
+
],
|
|
181
|
+
response_format: {
|
|
182
|
+
type: "json_schema",
|
|
183
|
+
json_schema: {
|
|
184
|
+
name: "flight_offers",
|
|
185
|
+
schema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
flights: {
|
|
189
|
+
type: "array",
|
|
190
|
+
items: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: {
|
|
193
|
+
airline: { type: "string" },
|
|
194
|
+
flight_number: { type: "string" },
|
|
195
|
+
origin: { type: "string" },
|
|
196
|
+
destination: { type: "string" },
|
|
197
|
+
duration: { type: "string" },
|
|
198
|
+
stops: { type: "string" },
|
|
199
|
+
price: { type: "string" },
|
|
200
|
+
url: { type: "string" },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
signal: controller.signal,
|
|
210
|
+
});
|
|
211
|
+
if (!res.ok) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
const payload = (await res.json());
|
|
215
|
+
const rawContent = payload.choices?.[0]?.message?.content;
|
|
216
|
+
if (!rawContent) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
const parsed = JSON.parse(rawContent);
|
|
220
|
+
const items = parsed.structured_result?.flights ??
|
|
221
|
+
(Array.isArray(parsed.flights) ? parsed.flights : []);
|
|
222
|
+
const out = [];
|
|
223
|
+
for (const item of items) {
|
|
224
|
+
const airline = typeof item.airline === "string" ? item.airline.trim() : undefined;
|
|
225
|
+
const origin = typeof item.origin === "string" ? item.origin.trim() : undefined;
|
|
226
|
+
const destination = typeof item.destination === "string"
|
|
227
|
+
? item.destination.trim()
|
|
228
|
+
: undefined;
|
|
229
|
+
const rawPrice = typeof item.price === "string" ? item.price.trim() : undefined;
|
|
230
|
+
if (!airline && !origin && !rawPrice) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const parsedPrice = parseFlightPrice(rawPrice, countryCode);
|
|
234
|
+
const rawUrl = typeof item.url === "string" && item.url.trim()
|
|
235
|
+
? resolveAbsoluteUrl(item.url.trim(), pageUrl)
|
|
236
|
+
: pageUrl;
|
|
237
|
+
out.push({
|
|
238
|
+
title: `${airline ? `${airline} · ` : ""}${origin || "Flight"} → ${destination || ""}`.trim(),
|
|
239
|
+
airline,
|
|
240
|
+
flightNumber: typeof item.flight_number === "string"
|
|
241
|
+
? item.flight_number.trim()
|
|
242
|
+
: undefined,
|
|
243
|
+
origin,
|
|
244
|
+
destination,
|
|
245
|
+
duration: typeof item.duration === "string" ? item.duration.trim() : undefined,
|
|
246
|
+
stops: typeof item.stops === "string" ? item.stops.trim() : undefined,
|
|
247
|
+
price: parsedPrice.price,
|
|
248
|
+
priceText: parsedPrice.priceText,
|
|
249
|
+
currency: parsedPrice.currency,
|
|
250
|
+
url: rawUrl,
|
|
251
|
+
provider: extractDomainHost(rawUrl) || provider,
|
|
252
|
+
extractedVia: "scaledown-extract-1",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export async function extractHotelListingsWithScaleDown(text, pageUrl, countryCode, timeoutMs = 3000) {
|
|
265
|
+
const apiKey = process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN || "";
|
|
266
|
+
const trimmed = text.trim().slice(0, 6_000);
|
|
267
|
+
if (!apiKey || trimmed.length < 40) {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
const controller = new AbortController();
|
|
271
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
272
|
+
const provider = extractDomainHost(pageUrl) || "unknown";
|
|
273
|
+
try {
|
|
274
|
+
const res = await fetch("https://ai-gateway.vercel.sh/v1/chat/completions", {
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: {
|
|
277
|
+
"Content-Type": "application/json",
|
|
278
|
+
Authorization: `Bearer ${apiKey}`,
|
|
279
|
+
},
|
|
280
|
+
body: JSON.stringify({
|
|
281
|
+
model: "scaledown/scaledown-extract-1",
|
|
282
|
+
messages: [
|
|
283
|
+
{
|
|
284
|
+
role: "system",
|
|
285
|
+
content: "Extract distinct hotel or resort listings with hotel name, address/city, nightly price, star rating, guest rating, and amenities.",
|
|
286
|
+
},
|
|
287
|
+
{ role: "user", content: trimmed },
|
|
288
|
+
],
|
|
289
|
+
response_format: {
|
|
290
|
+
type: "json_schema",
|
|
291
|
+
json_schema: {
|
|
292
|
+
name: "hotel_listings",
|
|
293
|
+
schema: {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {
|
|
296
|
+
hotels: {
|
|
297
|
+
type: "array",
|
|
298
|
+
items: {
|
|
299
|
+
type: "object",
|
|
300
|
+
properties: {
|
|
301
|
+
name: {
|
|
302
|
+
type: "string",
|
|
303
|
+
description: "specific hotel or resort name",
|
|
304
|
+
},
|
|
305
|
+
address: {
|
|
306
|
+
type: "string",
|
|
307
|
+
description: "street address, neighborhood, or city",
|
|
308
|
+
},
|
|
309
|
+
price_per_night: {
|
|
310
|
+
type: "string",
|
|
311
|
+
description: "nightly room rate or starting price",
|
|
312
|
+
},
|
|
313
|
+
star_rating: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "hotel star classification from 1 to 5",
|
|
316
|
+
},
|
|
317
|
+
guest_rating: {
|
|
318
|
+
type: "string",
|
|
319
|
+
description: "guest review score such as 9.2 or 4.7",
|
|
320
|
+
},
|
|
321
|
+
url: {
|
|
322
|
+
type: "string",
|
|
323
|
+
description: "link to the hotel page",
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
}),
|
|
333
|
+
signal: controller.signal,
|
|
334
|
+
});
|
|
335
|
+
if (!res.ok) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
const payload = (await res.json());
|
|
339
|
+
const rawContent = payload.choices?.[0]?.message?.content;
|
|
340
|
+
if (!rawContent) {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
const parsed = JSON.parse(rawContent);
|
|
344
|
+
const items = parsed.structured_result?.hotels ??
|
|
345
|
+
(Array.isArray(parsed.hotels) ? parsed.hotels : []);
|
|
346
|
+
const out = [];
|
|
347
|
+
for (const item of items) {
|
|
348
|
+
const name = typeof item.name === "string" ? item.name.trim() : "";
|
|
349
|
+
if (!name || name.length < 3) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const parsedPrice = parseHotelPrice(typeof item.price_per_night === "string"
|
|
353
|
+
? item.price_per_night
|
|
354
|
+
: undefined, countryCode);
|
|
355
|
+
const rawUrl = typeof item.url === "string" && item.url.trim()
|
|
356
|
+
? resolveAbsoluteUrl(item.url.trim(), pageUrl)
|
|
357
|
+
: pageUrl;
|
|
358
|
+
const starNum = typeof item.star_rating === "string"
|
|
359
|
+
? Number.parseFloat(item.star_rating)
|
|
360
|
+
: undefined;
|
|
361
|
+
const guestNum = typeof item.guest_rating === "string"
|
|
362
|
+
? Number.parseFloat(item.guest_rating)
|
|
363
|
+
: undefined;
|
|
364
|
+
out.push({
|
|
365
|
+
name,
|
|
366
|
+
address: typeof item.address === "string" ? item.address.trim() : undefined,
|
|
367
|
+
country: countryCode,
|
|
368
|
+
pricePerNight: parsedPrice.pricePerNight,
|
|
369
|
+
priceText: parsedPrice.priceText,
|
|
370
|
+
currency: parsedPrice.currency,
|
|
371
|
+
starRating: starNum && starNum >= 1 && starNum <= 5 ? starNum : undefined,
|
|
372
|
+
guestRating: guestNum && guestNum > 0 && guestNum <= 10 ? guestNum : undefined,
|
|
373
|
+
url: rawUrl,
|
|
374
|
+
provider: extractDomainHost(rawUrl) || provider,
|
|
375
|
+
extractedVia: "scaledown-extract-1",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
export async function extractBusinessListingsWithScaleDown(text, pageUrl, countryCode, timeoutMs = 3000) {
|
|
388
|
+
const apiKey = process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN || "";
|
|
389
|
+
const trimmed = text.trim().slice(0, 6_000);
|
|
390
|
+
if (!apiKey || trimmed.length < 40) {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
const controller = new AbortController();
|
|
394
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
395
|
+
const sourceDomain = extractDomainHost(pageUrl) || "unknown";
|
|
396
|
+
try {
|
|
397
|
+
const res = await fetch("https://ai-gateway.vercel.sh/v1/chat/completions", {
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers: {
|
|
400
|
+
"Content-Type": "application/json",
|
|
401
|
+
Authorization: `Bearer ${apiKey}`,
|
|
402
|
+
},
|
|
403
|
+
body: JSON.stringify({
|
|
404
|
+
model: "scaledown/scaledown-extract-1",
|
|
405
|
+
messages: [
|
|
406
|
+
{
|
|
407
|
+
role: "system",
|
|
408
|
+
content: "Extract distinct businesses or companies with business name, category, address, phone, rating, and website URL.",
|
|
409
|
+
},
|
|
410
|
+
{ role: "user", content: trimmed },
|
|
411
|
+
],
|
|
412
|
+
response_format: {
|
|
413
|
+
type: "json_schema",
|
|
414
|
+
json_schema: {
|
|
415
|
+
name: "business_listings",
|
|
416
|
+
schema: {
|
|
417
|
+
type: "object",
|
|
418
|
+
properties: {
|
|
419
|
+
businesses: {
|
|
420
|
+
type: "array",
|
|
421
|
+
items: {
|
|
422
|
+
type: "object",
|
|
423
|
+
properties: {
|
|
424
|
+
name: { type: "string" },
|
|
425
|
+
category: { type: "string" },
|
|
426
|
+
address: { type: "string" },
|
|
427
|
+
phone: { type: "string" },
|
|
428
|
+
rating: { type: "string" },
|
|
429
|
+
url: { type: "string" },
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
}),
|
|
438
|
+
signal: controller.signal,
|
|
439
|
+
});
|
|
440
|
+
if (!res.ok) {
|
|
441
|
+
return [];
|
|
442
|
+
}
|
|
443
|
+
const payload = (await res.json());
|
|
444
|
+
const rawContent = payload.choices?.[0]?.message?.content;
|
|
445
|
+
if (!rawContent) {
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
const parsed = JSON.parse(rawContent);
|
|
449
|
+
const items = parsed.structured_result?.businesses ??
|
|
450
|
+
(Array.isArray(parsed.businesses) ? parsed.businesses : []);
|
|
451
|
+
const out = [];
|
|
452
|
+
for (const item of items) {
|
|
453
|
+
const name = typeof item.name === "string" ? item.name.trim() : "";
|
|
454
|
+
if (!name || name.length < 2) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const rawUrl = typeof item.url === "string" && item.url.trim()
|
|
458
|
+
? resolveAbsoluteUrl(item.url.trim(), pageUrl)
|
|
459
|
+
: pageUrl;
|
|
460
|
+
const ratingNum = typeof item.rating === "string"
|
|
461
|
+
? Number.parseFloat(item.rating)
|
|
462
|
+
: undefined;
|
|
463
|
+
out.push({
|
|
464
|
+
name,
|
|
465
|
+
category: typeof item.category === "string" ? item.category.trim() : undefined,
|
|
466
|
+
address: typeof item.address === "string" ? item.address.trim() : undefined,
|
|
467
|
+
country: countryCode,
|
|
468
|
+
phone: typeof item.phone === "string" ? item.phone.trim() : undefined,
|
|
469
|
+
website: rawUrl,
|
|
470
|
+
rating: ratingNum && ratingNum > 0 && ratingNum <= 10 ? ratingNum : undefined,
|
|
471
|
+
url: rawUrl,
|
|
472
|
+
sourceDomain: extractDomainHost(rawUrl) || sourceDomain,
|
|
473
|
+
extractedVia: "scaledown-extract-1",
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
clearTimeout(timer);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface ProductOffer {
|
|
2
|
+
title: string;
|
|
3
|
+
vendor: string;
|
|
4
|
+
domain: string;
|
|
5
|
+
url: string;
|
|
6
|
+
price: number;
|
|
7
|
+
rawPrice: string;
|
|
8
|
+
currency: string;
|
|
9
|
+
availability?: "in_stock" | "out_of_stock" | "preorder" | "unknown";
|
|
10
|
+
rating?: number;
|
|
11
|
+
reviewCount?: number;
|
|
12
|
+
freeShipping?: boolean;
|
|
13
|
+
shippingNote?: string;
|
|
14
|
+
imageUrl?: string;
|
|
15
|
+
source: "bing-shopping" | "google-shopping" | "json-ld" | "opengraph" | "serp-snippet";
|
|
16
|
+
}
|
|
17
|
+
export interface AggregatedProduct {
|
|
18
|
+
name: string;
|
|
19
|
+
bestOffer: ProductOffer;
|
|
20
|
+
minPrice: number;
|
|
21
|
+
maxPrice: number;
|
|
22
|
+
currency: string;
|
|
23
|
+
offerCount: number;
|
|
24
|
+
vendors: string[];
|
|
25
|
+
offers: ProductOffer[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Extract structured ProductOffer items from Bing Shopping HTML (`CustomData` JSON + `<li class="br-item">` seller/rating DOM).
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseBingShoppingHtml(html: string, countryCode?: string): ProductOffer[];
|
|
31
|
+
/**
|
|
32
|
+
* Extract structured ProductOffer items from arbitrary retailer or price-aggregator HTML
|
|
33
|
+
* using Schema.org JSON-LD (`Product`, `Offer`, `AggregateOffer`), OpenGraph `product:price:*`, and microdata.
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractOffersFromHtml(html: string, pageUrl: string, fallbackTitle?: string, countryCode?: string): ProductOffer[];
|
|
36
|
+
/**
|
|
37
|
+
* Extract fallback price offer from a SERP result title/description when a snippet explicitly states a price
|
|
38
|
+
* (e.g. "Buy Sony WH-1000XM5 from £173.49", "Sony WH-1000XM5 - $399.00 at JB Hi-Fi").
|
|
39
|
+
*/
|
|
40
|
+
export declare function extractOfferFromSerpSnippet(title: string, description: string, url: string, countryCode?: string): ProductOffer | null;
|
|
41
|
+
/**
|
|
42
|
+
* Group extracted ProductOffer items into normalized AggregatedProduct clusters and compute `bestOffer` (lowest in-stock price).
|
|
43
|
+
*/
|
|
44
|
+
export declare function groupOffersIntoProducts(offers: ProductOffer[], query: string, maxProducts?: number): AggregatedProduct[];
|