@librechat/agents 3.2.63 → 3.2.65
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/cjs/graphs/Graph.cjs +3 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +73 -9
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +41 -9
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +3 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/stream_events.cjs +337 -0
- package/dist/cjs/llm/anthropic/utils/stream_events.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +82 -34
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-scraper.cjs +165 -0
- package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/crw-search.cjs +105 -0
- package/dist/cjs/tools/search/crw-search.cjs.map +1 -0
- package/dist/cjs/tools/search/search.cjs +4 -2
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +15 -3
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +3 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +73 -9
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +41 -9
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +3 -2
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/stream_events.mjs +337 -0
- package/dist/esm/llm/anthropic/utils/stream_events.mjs.map +1 -0
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +82 -34
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/tools/search/crw-scraper.mjs +163 -0
- package/dist/esm/tools/search/crw-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/crw-search.mjs +103 -0
- package/dist/esm/tools/search/crw-search.mjs.map +1 -0
- package/dist/esm/tools/search/search.mjs +4 -2
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +15 -3
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/types/llm/anthropic/index.d.ts +2 -0
- package/dist/types/llm/anthropic/types.d.ts +2 -0
- package/dist/types/llm/anthropic/utils/message_outputs.d.ts +1 -2
- package/dist/types/llm/anthropic/utils/stream_events.d.ts +25 -0
- package/dist/types/tools/search/crw-scraper.d.ts +41 -0
- package/dist/types/tools/search/crw-search.d.ts +4 -0
- package/dist/types/tools/search/types.d.ts +88 -3
- package/package.json +1 -1
- package/src/graphs/Graph.ts +15 -0
- package/src/llm/anthropic/index.ts +134 -10
- package/src/llm/anthropic/inherited-content-utils.spec.ts +10 -5
- package/src/llm/anthropic/inherited-stream-events.spec.ts +513 -9
- package/src/llm/anthropic/llm.spec.ts +100 -16
- package/src/llm/anthropic/types.ts +3 -0
- package/src/llm/anthropic/utils/message_inputs.ts +60 -3
- package/src/llm/anthropic/utils/message_outputs.ts +10 -2
- package/src/llm/anthropic/utils/stream_events.ts +471 -0
- package/src/llm/bedrock/utils/message_inputs.test.ts +276 -1
- package/src/llm/bedrock/utils/message_inputs.ts +121 -73
- package/src/tools/search/crw-scraper.ts +244 -0
- package/src/tools/search/crw-search.ts +167 -0
- package/src/tools/search/crw.test.ts +836 -0
- package/src/tools/search/search.ts +7 -1
- package/src/tools/search/tool.ts +23 -3
- package/src/tools/search/types.ts +103 -3
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import type * as t from './types';
|
|
3
|
+
import { createDefaultLogger } from './utils';
|
|
4
|
+
import { processContent } from './content';
|
|
5
|
+
|
|
6
|
+
/** HTTP headroom over the payload render budget: fastCRW's queue/verification
|
|
7
|
+
* overhead is not counted against `timeout`, so the client must wait longer. */
|
|
8
|
+
const CRW_TIMEOUT_BUFFER = 5000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* fastCRW scraper. Firecrawl-compatible web scraper; single binary;
|
|
12
|
+
* self-host or cloud. Posts to {base}/v1/scrape.
|
|
13
|
+
*/
|
|
14
|
+
export class CrwScraper implements t.BaseScraper {
|
|
15
|
+
private apiKey: string;
|
|
16
|
+
private apiUrl: string;
|
|
17
|
+
private defaultFormats: string[];
|
|
18
|
+
private timeout: number;
|
|
19
|
+
private logger: t.Logger;
|
|
20
|
+
private onlyMainContent?: boolean;
|
|
21
|
+
private includeTags?: string[];
|
|
22
|
+
private excludeTags?: string[];
|
|
23
|
+
private waitFor?: number;
|
|
24
|
+
private headers?: Record<string, string>;
|
|
25
|
+
private renderJs?: boolean | null;
|
|
26
|
+
private cssSelector?: string;
|
|
27
|
+
private xpath?: string;
|
|
28
|
+
private proxy?: string;
|
|
29
|
+
private stealth?: boolean;
|
|
30
|
+
|
|
31
|
+
constructor(config: t.CrwScraperConfig = {}) {
|
|
32
|
+
this.apiKey = config.apiKey ?? process.env.CRW_API_KEY ?? '';
|
|
33
|
+
|
|
34
|
+
const baseUrl =
|
|
35
|
+
config.apiUrl ?? process.env.CRW_API_URL ?? 'https://api.fastcrw.com';
|
|
36
|
+
this.apiUrl = `${baseUrl.replace(/\/+$/, '')}/v1/scrape`;
|
|
37
|
+
|
|
38
|
+
this.defaultFormats = config.formats ?? ['markdown', 'html'];
|
|
39
|
+
this.timeout = config.timeout ?? 7500;
|
|
40
|
+
this.logger = config.logger || createDefaultLogger();
|
|
41
|
+
|
|
42
|
+
this.onlyMainContent = config.onlyMainContent;
|
|
43
|
+
this.includeTags = config.includeTags;
|
|
44
|
+
this.excludeTags = config.excludeTags;
|
|
45
|
+
this.waitFor = config.waitFor;
|
|
46
|
+
this.headers = config.headers;
|
|
47
|
+
this.renderJs = config.renderJs;
|
|
48
|
+
this.cssSelector = config.cssSelector;
|
|
49
|
+
this.xpath = config.xpath;
|
|
50
|
+
this.proxy = config.proxy;
|
|
51
|
+
this.stealth = config.stealth;
|
|
52
|
+
|
|
53
|
+
// Self-host fastCRW may run without auth, so a missing key is only a
|
|
54
|
+
// warning — unlike Firecrawl/Tavily, scrapeUrl does NOT early-return on it.
|
|
55
|
+
if (!this.apiKey) {
|
|
56
|
+
this.logger.warn('CRW_API_KEY is not set. Scraping will not work.');
|
|
57
|
+
}
|
|
58
|
+
this.logger.debug(`CRW scraper initialized with API URL: ${this.apiUrl}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async scrapeUrl(
|
|
62
|
+
url: string,
|
|
63
|
+
options: t.CrwScrapeOptions = {}
|
|
64
|
+
): Promise<[string, t.CrwScrapeResponse]> {
|
|
65
|
+
try {
|
|
66
|
+
const payloadTimeout = options.timeout ?? this.timeout;
|
|
67
|
+
const payload = omitUndefined({
|
|
68
|
+
url,
|
|
69
|
+
formats: options.formats ?? this.defaultFormats,
|
|
70
|
+
onlyMainContent: options.onlyMainContent ?? this.onlyMainContent,
|
|
71
|
+
includeTags: options.includeTags ?? this.includeTags,
|
|
72
|
+
excludeTags: options.excludeTags ?? this.excludeTags,
|
|
73
|
+
waitFor: options.waitFor ?? this.waitFor,
|
|
74
|
+
headers: options.headers ?? this.headers,
|
|
75
|
+
renderJs: options.renderJs ?? this.renderJs,
|
|
76
|
+
cssSelector: options.cssSelector ?? this.cssSelector,
|
|
77
|
+
xpath: options.xpath ?? this.xpath,
|
|
78
|
+
proxy: options.proxy ?? this.proxy,
|
|
79
|
+
stealth: options.stealth ?? this.stealth,
|
|
80
|
+
// Cloud honors `timeout` (live-verified); the published OpenAPI
|
|
81
|
+
// documents `deadlineMs` (1..60000) instead. Send both.
|
|
82
|
+
timeout: payloadTimeout,
|
|
83
|
+
deadlineMs: Math.max(1, Math.min(payloadTimeout, 60000)),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const headers: Record<string, string> = {
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
};
|
|
89
|
+
if (this.apiKey) {
|
|
90
|
+
headers.Authorization = `Bearer ${this.apiKey}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const response = await axios.post<t.CrwRawScrapeResponse>(
|
|
94
|
+
this.apiUrl,
|
|
95
|
+
payload,
|
|
96
|
+
{
|
|
97
|
+
headers,
|
|
98
|
+
timeout: payloadTimeout + CRW_TIMEOUT_BUFFER,
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return [url, normalizeCrwResponse(response.data)];
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const errorMessage =
|
|
105
|
+
error instanceof Error ? error.message : String(error);
|
|
106
|
+
return [
|
|
107
|
+
url,
|
|
108
|
+
{
|
|
109
|
+
success: false,
|
|
110
|
+
error: `fastCRW API request failed: ${errorMessage}`,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extract content from scrape response. Mirrors FirecrawlScraper — reads
|
|
118
|
+
* response.data.*, which normalizeCrwResponse guarantees, preserving the
|
|
119
|
+
* processContent ref-markers used by the reranker. Parameter is typed as the
|
|
120
|
+
* NARROWER t.CrwScrapeResponse, exactly like TavilyScraper/FirecrawlScraper:
|
|
121
|
+
* TS class-method bivariance accepts this against BaseScraper's
|
|
122
|
+
* AnyScraperResponse, AND it lets us read response.data.plainText (a
|
|
123
|
+
* CrwScrapeResponse-only field) with no cast.
|
|
124
|
+
*/
|
|
125
|
+
extractContent(
|
|
126
|
+
response: t.CrwScrapeResponse
|
|
127
|
+
): [string, undefined | t.References] {
|
|
128
|
+
if (!response.success || !response.data) {
|
|
129
|
+
return ['', undefined];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const htmlSource = response.data.html ?? response.data.rawHtml;
|
|
133
|
+
if (response.data.markdown != null && htmlSource != null) {
|
|
134
|
+
try {
|
|
135
|
+
const { markdown, ...rest } = processContent(
|
|
136
|
+
htmlSource,
|
|
137
|
+
response.data.markdown
|
|
138
|
+
);
|
|
139
|
+
return [markdown, rest];
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this.logger.error('Error processing content:', error);
|
|
142
|
+
return [response.data.markdown, undefined];
|
|
143
|
+
}
|
|
144
|
+
} else if (response.data.markdown != null) {
|
|
145
|
+
return [response.data.markdown, undefined];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fall back to HTML content
|
|
149
|
+
if (response.data.html != null) {
|
|
150
|
+
return [response.data.html, undefined];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Fall back to raw HTML content
|
|
154
|
+
if (response.data.rawHtml != null) {
|
|
155
|
+
return [response.data.rawHtml, undefined];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// CRW-only fallback (no Firecrawl equivalent): plain-text body.
|
|
159
|
+
if (response.data.plainText != null) {
|
|
160
|
+
return [response.data.plainText, undefined];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return ['', undefined];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
extractMetadata(response: t.CrwScrapeResponse): t.ScrapeMetadata {
|
|
167
|
+
if (!response.success || !response.data || !response.data.metadata) {
|
|
168
|
+
return {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return response.data.metadata;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Create a fastCRW scraper instance
|
|
177
|
+
* @param config Scraper configuration
|
|
178
|
+
* @returns fastCRW scraper instance
|
|
179
|
+
*/
|
|
180
|
+
export const createCrwScraper = (config: t.CrwScraperConfig = {}): CrwScraper =>
|
|
181
|
+
new CrwScraper(config);
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* fastCRW cloud nests scrape fields under `data` ({success, data: {markdown,
|
|
185
|
+
* ...}}, live-verified 2026-07-02), matching Firecrawl. Prefer the nested
|
|
186
|
+
* container and fall back to top-level fields for self-host/legacy responses.
|
|
187
|
+
*/
|
|
188
|
+
function normalizeCrwResponse(
|
|
189
|
+
raw: t.CrwRawScrapeResponse | null | undefined
|
|
190
|
+
): t.CrwScrapeResponse {
|
|
191
|
+
if (raw == null) {
|
|
192
|
+
return { success: false, error: 'Empty fastCRW response' };
|
|
193
|
+
}
|
|
194
|
+
if (raw.success === false) {
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
error:
|
|
198
|
+
raw.error_code != null
|
|
199
|
+
? `[${raw.error_code}] ${raw.error ?? 'Unknown error'}`
|
|
200
|
+
: (raw.error ?? 'fastCRW scrape failed'),
|
|
201
|
+
error_code: raw.error_code,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const data = raw.data ?? raw;
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
data: {
|
|
208
|
+
// Strip inline base64 image payloads from text content (not `screenshot`,
|
|
209
|
+
// which is base64 by design and never reaches the content processor).
|
|
210
|
+
markdown: stripBase64DataUris(data.markdown),
|
|
211
|
+
html: stripBase64DataUris(data.html),
|
|
212
|
+
rawHtml: stripBase64DataUris(data.rawHtml),
|
|
213
|
+
plainText: stripBase64DataUris(data.plainText),
|
|
214
|
+
screenshot: data.screenshot,
|
|
215
|
+
links: data.links,
|
|
216
|
+
metadata: data.metadata,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Replace inline base64 data-URI payloads (typically images) in scraped text
|
|
223
|
+
* with a short placeholder. Such payloads can be hundreds of KB; the content
|
|
224
|
+
* processor builds a per-link RegExp from each URL, and one that large overflows
|
|
225
|
+
* the engine's pattern-size limit ("Invalid regular expression"). Firecrawl
|
|
226
|
+
* drops them via removeBase64Images — mirror that so image-heavy pages stay
|
|
227
|
+
* processable (and don't bloat the LLM context with base64 noise).
|
|
228
|
+
*/
|
|
229
|
+
function stripBase64DataUris(text: string | undefined): string | undefined {
|
|
230
|
+
if (text == null) {
|
|
231
|
+
return text;
|
|
232
|
+
}
|
|
233
|
+
return text.replace(
|
|
234
|
+
/data:[\w.+-]+\/[\w.+-]+;base64,[A-Za-z0-9+/=]+/g,
|
|
235
|
+
'data:base64-content-removed'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Helper function to clean up payload for fastCRW
|
|
240
|
+
function omitUndefined<T extends object>(obj: T): Partial<T> {
|
|
241
|
+
return Object.fromEntries(
|
|
242
|
+
Object.entries(obj).filter(([, v]) => v !== undefined)
|
|
243
|
+
) as Partial<T>;
|
|
244
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import type * as t from './types';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CRW_TIMEOUT = 15000;
|
|
5
|
+
|
|
6
|
+
const getHostname = (link: string): string => {
|
|
7
|
+
try {
|
|
8
|
+
return new URL(link).hostname;
|
|
9
|
+
} catch {
|
|
10
|
+
return link;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** The published OpenAPI (docs.fastcrw.com) documents `data` as a flat
|
|
15
|
+
* SearchResult[] with a `category` field; route rows into source groups. */
|
|
16
|
+
const splitByCategory = (rows: t.CrwSearchResult[]): t.CrwSearchGroups => {
|
|
17
|
+
const groups: Required<t.CrwSearchGroups> = { web: [], images: [], news: [] };
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
if (row.category === 'news') {
|
|
20
|
+
groups.news.push(row);
|
|
21
|
+
} else if (row.category === 'images') {
|
|
22
|
+
groups.images.push({ ...row, imageUrl: row.url });
|
|
23
|
+
} else {
|
|
24
|
+
groups.web.push(row);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return groups;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const createCrwAPI = (
|
|
31
|
+
apiKey?: string,
|
|
32
|
+
apiUrl?: string,
|
|
33
|
+
options?: t.CrwSearchOptions
|
|
34
|
+
): {
|
|
35
|
+
getSources: (params: t.GetSourcesParams) => Promise<t.SearchResult>;
|
|
36
|
+
} => {
|
|
37
|
+
// NOTE: fastCRW /v1/search is cloud-only, but we still allow a base-URL
|
|
38
|
+
// override for parity with the scraper. Self-host may have no auth, so —
|
|
39
|
+
// unlike Tavily (createTavilyAPI throws on missing key) — we do NOT throw
|
|
40
|
+
// here; we only attach the Authorization header when a key is present.
|
|
41
|
+
const base = (
|
|
42
|
+
apiUrl ??
|
|
43
|
+
process.env.CRW_API_URL ??
|
|
44
|
+
'https://api.fastcrw.com'
|
|
45
|
+
).replace(/\/+$/, '');
|
|
46
|
+
const config = {
|
|
47
|
+
apiKey: apiKey ?? process.env.CRW_API_KEY,
|
|
48
|
+
apiUrl: `${base}/v1/search`,
|
|
49
|
+
timeout: options?.timeout ?? DEFAULT_CRW_TIMEOUT,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getSources = async ({
|
|
53
|
+
query,
|
|
54
|
+
date,
|
|
55
|
+
numResults = 8,
|
|
56
|
+
type,
|
|
57
|
+
}: t.GetSourcesParams): Promise<t.SearchResult> => {
|
|
58
|
+
if (!query.trim()) {
|
|
59
|
+
return { success: false, error: 'Query cannot be empty' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const limit = Math.min(
|
|
64
|
+
Math.max(1, options?.maxResults ?? numResults),
|
|
65
|
+
20
|
|
66
|
+
);
|
|
67
|
+
// Mirror Serper's verticals: image/news requests hit only their native
|
|
68
|
+
// fastCRW source; plain web optionally adds images via includeImages.
|
|
69
|
+
let sources: t.CrwSearchSource[];
|
|
70
|
+
if (type === 'images') {
|
|
71
|
+
sources = ['images'];
|
|
72
|
+
} else if (type === 'news') {
|
|
73
|
+
sources = ['news'];
|
|
74
|
+
} else {
|
|
75
|
+
sources = options?.includeImages === true ? ['web', 'images'] : ['web'];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const payload: t.CrwSearchPayload = { query, limit, sources };
|
|
79
|
+
if (date != null) {
|
|
80
|
+
// Serper-style qdr filter; live-verified to constrain results.
|
|
81
|
+
payload.tbs = `qdr:${date}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const headers: Record<string, string> = {
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
};
|
|
87
|
+
if (config.apiKey != null && config.apiKey !== '') {
|
|
88
|
+
headers.Authorization = `Bearer ${config.apiKey}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const response = await axios.post<t.CrwSearchResponse>(
|
|
92
|
+
config.apiUrl,
|
|
93
|
+
payload,
|
|
94
|
+
{ headers, timeout: config.timeout }
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const body = response.data;
|
|
98
|
+
if (body.success === false) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
error: `fastCRW search failed: ${
|
|
102
|
+
body.error_code != null ? `[${body.error_code}] ` : ''
|
|
103
|
+
}${body.error ?? 'Unknown error'}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// fastCRW cloud keys results by source: {data: {web: [...], images:
|
|
108
|
+
// [...], news: [...]}} (live-verified 2026-07-02); the published
|
|
109
|
+
// OpenAPI documents a flat SearchResult[] instead (self-host), and a
|
|
110
|
+
// {data: {results: {...groups}}} wrapper also exists. Accept all three.
|
|
111
|
+
// OrganicResult.link is a REQUIRED string (types.ts), so defend against
|
|
112
|
+
// null/empty urls to never feed a broken '' link into the scraper.
|
|
113
|
+
const container = body.data ?? {};
|
|
114
|
+
const data: t.CrwSearchGroups = Array.isArray(container)
|
|
115
|
+
? splitByCategory(container)
|
|
116
|
+
: (container.results ?? container);
|
|
117
|
+
const organicResults: t.OrganicResult[] = (data.web ?? [])
|
|
118
|
+
.filter((r) => r.url != null && r.url !== '')
|
|
119
|
+
.map((r) => ({
|
|
120
|
+
title: r.title ?? '',
|
|
121
|
+
link: r.url as string,
|
|
122
|
+
snippet: r.description ?? r.snippet ?? '',
|
|
123
|
+
position: r.position,
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
const imageResults: t.ImageResult[] = (data.images ?? [])
|
|
127
|
+
.filter((r) => r.imageUrl != null && r.imageUrl !== '')
|
|
128
|
+
.map((r) => ({
|
|
129
|
+
title: r.title,
|
|
130
|
+
imageUrl: r.imageUrl,
|
|
131
|
+
link: r.url,
|
|
132
|
+
position: r.position,
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
const newsResults: t.NewsResult[] = (data.news ?? [])
|
|
136
|
+
.filter((r) => r.url != null && r.url !== '')
|
|
137
|
+
.map((r) => ({
|
|
138
|
+
title: r.title ?? '',
|
|
139
|
+
link: r.url as string,
|
|
140
|
+
snippet: r.description ?? r.snippet ?? '',
|
|
141
|
+
date: r.publishedDate,
|
|
142
|
+
source: getHostname(r.url as string),
|
|
143
|
+
position: r.position,
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
const results: t.SearchResultData = {
|
|
147
|
+
organic: organicResults,
|
|
148
|
+
images: imageResults,
|
|
149
|
+
topStories: [],
|
|
150
|
+
videos: [],
|
|
151
|
+
news: newsResults,
|
|
152
|
+
relatedSearches: [],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return { success: true, data: results };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const errorMessage =
|
|
158
|
+
error instanceof Error ? error.message : String(error);
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
error: `fastCRW search request failed: ${errorMessage}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return { getSources };
|
|
167
|
+
};
|