openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
227 lines (226 loc) • 8.7 kB
JavaScript
import { u as normalizeResolvedSecretInputString } from "./types.secrets-_0JOMGE5.js";
import { _ as resolvePinnedHostnameWithPolicy, c as isBlockedHostnameOrIp, u as isPrivateIpAddress } from "./ssrf-CvPEXMGn.js";
import { n as normalizeSecretInput } from "./normalize-secret-input-OwRUfByL.js";
import { a as wrapWebContent } from "./external-content-pX-Pk1Iu.js";
import { a as readResponseText, c as resolveTimeoutSeconds, i as readCache, o as resolveCacheTtlMs, r as normalizeCacheKey, u as writeCache } from "./web-shared-TPeHhehr.js";
import { b as withSelfHostedWebSearchEndpoint, g as resolveSearchCount, v as resolveSiteName, x as withTrustedWebSearchEndpoint } from "./web-search-provider-common-DisZTXTb.js";
import "./secret-input-DVCzFGxN.js";
import { t as assertHttpUrlTargetsPrivateNetwork } from "./ssrf-policy-sZLyyYht.js";
import "./ssrf-runtime-BOGN5pUi.js";
import "./provider-web-search-9G87vZMY.js";
//#region extensions/searxng/src/config.ts
function normalizeConfiguredString(value, path) {
try {
return normalizeSecretInput(normalizeResolvedSecretInputString({
value,
path
}));
} catch {
return;
}
}
function readInlineEnvSecretRefValue(value, env) {
if (!value || typeof value !== "object" || Array.isArray(value)) return;
const record = value;
if (record.source !== "env" || typeof record.id !== "string") return;
return normalizeSecretInput(env[record.id]);
}
function normalizeTrimmedString(value) {
if (typeof value !== "string") return;
return value.trim() || void 0;
}
function normalizeBaseUrl(value) {
return value?.replace(/\/+$/u, "") || void 0;
}
function resolveSearxngWebSearchConfig(config) {
const webSearch = (config?.plugins?.entries?.searxng?.config)?.webSearch;
if (webSearch && typeof webSearch === "object" && !Array.isArray(webSearch)) return webSearch;
}
function resolveSearxngBaseUrl(config, env = process.env) {
const webSearch = resolveSearxngWebSearchConfig(config);
return normalizeBaseUrl(normalizeConfiguredString(webSearch?.baseUrl, "plugins.entries.searxng.config.webSearch.baseUrl")) ?? normalizeBaseUrl(readInlineEnvSecretRefValue(webSearch?.baseUrl, env)) ?? normalizeBaseUrl(normalizeSecretInput(env.SEARXNG_BASE_URL));
}
function resolveSearxngCategories(config) {
return normalizeTrimmedString(resolveSearxngWebSearchConfig(config)?.categories);
}
function resolveSearxngLanguage(config) {
return normalizeTrimmedString(resolveSearxngWebSearchConfig(config)?.language);
}
//#endregion
//#region extensions/searxng/src/searxng-client.ts
const DEFAULT_TIMEOUT_SECONDS = 20;
const MAX_RESPONSE_BYTES = 1e6;
const SEARXNG_SEARCH_CACHE = /* @__PURE__ */ new Map();
function normalizeSearxngResult(value) {
if (!value || typeof value !== "object") return null;
const candidate = value;
if (typeof candidate.url !== "string" || typeof candidate.title !== "string") return null;
return {
url: candidate.url,
title: candidate.title,
content: typeof candidate.content === "string" ? candidate.content : void 0,
img_src: typeof candidate.img_src === "string" ? candidate.img_src : void 0
};
}
function buildSearxngSearchUrl(params) {
const url = new URL(params.baseUrl);
url.pathname = url.pathname.endsWith("/") ? `${url.pathname}search` : `${url.pathname}/search`;
url.search = "";
url.searchParams.set("q", params.query);
url.searchParams.set("format", "json");
if (params.categories) url.searchParams.set("categories", params.categories);
if (params.language) url.searchParams.set("language", params.language);
return url.toString();
}
function shouldRetryEmptyCategorySearchWithGeneral(categories) {
if (!categories) return false;
const normalized = categories.split(",").map((category) => category.trim().toLowerCase()).filter((category) => category.length > 0);
return normalized.length > 0 && !normalized.includes("general");
}
async function searxngEndpointTargetsPrivateNetwork(url, lookupFn) {
if (isBlockedHostnameOrIp(url.hostname)) return true;
try {
return (await resolvePinnedHostnameWithPolicy(url.hostname, {
lookupFn,
policy: {
allowPrivateNetwork: true,
allowRfc2544BenchmarkRange: true
}
})).addresses.every((address) => isPrivateIpAddress(address));
} catch {
return false;
}
}
async function validateSearxngBaseUrl(baseUrl, lookupFn) {
let parsed;
try {
parsed = new URL(baseUrl);
} catch {
throw new Error("SearXNG base URL must be a valid http:// or https:// URL.");
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("SearXNG base URL must use http:// or https://.");
if (parsed.protocol === "http:") {
await assertHttpUrlTargetsPrivateNetwork(parsed.toString(), {
dangerouslyAllowPrivateNetwork: true,
lookupFn,
errorMessage: "SearXNG HTTP base URL must target a trusted private or loopback host. Use https:// for public hosts."
});
return "selfHosted";
}
return await searxngEndpointTargetsPrivateNetwork(parsed, lookupFn) ? "selfHosted" : "strict";
}
function parseSearxngResponseText(text, count) {
let parsed;
try {
parsed = JSON.parse(text);
} catch {
throw new Error("SearXNG returned invalid JSON.");
}
if (!parsed || typeof parsed !== "object") return [];
const response = parsed;
const rawResults = Array.isArray(response.results) ? response.results : [];
const results = [];
for (const rawResult of rawResults) {
const result = normalizeSearxngResult(rawResult);
if (result) results.push(result);
if (results.length >= count) break;
}
return results;
}
async function fetchSearxngResults(params) {
const url = buildSearxngSearchUrl({
baseUrl: params.baseUrl,
query: params.query,
categories: params.categories,
language: params.language
});
return await (params.endpointMode === "selfHosted" ? withSelfHostedWebSearchEndpoint : withTrustedWebSearchEndpoint)({
url,
timeoutSeconds: params.timeoutSeconds,
init: {
method: "GET",
headers: { Accept: "application/json" }
}
}, async (response) => {
if (!response.ok) {
const detail = (await readResponseText(response, { maxBytes: 64e3 })).text;
throw new Error(`SearXNG search error (${response.status}): ${detail || response.statusText}`);
}
const body = await readResponseText(response, { maxBytes: MAX_RESPONSE_BYTES });
if (body.truncated) throw new Error("SearXNG response too large.");
return parseSearxngResponseText(body.text, params.count);
});
}
async function runSearxngSearch(params) {
const count = resolveSearchCount(params.count, 5);
const categories = params.categories ?? resolveSearxngCategories(params.config);
const language = params.language ?? resolveSearxngLanguage(params.config);
const baseUrl = params.baseUrl ?? resolveSearxngBaseUrl(params.config);
const timeoutSeconds = resolveTimeoutSeconds(params.timeoutSeconds, DEFAULT_TIMEOUT_SECONDS);
const cacheTtlMs = resolveCacheTtlMs(params.cacheTtlMinutes, 15);
if (!baseUrl) throw new Error("SearXNG base URL is not configured. Set SEARXNG_BASE_URL or configure plugins.entries.searxng.config.webSearch.baseUrl.");
const endpointMode = await validateSearxngBaseUrl(baseUrl);
const cacheKey = normalizeCacheKey(JSON.stringify({
provider: "searxng",
query: params.query,
count,
categories: categories ?? "",
language: language ?? "",
baseUrl
}));
const cached = readCache(SEARXNG_SEARCH_CACHE, cacheKey);
if (cached) return {
...cached.value,
cached: true
};
const startedAt = Date.now();
let results = await fetchSearxngResults({
baseUrl,
query: params.query,
categories,
language,
timeoutSeconds,
count,
endpointMode
});
if (results.length === 0 && shouldRetryEmptyCategorySearchWithGeneral(categories)) results = await fetchSearxngResults({
baseUrl,
query: params.query,
categories: "general",
language,
timeoutSeconds,
count,
endpointMode
});
const payload = {
query: params.query,
provider: "searxng",
count: results.length,
tookMs: Date.now() - startedAt,
externalContent: {
untrusted: true,
source: "web_search",
provider: "searxng",
wrapped: true
},
results: results.map((result) => ({
title: wrapWebContent(result.title, "web_search"),
url: result.url,
snippet: result.content ? wrapWebContent(result.content, "web_search") : "",
siteName: resolveSiteName(result.url) || void 0,
img_src: result.img_src || void 0
}))
};
writeCache(SEARXNG_SEARCH_CACHE, cacheKey, payload, cacheTtlMs);
return payload;
}
const testing = {
buildSearxngSearchUrl,
normalizeSearxngResult,
parseSearxngResponseText,
shouldRetryEmptyCategorySearchWithGeneral,
validateSearxngBaseUrl,
SEARXNG_SEARCH_CACHE
};
//#endregion
export { testing as __testing, testing, runSearxngSearch };