@bacnh85/pi-web 0.5.2 → 0.5.4
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/index.ts +368 -368
- package/extensions/lib/brave.ts +30 -30
- package/extensions/lib/config.ts +99 -93
- package/extensions/lib/content.ts +67 -59
- package/extensions/lib/crawl4ai.ts +97 -97
- package/extensions/lib/extract.ts +137 -137
- package/extensions/lib/firecrawl.ts +28 -28
- package/extensions/lib/format.ts +138 -141
- package/extensions/lib/retry.ts +32 -32
- package/extensions/lib/search.ts +174 -168
- package/extensions/lib/searxng.ts +49 -49
- package/extensions/types.d.ts +1 -1
- package/package.json +1 -1
package/extensions/lib/format.ts
CHANGED
|
@@ -8,13 +8,13 @@ const OUTPUT_MAX_LINES = 2_000;
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
10
|
export interface SearchResultItem {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
title: string;
|
|
12
|
+
url: string;
|
|
13
|
+
snippet?: string;
|
|
14
|
+
age?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
content?: string;
|
|
17
|
+
markdown?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
@@ -22,42 +22,39 @@ export interface SearchResultItem {
|
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
23
|
|
|
24
24
|
export function sanitizeSnippet(text = ""): string {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
return text
|
|
26
|
+
.replace(/<[^>]*>/g, "")
|
|
27
|
+
.replace(/ /gi, " ")
|
|
28
|
+
.replace(/&/gi, "&")
|
|
29
|
+
.replace(/</gi, "<")
|
|
30
|
+
.replace(/>/gi, ">")
|
|
31
|
+
.replace(/"/gi, '"')
|
|
32
|
+
.replace(/'|'/gi, "'")
|
|
33
|
+
.replace(/&#(\d+);/g, (_: string, code: string) => String.fromCodePoint(Number(code)))
|
|
34
|
+
.replace(/&#x([0-9a-f]+);/gi, (_: string, code: string) => String.fromCodePoint(Number.parseInt(code, 16)))
|
|
35
|
+
.replace(/\s+/g, " ")
|
|
36
|
+
.trim();
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Sanitize an error message — redact sensitive tokens and truncate length.
|
|
41
41
|
*/
|
|
42
42
|
export function sanitizeError(error: unknown): string {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
44
|
+
return raw
|
|
45
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]")
|
|
46
|
+
.replace(/X-Subscription-Token\s*[:=]\s*[^\s]+/gi, "X-Subscription-Token: [REDACTED]")
|
|
47
|
+
.replace(/api[_-]?key[=:][^\s&]+/gi, "api_key=[REDACTED]")
|
|
48
|
+
.slice(0, 500);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function truncateText(text: string): string {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
output += `\n\n[Web output truncated to ${OUTPUT_MAX_LINES} lines / ${OUTPUT_MAX_BYTES} bytes.]`;
|
|
59
|
-
}
|
|
60
|
-
return output;
|
|
52
|
+
const lines = text.split("\n");
|
|
53
|
+
const lineTruncated = lines.slice(0, OUTPUT_MAX_LINES).join("\n");
|
|
54
|
+
const buf = Buffer.from(lineTruncated, "utf8");
|
|
55
|
+
if (buf.length <= OUTPUT_MAX_BYTES && lines.length <= OUTPUT_MAX_LINES) return text;
|
|
56
|
+
const truncated = buf.slice(0, OUTPUT_MAX_BYTES).toString("utf8").replace(/\uFFFD+$/g, "");
|
|
57
|
+
return truncated + `\n\n[Web output truncated to ${OUTPUT_MAX_LINES} lines / ${OUTPUT_MAX_BYTES} bytes.]`;
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
// ---------------------------------------------------------------------------
|
|
@@ -65,30 +62,30 @@ export function truncateText(text: string): string {
|
|
|
65
62
|
// ---------------------------------------------------------------------------
|
|
66
63
|
|
|
67
64
|
export interface UnifiedSearchResult {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
title: string;
|
|
66
|
+
url: string;
|
|
67
|
+
snippet: string;
|
|
68
|
+
age: string;
|
|
69
|
+
content: string;
|
|
70
|
+
backend: string;
|
|
74
71
|
}
|
|
75
72
|
|
|
76
73
|
export function formatUnifiedSearchResults(results: UnifiedSearchResult[]): string {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
74
|
+
if (!results.length) return "No results found.";
|
|
75
|
+
return results
|
|
76
|
+
.map((r, i) =>
|
|
77
|
+
[
|
|
78
|
+
`--- Result ${i + 1} (backend: ${r.backend}) ---`,
|
|
79
|
+
`Title: ${r.title || ""}`,
|
|
80
|
+
`Link: ${r.url || ""}`,
|
|
81
|
+
r.age ? `Age: ${r.age}` : "",
|
|
82
|
+
r.snippet ? `Snippet: ${r.snippet}` : "",
|
|
83
|
+
r.content ? `Content:\n${r.content}` : "",
|
|
84
|
+
]
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.join("\n"),
|
|
87
|
+
)
|
|
88
|
+
.join("\n\n");
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
// ---------------------------------------------------------------------------
|
|
@@ -96,20 +93,20 @@ export function formatUnifiedSearchResults(results: UnifiedSearchResult[]): stri
|
|
|
96
93
|
// ---------------------------------------------------------------------------
|
|
97
94
|
|
|
98
95
|
export function formatFirecrawlScrape(data: Record<string, unknown>, maxChars = 20000): string {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
const page = (data.data as Record<string, unknown>) || data;
|
|
97
|
+
const meta = (page.metadata as Record<string, unknown>) || {};
|
|
98
|
+
const parts: string[] = [];
|
|
99
|
+
if (meta.title) parts.push(`# ${meta.title}`);
|
|
100
|
+
if (meta.sourceURL || meta.url) parts.push(`Source: ${(meta.sourceURL || meta.url) as string}`);
|
|
101
|
+
if (meta.statusCode) parts.push(`Status: ${meta.statusCode}`);
|
|
102
|
+
const warning = (data.warning || page.warning) as string | undefined;
|
|
103
|
+
if (warning) parts.push(`Warning: ${warning}`);
|
|
104
|
+
const body = (page.markdown || page.summary || page.answer || page.html || page.rawHtml) as string | undefined;
|
|
105
|
+
if (body) parts.push(String(body).slice(0, maxChars));
|
|
106
|
+
if (Array.isArray(page.links)) {
|
|
107
|
+
parts.push(["## Links", ...(page.links as string[]).slice(0, 100).map((l: string) => `- ${l}`)].join("\n"));
|
|
108
|
+
}
|
|
109
|
+
return parts.join("\n\n") || JSON.stringify(data, null, 2);
|
|
113
110
|
}
|
|
114
111
|
|
|
115
112
|
// ---------------------------------------------------------------------------
|
|
@@ -117,83 +114,83 @@ export function formatFirecrawlScrape(data: Record<string, unknown>, maxChars =
|
|
|
117
114
|
// ---------------------------------------------------------------------------
|
|
118
115
|
|
|
119
116
|
function extractMarkdown(md: unknown): string {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
117
|
+
if (!md) return "";
|
|
118
|
+
if (typeof md === "string") return md;
|
|
119
|
+
if (typeof md === "object") {
|
|
120
|
+
const obj = md as Record<string, unknown>;
|
|
121
|
+
return (obj.fit_markdown as string) || (obj.raw_markdown as string) || "";
|
|
122
|
+
}
|
|
123
|
+
return "";
|
|
127
124
|
}
|
|
128
125
|
|
|
129
126
|
function extractLinks(links: unknown): string[] {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
127
|
+
if (!links) return [];
|
|
128
|
+
const arr = Array.isArray(links) ? links : typeof links === "object" ? Object.values(links) : [];
|
|
129
|
+
return arr.flatMap((item: any) => {
|
|
130
|
+
if (typeof item === "string") return [item];
|
|
131
|
+
if (item?.href) return [item.href];
|
|
132
|
+
if (item?.url) return [item.url];
|
|
133
|
+
return [];
|
|
134
|
+
});
|
|
138
135
|
}
|
|
139
136
|
|
|
140
137
|
export function formatCrawl4aiResult(data: Record<string, unknown>, maxChars = 20000): string {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
138
|
+
// Handle wrapped response: { results: [...] }
|
|
139
|
+
const rawResults = (data.results as unknown[]) || [data];
|
|
140
|
+
return rawResults
|
|
141
|
+
.map((raw: any, i: number) => {
|
|
142
|
+
const parts: string[] = [];
|
|
143
|
+
const url = raw.url || raw.redirected_url || "";
|
|
144
|
+
const success = raw.success !== false;
|
|
145
|
+
const statusCode = raw.status_code || raw.metadata?.statusCode;
|
|
146
|
+
|
|
147
|
+
if (rawResults.length > 1) parts.push(`=== Result ${i + 1} ===`);
|
|
148
|
+
if (url) parts.push(`URL: ${url}`);
|
|
149
|
+
if (statusCode) parts.push(`Status: ${statusCode}`);
|
|
150
|
+
if (!success) {
|
|
151
|
+
parts.push(`Error: ${raw.error_message || "Crawl failed"}`);
|
|
152
|
+
return parts.join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Markdown content
|
|
156
|
+
const md = extractMarkdown(raw.markdown);
|
|
157
|
+
if (md) {
|
|
158
|
+
const truncated = md.slice(0, maxChars);
|
|
159
|
+
parts.push(truncated);
|
|
160
|
+
if (truncated.length < md.length) parts.push("[Markdown truncated...]");
|
|
161
|
+
} else if (raw.extracted_content) {
|
|
162
|
+
const ec =
|
|
163
|
+
typeof raw.extracted_content === "string"
|
|
164
|
+
? raw.extracted_content
|
|
165
|
+
: JSON.stringify(raw.extracted_content, null, 2);
|
|
166
|
+
parts.push(ec.slice(0, maxChars));
|
|
167
|
+
} else if (raw.cleaned_html) {
|
|
168
|
+
parts.push(`(Cleaned HTML: ${raw.cleaned_html.length} chars)`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Links
|
|
172
|
+
const links = raw.links as Record<string, unknown> | undefined;
|
|
173
|
+
if (links) {
|
|
174
|
+
const internalLinks = extractLinks(links.internal);
|
|
175
|
+
const externalLinks = extractLinks(links.external);
|
|
176
|
+
const allLinks = [...internalLinks, ...externalLinks];
|
|
177
|
+
if (allLinks.length) {
|
|
178
|
+
parts.push(`Links (${allLinks.length}): ${allLinks.slice(0, 50).join(", ")}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Media
|
|
183
|
+
const media = raw.media as Record<string, unknown> | undefined;
|
|
184
|
+
if (media) {
|
|
185
|
+
const imageCount = (media.images as unknown[])?.length || 0;
|
|
186
|
+
const videoCount = (media.videos as unknown[])?.length || 0;
|
|
187
|
+
if (imageCount || videoCount) {
|
|
188
|
+
parts.push(`Media: ${imageCount} images, ${videoCount} videos`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return parts.join("\n\n");
|
|
193
|
+
})
|
|
194
|
+
.join("\n\n---\n\n") || "(No data)";
|
|
198
195
|
}
|
|
199
196
|
|
package/extensions/lib/retry.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
7
|
export class HttpError extends Error {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
status: number;
|
|
9
|
+
constructor(status: number, statusText: string, text: string) {
|
|
10
|
+
super(`HTTP ${status}: ${statusText}${text ? `\n${text}` : ""}`);
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
@@ -17,43 +17,43 @@ export class HttpError extends Error {
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
|
|
19
19
|
export async function withRetry<T>(fn: () => Promise<T>, attempts = 3, signal?: AbortSignal): Promise<T> {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
let last: unknown;
|
|
21
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
22
|
+
if (signal?.aborted) throw signal.reason ?? new Error("Operation aborted.");
|
|
23
|
+
try { return await fn(); }
|
|
24
|
+
catch (error) {
|
|
25
|
+
last = error;
|
|
26
|
+
if (attempt === attempts - 1) break;
|
|
27
|
+
await abortableSleep(1000 * 2 ** attempt, signal);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw last ?? new Error("retry failed");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Combine a timeout signal with an optional external signal.
|
|
35
35
|
*/
|
|
36
36
|
export function signalWithTimeout(timeoutMs: number, signal?: AbortSignal): AbortSignal {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
38
|
+
if (!signal) return timeoutSignal;
|
|
39
|
+
return AbortSignal.any([signal, timeoutSignal]);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Sleep that is abortable via an AbortSignal.
|
|
44
44
|
*/
|
|
45
45
|
export function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
46
|
+
if (signal?.aborted) return Promise.reject(signal.reason ?? new Error("Operation aborted."));
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const timer = setTimeout(cleanupResolve, ms);
|
|
49
|
+
function cleanupResolve() {
|
|
50
|
+
signal?.removeEventListener("abort", cleanupReject);
|
|
51
|
+
resolve();
|
|
52
|
+
}
|
|
53
|
+
function cleanupReject() {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
reject(signal?.reason ?? new Error("Operation aborted."));
|
|
56
|
+
}
|
|
57
|
+
signal?.addEventListener("abort", cleanupReject, { once: true });
|
|
58
|
+
});
|
|
59
59
|
}
|