@bacnh85/pi-web 0.5.2 → 0.5.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/extensions/lib/config.ts +11 -5
- package/extensions/lib/content.ts +18 -10
- package/extensions/lib/format.ts +5 -8
- package/extensions/lib/search.ts +16 -10
- package/package.json +1 -1
package/extensions/lib/config.ts
CHANGED
|
@@ -9,8 +9,8 @@ import path from "node:path";
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
11
|
export const HOSTED_FIRECRAWL_BASE_URL = "https://api.firecrawl.dev/v2";
|
|
12
|
-
export const DEFAULT_SEARXNG_BASE_URL = "http://
|
|
13
|
-
export const DEFAULT_CRAWL4AI_API_URL = "http://
|
|
12
|
+
export const DEFAULT_SEARXNG_BASE_URL = "http://127.0.0.1:8888";
|
|
13
|
+
export const DEFAULT_CRAWL4AI_API_URL = "http://127.0.0.1:11235";
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Environment loading
|
|
@@ -54,7 +54,7 @@ export function parseDotenvValue(rawValue: string): string {
|
|
|
54
54
|
|
|
55
55
|
export function parseDotenvFile(file: string): Record<string, string> | null {
|
|
56
56
|
let text: string;
|
|
57
|
-
try { text = fs.readFileSync(file, "utf8"); } catch
|
|
57
|
+
try { text = fs.readFileSync(file, "utf8"); } catch { return null; }
|
|
58
58
|
const values: Record<string, string> = {};
|
|
59
59
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
60
60
|
const line = rawLine.trim();
|
|
@@ -142,7 +142,10 @@ export function loadCrawl4aiConfig(params: Record<string, unknown> = {}, cwd = p
|
|
|
142
142
|
const explicitApiUrl = params.crawl4ai_api_url as string | undefined;
|
|
143
143
|
const explicitApiToken = params.crawl4ai_api_token as string | undefined;
|
|
144
144
|
const baseUrl = normalizeCrawl4aiApiUrl(explicitApiUrl || apiUrlLookup.value);
|
|
145
|
-
|
|
145
|
+
// Security: only use env-var API token when URL is default or also from env var.
|
|
146
|
+
// If the URL is overridden via params but no explicit token was provided, don't send
|
|
147
|
+
// the env-var token to a potentially attacker-controlled URL.
|
|
148
|
+
const apiToken = explicitApiToken || (explicitApiUrl ? "" : apiTokenLookup.value) || "";
|
|
146
149
|
const timeoutValue = (params.timeout_ms as number) || findEnvValue("CRAWL4AI_API_TIMEOUT_MS", cwd, includeCwdEnv).value;
|
|
147
150
|
const timeoutMs = timeoutValue ? Number.parseInt(String(timeoutValue), 10) : 60000;
|
|
148
151
|
if (!Number.isInteger(timeoutMs) || timeoutMs < 1000) throw new Error("CRAWL4AI_API_TIMEOUT_MS/timeout_ms must be an integer >= 1000.");
|
|
@@ -171,7 +174,10 @@ export function loadFirecrawlConfig(params: Record<string, unknown> = {}, cwd =
|
|
|
171
174
|
const apiKeyLookup = findEnvValue("FIRECRAWL_API_KEY", cwd, includeCwdEnv);
|
|
172
175
|
const explicitApiKey = (params.firecrawl_api_key as string) || (params.api_key as string);
|
|
173
176
|
const apiUrl = (params.firecrawl_api_url as string) || (params.api_url as string) || apiUrlLookup.value;
|
|
174
|
-
|
|
177
|
+
// Security: only use env-var API key when URL is default or also from env var.
|
|
178
|
+
// If the URL is overridden via params but no explicit key was provided, don't send
|
|
179
|
+
// the env-var key to a potentially attacker-controlled URL.
|
|
180
|
+
const apiKey = explicitApiKey || (apiUrl !== apiUrlLookup.value && apiUrl ? "" : apiKeyLookup.value) || "";
|
|
175
181
|
const timeoutValue = (params.timeout_ms as number) || findEnvValue("FIRECRAWL_TIMEOUT_MS", cwd, includeCwdEnv).value;
|
|
176
182
|
const baseUrl = normalizeFirecrawlBaseUrl(apiUrl);
|
|
177
183
|
const isHosted = !apiUrl || baseUrl.startsWith(HOSTED_FIRECRAWL_BASE_URL);
|
|
@@ -62,17 +62,25 @@ export async function fetchReadableContent(
|
|
|
62
62
|
const html = await response.text();
|
|
63
63
|
const deps = loadReadableContentDependencies();
|
|
64
64
|
const dom = new deps.JSDOM(html, { url });
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
try {
|
|
66
|
+
const article = new deps.Readability(dom.window.document).parse();
|
|
67
|
+
if (article?.content) {
|
|
68
|
+
return { title: article.title || "", markdown: htmlToMarkdown(article.content, deps) };
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
dom.window.close();
|
|
68
72
|
}
|
|
69
73
|
// Fallback: strip non-content elements and use main/article/body
|
|
70
74
|
const fallbackDoc = new deps.JSDOM(html, { url });
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
try {
|
|
76
|
+
const doc = fallbackDoc.window.document;
|
|
77
|
+
doc.querySelectorAll("script, style, noscript, nav, header, footer, aside").forEach((el: any) => el.remove());
|
|
78
|
+
const title = doc.querySelector("title")?.textContent?.trim() || "";
|
|
79
|
+
const main = doc.querySelector("main, article, [role='main'], .content, #content") || doc.body;
|
|
80
|
+
const fallbackHtml = main?.innerHTML || "";
|
|
81
|
+
if (fallbackHtml.trim().length <= 100) throw new Error("Could not extract readable content from this page.");
|
|
82
|
+
return { title, markdown: htmlToMarkdown(fallbackHtml, deps) };
|
|
83
|
+
} finally {
|
|
84
|
+
fallbackDoc.window.close();
|
|
85
|
+
}
|
|
78
86
|
}
|
package/extensions/lib/format.ts
CHANGED
|
@@ -50,14 +50,11 @@ export function sanitizeError(error: unknown): string {
|
|
|
50
50
|
|
|
51
51
|
export function truncateText(text: string): string {
|
|
52
52
|
const lines = text.split("\n");
|
|
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;
|
|
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
|
// ---------------------------------------------------------------------------
|
package/extensions/lib/search.ts
CHANGED
|
@@ -128,16 +128,22 @@ async function searchBrave(params: SearchParams, backends: BackendConfig): Promi
|
|
|
128
128
|
}));
|
|
129
129
|
if (params.include_content && results.length > 0) {
|
|
130
130
|
const maxChars = params.content_chars ?? 5000;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
// Cap concurrent content fetches to avoid OOM from parallel JSDOM parsing
|
|
132
|
+
const MAX_CONCURRENT_FETCHES = 5;
|
|
133
|
+
const batchSize = Math.min(results.length, MAX_CONCURRENT_FETCHES);
|
|
134
|
+
for (let i = 0; i < results.length; i += batchSize) {
|
|
135
|
+
const batch = results.slice(i, i + batchSize);
|
|
136
|
+
await Promise.all(
|
|
137
|
+
batch.map(async (r) => {
|
|
138
|
+
try {
|
|
139
|
+
const article = await fetchReadableContent(r.url, params.timeout_ms ?? 10000, params.signal);
|
|
140
|
+
r.content = article.markdown.slice(0, maxChars);
|
|
141
|
+
} catch (e: any) {
|
|
142
|
+
r.content = `(Fetch error for ${r.url}: ${sanitizeError(e)})`;
|
|
143
|
+
}
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
141
147
|
}
|
|
142
148
|
return results;
|
|
143
149
|
}
|
package/package.json
CHANGED