@bacnh85/pi-web 0.4.4 → 0.4.5
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/extensions/lib/brave.ts +23 -28
- package/extensions/lib/config.ts +17 -21
- package/extensions/lib/firecrawl.ts +2 -10
- package/extensions/lib/retry.ts +13 -0
- package/extensions/lib/searxng.ts +37 -43
- package/package.json +1 -1
package/extensions/lib/brave.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Brave Search API client.
|
|
2
2
|
|
|
3
3
|
import { sanitizeSnippet, type SearchResultItem } from "./format";
|
|
4
|
+
import { withRetry } from "./retry";
|
|
4
5
|
|
|
5
6
|
export interface BraveResult extends SearchResultItem {
|
|
6
7
|
age: string;
|
|
@@ -14,32 +15,26 @@ export async function fetchBraveResults(
|
|
|
14
15
|
apiKey: string,
|
|
15
16
|
signal?: AbortSignal,
|
|
16
17
|
): Promise<BraveResult[]> {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} catch (e) {
|
|
40
|
-
if (attempt === 2) throw e;
|
|
41
|
-
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
throw new Error("unreachable");
|
|
18
|
+
return withRetry(async () => {
|
|
19
|
+
const params = new URLSearchParams({ q: query, count: String(count), country });
|
|
20
|
+
if (freshness) params.append("freshness", freshness);
|
|
21
|
+
const response = await fetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
|
|
22
|
+
headers: {
|
|
23
|
+
Accept: "application/json",
|
|
24
|
+
"Accept-Encoding": "gzip",
|
|
25
|
+
"X-Subscription-Token": apiKey,
|
|
26
|
+
},
|
|
27
|
+
signal,
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}\n${await response.text()}`);
|
|
30
|
+
const data: any = await response.json();
|
|
31
|
+
return (data.web?.results || [])
|
|
32
|
+
.slice(0, count)
|
|
33
|
+
.map((r: any) => ({
|
|
34
|
+
title: sanitizeSnippet(r.title || ""),
|
|
35
|
+
url: r.url || "",
|
|
36
|
+
snippet: sanitizeSnippet(r.description || ""),
|
|
37
|
+
age: sanitizeSnippet(r.age || r.page_age || ""),
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
45
40
|
}
|
package/extensions/lib/config.ts
CHANGED
|
@@ -87,6 +87,20 @@ export function includeProjectEnv(ctx: Record<string, unknown>): boolean {
|
|
|
87
87
|
return typeof ctx?.isProjectTrusted === "function" ? (ctx.isProjectTrusted as () => boolean)() : false;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// URL helpers
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
function normalizeHttpUrl(raw: string | undefined, fallback: string): string {
|
|
95
|
+
let value = (raw || fallback).trim();
|
|
96
|
+
if (!/^https?:\/\//i.test(value)) {
|
|
97
|
+
value = /^(localhost|127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/i.test(value)
|
|
98
|
+
? `http://${value}`
|
|
99
|
+
: `https://${value}`;
|
|
100
|
+
}
|
|
101
|
+
return value.replace(/\/+$/, "");
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
// ---------------------------------------------------------------------------
|
|
91
105
|
// SearXNG config
|
|
92
106
|
// ---------------------------------------------------------------------------
|
|
@@ -97,13 +111,7 @@ export interface SearxngConfig {
|
|
|
97
111
|
}
|
|
98
112
|
|
|
99
113
|
export function normalizeSearxngBaseUrl(raw?: string): string {
|
|
100
|
-
|
|
101
|
-
if (!/^https?:\/\//i.test(value)) {
|
|
102
|
-
value = /^(localhost|127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/i.test(value)
|
|
103
|
-
? `http://${value}`
|
|
104
|
-
: `https://${value}`;
|
|
105
|
-
}
|
|
106
|
-
return value.replace(/\/+$/, "");
|
|
114
|
+
return normalizeHttpUrl(raw, DEFAULT_SEARXNG_BASE_URL);
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
export function loadSearxngConfig(params: Record<string, unknown> = {}, cwd = process.cwd(), includeCwdEnv = false): SearxngConfig {
|
|
@@ -125,13 +133,7 @@ export interface Crawl4aiConfig {
|
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
export function normalizeCrawl4aiApiUrl(raw?: string): string {
|
|
128
|
-
|
|
129
|
-
if (!/^https?:\/\//i.test(value)) {
|
|
130
|
-
value = /^(localhost|127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/i.test(value)
|
|
131
|
-
? `http://${value}`
|
|
132
|
-
: `https://${value}`;
|
|
133
|
-
}
|
|
134
|
-
return value.replace(/\/+$/, "");
|
|
136
|
+
return normalizeHttpUrl(raw, DEFAULT_CRAWL4AI_API_URL);
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
export function loadCrawl4aiConfig(params: Record<string, unknown> = {}, cwd = process.cwd(), includeCwdEnv = false): Crawl4aiConfig {
|
|
@@ -159,13 +161,7 @@ export interface FirecrawlConfig {
|
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
export function normalizeFirecrawlBaseUrl(raw?: string): string {
|
|
162
|
-
let value = (raw
|
|
163
|
-
if (!/^https?:\/\//i.test(value)) {
|
|
164
|
-
value = /^(localhost|127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/i.test(value)
|
|
165
|
-
? `http://${value}`
|
|
166
|
-
: `https://${value}`;
|
|
167
|
-
}
|
|
168
|
-
value = value.replace(/\/+$/, "");
|
|
164
|
+
let value = normalizeHttpUrl(raw, HOSTED_FIRECRAWL_BASE_URL);
|
|
169
165
|
if (!/\/v\d+$/i.test(value)) value += "/v2";
|
|
170
166
|
return value;
|
|
171
167
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Firecrawl API client (search, scrape, map, crawl).
|
|
2
2
|
|
|
3
3
|
import type { FirecrawlConfig } from "./config";
|
|
4
|
-
import { signalWithTimeout } from "./retry";
|
|
4
|
+
import { signalWithTimeout, withRetry } from "./retry";
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// Types
|
|
@@ -62,13 +62,5 @@ export async function firecrawlRequest(
|
|
|
62
62
|
body?: unknown,
|
|
63
63
|
signal?: AbortSignal,
|
|
64
64
|
): Promise<Record<string, unknown>> {
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
return await firecrawlRequestJson(config, method, endpoint, body, signal);
|
|
68
|
-
} catch (e) {
|
|
69
|
-
if (attempt === 2) throw e;
|
|
70
|
-
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
throw new Error("unreachable");
|
|
65
|
+
return withRetry(() => firecrawlRequestJson(config, method, endpoint, body, signal));
|
|
74
66
|
}
|
package/extensions/lib/retry.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
// Retry utility: signal/timeout helpers.
|
|
2
2
|
|
|
3
|
+
export async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
|
|
4
|
+
let last: unknown;
|
|
5
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
6
|
+
try { return await fn(); }
|
|
7
|
+
catch (error) {
|
|
8
|
+
last = error;
|
|
9
|
+
if (attempt === attempts - 1) break;
|
|
10
|
+
await abortableSleep(1000 * 2 ** attempt);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
throw last ?? new Error("retry failed");
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
/**
|
|
4
17
|
* Combine a timeout signal with an optional external signal.
|
|
5
18
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SearXNG metasearch client.
|
|
2
2
|
|
|
3
3
|
import { sanitizeSnippet } from "./format";
|
|
4
|
-
import { signalWithTimeout } from "./retry";
|
|
4
|
+
import { signalWithTimeout, withRetry } from "./retry";
|
|
5
5
|
|
|
6
6
|
export interface SearxngResultItem {
|
|
7
7
|
title: string;
|
|
@@ -23,47 +23,41 @@ export async function fetchSearxngResults(
|
|
|
23
23
|
baseUrl: string,
|
|
24
24
|
signal?: AbortSignal,
|
|
25
25
|
): Promise<SearxngResponse> {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (val !== undefined && val !== "") url.searchParams.set(key, String(val));
|
|
36
|
-
}
|
|
37
|
-
const timeoutMs = (params.timeout_ms as number) || 15000;
|
|
38
|
-
const response = await fetch(url, {
|
|
39
|
-
headers: { Accept: "application/json", "User-Agent": "pi-web/0.1 SearXNG" },
|
|
40
|
-
signal: signalWithTimeout(timeoutMs, signal),
|
|
41
|
-
});
|
|
42
|
-
const text = await response.text();
|
|
43
|
-
if (!response.ok) {
|
|
44
|
-
const hint =
|
|
45
|
-
response.status === 403
|
|
46
|
-
? "\nHint: enable JSON output in SearXNG settings.yml with search.formats including json."
|
|
47
|
-
: "";
|
|
48
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}${hint}${text ? `\n${text}` : ""}`);
|
|
49
|
-
}
|
|
50
|
-
const data: any = text ? JSON.parse(text) : {};
|
|
51
|
-
const results = (data.results || [])
|
|
52
|
-
.slice(0, limit)
|
|
53
|
-
.map((r: any) => ({
|
|
54
|
-
title: sanitizeSnippet(r.title || ""),
|
|
55
|
-
url: r.url || "",
|
|
56
|
-
snippet: sanitizeSnippet(r.content || r.snippet || ""),
|
|
57
|
-
engine: r.engine || "",
|
|
58
|
-
category: r.category || "",
|
|
59
|
-
score: r.score,
|
|
60
|
-
publishedDate: r.publishedDate || r.published_date || "",
|
|
61
|
-
}));
|
|
62
|
-
return { ...data, results };
|
|
63
|
-
} catch (e) {
|
|
64
|
-
if (attempt === 2) throw e;
|
|
65
|
-
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
26
|
+
return withRetry(async () => {
|
|
27
|
+
const limit = Math.min(50, Math.max(1, (params.count as number) ?? 5));
|
|
28
|
+
const url = new URL(`${baseUrl}/search`);
|
|
29
|
+
url.searchParams.set("q", params.query as string);
|
|
30
|
+
url.searchParams.set("format", "json");
|
|
31
|
+
url.searchParams.set("pageno", String(Math.max(1, (params.pageno as number) ?? 1)));
|
|
32
|
+
for (const key of ["categories", "engines", "language", "time_range", "safesearch"] as const) {
|
|
33
|
+
const val = params[key];
|
|
34
|
+
if (val !== undefined && val !== "") url.searchParams.set(key, String(val));
|
|
66
35
|
}
|
|
67
|
-
|
|
68
|
-
|
|
36
|
+
const timeoutMs = (params.timeout_ms as number) || 15000;
|
|
37
|
+
const response = await fetch(url, {
|
|
38
|
+
headers: { Accept: "application/json", "User-Agent": "pi-web/0.1 SearXNG" },
|
|
39
|
+
signal: signalWithTimeout(timeoutMs, signal),
|
|
40
|
+
});
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const hint =
|
|
44
|
+
response.status === 403
|
|
45
|
+
? "\nHint: enable JSON output in SearXNG settings.yml with search.formats including json."
|
|
46
|
+
: "";
|
|
47
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}${hint}${text ? `\n${text}` : ""}`);
|
|
48
|
+
}
|
|
49
|
+
const data: any = text ? JSON.parse(text) : {};
|
|
50
|
+
const results = (data.results || [])
|
|
51
|
+
.slice(0, limit)
|
|
52
|
+
.map((r: any) => ({
|
|
53
|
+
title: sanitizeSnippet(r.title || ""),
|
|
54
|
+
url: r.url || "",
|
|
55
|
+
snippet: sanitizeSnippet(r.content || r.snippet || ""),
|
|
56
|
+
engine: r.engine || "",
|
|
57
|
+
category: r.category || "",
|
|
58
|
+
score: r.score,
|
|
59
|
+
publishedDate: r.publishedDate || r.published_date || "",
|
|
60
|
+
}));
|
|
61
|
+
return { ...data, results };
|
|
62
|
+
});
|
|
69
63
|
}
|
package/package.json
CHANGED