@openclaw/searxng-plugin 2026.10.10-beta.5 → 2026.11.5-beta.1
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/.setup/searxng-client-BdCQ2r3O.mjs +154 -0
- package/dist/.setup/{searxng-search-provider-G09qHDjp.mjs → searxng-search-provider-Ba_nS_GN.mjs} +4 -14
- package/dist/.source-module-urls/5269b191e9602ca2d2f21224255368aa26997632a9c699e6d428543dc4d20cec.json +1 -1
- package/dist/index.js +2 -3
- package/dist/web-search-provider.js +1 -1
- package/package.json +4 -4
- package/dist/.setup/searxng-client-DHycwhcy.mjs +0 -203
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { ProviderHttpError } from "openclaw/plugin-sdk/provider-http";
|
|
2
|
+
import { DEFAULT_CACHE_TTL_MINUTES, DEFAULT_SEARCH_COUNT, externalContentStamp, readCachedSearchPayload, readResponseText, resolveCacheTtlMs, resolveSearchCount, resolveSiteName, resolveTimeoutSeconds, withSelfHostedWebSearchEndpoint, withTrustedWebSearchEndpoint, wrapWebContent, writeCachedSearchPayload } from "openclaw/plugin-sdk/provider-web-search";
|
|
3
|
+
import { resolveProviderWebSearchPluginConfig } from "openclaw/plugin-sdk/provider-web-search-config-contract";
|
|
4
|
+
import { normalizeSecretInput } from "openclaw/plugin-sdk/secret-input";
|
|
5
|
+
import { resolveReadOnlyEnvSecretRef } from "openclaw/plugin-sdk/secret-ref-readonly";
|
|
6
|
+
import { assertHttpUrlTargetsPrivateNetwork, isBlockedHostnameOrIp, isPrivateIpAddress, resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
7
|
+
import { asOptionalObjectRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
|
+
// extensions/searxng/src/searxng-client.ts
|
|
9
|
+
const SEARXNG_BASE_URL_ENV_VAR = "SEARXNG_BASE_URL";
|
|
10
|
+
const SEARXNG_BASE_URL_PATH = "plugins.entries.searxng.config.webSearch.baseUrl";
|
|
11
|
+
const DEFAULT_TIMEOUT_SECONDS = 20;
|
|
12
|
+
const MAX_RESPONSE_BYTES = 1e6;
|
|
13
|
+
function normalizeBaseUrl(value) {
|
|
14
|
+
return normalizeSecretInput(value)?.replace(/\/+$/u, "") || void 0;
|
|
15
|
+
}
|
|
16
|
+
function resolveSearxngBaseUrl(config) {
|
|
17
|
+
const webSearch = resolveProviderWebSearchPluginConfig(config, "searxng");
|
|
18
|
+
const resolved = resolveReadOnlyEnvSecretRef({
|
|
19
|
+
value: webSearch?.baseUrl,
|
|
20
|
+
path: SEARXNG_BASE_URL_PATH,
|
|
21
|
+
cfg: config,
|
|
22
|
+
expectedEnvId: SEARXNG_BASE_URL_ENV_VAR,
|
|
23
|
+
normalizeValue: normalizeBaseUrl
|
|
24
|
+
});
|
|
25
|
+
if (resolved.status === "available") return resolved.value;
|
|
26
|
+
if (resolved.status === "blocked") return;
|
|
27
|
+
return normalizeBaseUrl(process.env[SEARXNG_BASE_URL_ENV_VAR]);
|
|
28
|
+
}
|
|
29
|
+
async function searxngEndpointTargetsPrivateNetwork(url) {
|
|
30
|
+
if (isBlockedHostnameOrIp(url.hostname)) return true;
|
|
31
|
+
try {
|
|
32
|
+
return (await resolvePinnedHostnameWithPolicy(url.hostname, { policy: {
|
|
33
|
+
allowPrivateNetwork: true,
|
|
34
|
+
allowRfc2544BenchmarkRange: true
|
|
35
|
+
} })).addresses.every((address) => isPrivateIpAddress(address));
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function validateSearxngBaseUrl(baseUrl) {
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = new URL(baseUrl);
|
|
44
|
+
} catch {
|
|
45
|
+
throw new Error("SearXNG base URL must be a valid http:// or https:// URL.");
|
|
46
|
+
}
|
|
47
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("SearXNG base URL must use http:// or https://.");
|
|
48
|
+
if (parsed.protocol === "http:") {
|
|
49
|
+
await assertHttpUrlTargetsPrivateNetwork(parsed.toString(), {
|
|
50
|
+
dangerouslyAllowPrivateNetwork: true,
|
|
51
|
+
errorMessage: "SearXNG HTTP base URL must target a trusted private or loopback host. Use https:// for public hosts."
|
|
52
|
+
});
|
|
53
|
+
return "selfHosted";
|
|
54
|
+
}
|
|
55
|
+
return await searxngEndpointTargetsPrivateNetwork(parsed) ? "selfHosted" : "strict";
|
|
56
|
+
}
|
|
57
|
+
async function runSearxngSearch(params) {
|
|
58
|
+
params.signal?.throwIfAborted();
|
|
59
|
+
const count = resolveSearchCount(params.count ?? params.config?.tools?.web?.search?.maxResults, DEFAULT_SEARCH_COUNT);
|
|
60
|
+
const categories = params.categories ?? normalizeOptionalString(resolveProviderWebSearchPluginConfig(params.config, "searxng")?.categories);
|
|
61
|
+
const language = params.language ?? normalizeOptionalString(resolveProviderWebSearchPluginConfig(params.config, "searxng")?.language);
|
|
62
|
+
const baseUrl = resolveSearxngBaseUrl(params.config);
|
|
63
|
+
const timeoutSeconds = resolveTimeoutSeconds(params.config?.tools?.web?.search?.timeoutSeconds, DEFAULT_TIMEOUT_SECONDS);
|
|
64
|
+
const cacheTtlMs = resolveCacheTtlMs(params.config?.tools?.web?.search?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES);
|
|
65
|
+
if (!baseUrl) throw new Error("SearXNG base URL is not configured. Set SEARXNG_BASE_URL or configure plugins.entries.searxng.config.webSearch.baseUrl.");
|
|
66
|
+
const endpointMode = await validateSearxngBaseUrl(baseUrl);
|
|
67
|
+
params.signal?.throwIfAborted();
|
|
68
|
+
const cacheKey = JSON.stringify({
|
|
69
|
+
provider: "searxng",
|
|
70
|
+
query: params.query,
|
|
71
|
+
count,
|
|
72
|
+
categories: categories ?? "",
|
|
73
|
+
language: language ?? "",
|
|
74
|
+
baseUrl
|
|
75
|
+
});
|
|
76
|
+
const cached = readCachedSearchPayload(cacheKey, cacheTtlMs);
|
|
77
|
+
if (cached) return cached;
|
|
78
|
+
const startedAt = Date.now();
|
|
79
|
+
const query = params.query;
|
|
80
|
+
const signal = params.signal;
|
|
81
|
+
const fetchSearxngResults = async (searchCategories) => {
|
|
82
|
+
const url = new URL(baseUrl);
|
|
83
|
+
const basePathname = url.pathname.replace(/\/+$/u, "");
|
|
84
|
+
url.pathname = basePathname.endsWith("/search") ? basePathname : `${basePathname}/search`;
|
|
85
|
+
url.search = "";
|
|
86
|
+
url.searchParams.set("q", query);
|
|
87
|
+
url.searchParams.set("format", "json");
|
|
88
|
+
if (searchCategories) url.searchParams.set("categories", searchCategories);
|
|
89
|
+
if (language) url.searchParams.set("language", language);
|
|
90
|
+
return await (endpointMode === "selfHosted" ? withSelfHostedWebSearchEndpoint : withTrustedWebSearchEndpoint)({
|
|
91
|
+
url: url.toString(),
|
|
92
|
+
timeoutSeconds,
|
|
93
|
+
signal,
|
|
94
|
+
init: {
|
|
95
|
+
method: "GET",
|
|
96
|
+
headers: { Accept: "application/json" }
|
|
97
|
+
}
|
|
98
|
+
}, async (response) => {
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const detail = (await readResponseText(response, { maxBytes: 64e3 })).text;
|
|
101
|
+
throw new ProviderHttpError(`SearXNG search error (${response.status}): ${detail || response.statusText}`, { status: response.status });
|
|
102
|
+
}
|
|
103
|
+
const body = await readResponseText(response, { maxBytes: MAX_RESPONSE_BYTES });
|
|
104
|
+
if (body.truncated) throw new Error(`SearXNG response incomplete after ${body.bytesRead} bytes.`);
|
|
105
|
+
const text = body.text;
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(text);
|
|
109
|
+
} catch {
|
|
110
|
+
throw new Error("SearXNG returned invalid JSON.");
|
|
111
|
+
}
|
|
112
|
+
const rawResults = asOptionalObjectRecord(parsed)?.results;
|
|
113
|
+
const results = [];
|
|
114
|
+
for (const rawResult of Array.isArray(rawResults) ? rawResults : []) {
|
|
115
|
+
const candidate = asOptionalObjectRecord(rawResult);
|
|
116
|
+
const result = typeof candidate?.url !== "string" || typeof candidate.title !== "string" ? null : {
|
|
117
|
+
url: candidate.url,
|
|
118
|
+
title: candidate.title,
|
|
119
|
+
content: typeof candidate.content === "string" ? candidate.content : void 0,
|
|
120
|
+
img_src: typeof candidate.img_src === "string" ? candidate.img_src : void 0
|
|
121
|
+
};
|
|
122
|
+
if (result) results.push(result);
|
|
123
|
+
if (results.length >= count) break;
|
|
124
|
+
}
|
|
125
|
+
return results;
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
let results = await fetchSearxngResults(categories);
|
|
129
|
+
params.signal?.throwIfAborted();
|
|
130
|
+
if (results.length === 0 && categories) {
|
|
131
|
+
const normalized = categories.split(",").map((category) => category.trim().toLowerCase()).filter((category) => category.length > 0);
|
|
132
|
+
if (normalized.length > 0 && !normalized.includes("general")) {
|
|
133
|
+
results = await fetchSearxngResults("general");
|
|
134
|
+
params.signal?.throwIfAborted();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const payload = {
|
|
138
|
+
query: params.query,
|
|
139
|
+
provider: "searxng",
|
|
140
|
+
count: results.length,
|
|
141
|
+
tookMs: Date.now() - startedAt,
|
|
142
|
+
externalContent: externalContentStamp("searxng"),
|
|
143
|
+
results: results.map((result) => ({
|
|
144
|
+
title: wrapWebContent(result.title, "web_search"),
|
|
145
|
+
url: result.url,
|
|
146
|
+
snippet: result.content ? wrapWebContent(result.content, "web_search") : "",
|
|
147
|
+
siteName: resolveSiteName(result.url) || void 0,
|
|
148
|
+
img_src: result.img_src || void 0
|
|
149
|
+
}))
|
|
150
|
+
};
|
|
151
|
+
writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
|
|
152
|
+
return payload;
|
|
153
|
+
}
|
|
154
|
+
export { runSearxngSearch };
|
package/dist/.setup/{searxng-search-provider-G09qHDjp.mjs → searxng-search-provider-Ba_nS_GN.mjs}
RENAMED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
2
2
|
import { readPositiveIntegerParam, readStringParam } from "openclaw/plugin-sdk/param-readers";
|
|
3
|
-
import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-contract";
|
|
4
|
-
|
|
3
|
+
import { WEB_SEARCH_QUERY_COUNT_PROPERTIES, createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-contract";
|
|
4
|
+
// extensions/searxng/src/searxng-search-provider.ts
|
|
5
5
|
const SEARXNG_CREDENTIAL_PATH = "plugins.entries.searxng.config.webSearch.baseUrl";
|
|
6
|
-
const loadSearxngClientModule = createLazyRuntimeModule(() => import("./searxng-client-
|
|
6
|
+
const loadSearxngClientModule = createLazyRuntimeModule(() => import("./searxng-client-BdCQ2r3O.mjs"));
|
|
7
7
|
const SearxngSearchSchema = {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
10
|
-
|
|
11
|
-
type: "string",
|
|
12
|
-
description: "Search query string."
|
|
13
|
-
},
|
|
14
|
-
count: {
|
|
15
|
-
type: "integer",
|
|
16
|
-
description: "Number of results to return (1-10).",
|
|
17
|
-
minimum: 1,
|
|
18
|
-
maximum: 10
|
|
19
|
-
},
|
|
10
|
+
...WEB_SEARCH_QUERY_COUNT_PROPERTIES,
|
|
20
11
|
categories: {
|
|
21
12
|
type: "string",
|
|
22
13
|
description: "Optional comma-separated search categories such as general, news, or science."
|
|
@@ -74,5 +65,4 @@ function createSearxngWebSearchProvider() {
|
|
|
74
65
|
})
|
|
75
66
|
};
|
|
76
67
|
}
|
|
77
|
-
//#endregion
|
|
78
68
|
export { createSearxngWebSearchProvider as t };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"entries":[{"output":"index.js","source":"extensions/searxng/index.ts"},{"output":"web-search-provider.js","source":"extensions/searxng/web-search-provider.ts"}],"chunks":[{"output":".setup/searxng-client-
|
|
1
|
+
{"version":3,"entries":[{"output":"index.js","source":"extensions/searxng/index.ts"},{"output":"web-search-provider.js","source":"extensions/searxng/web-search-provider.ts"}],"chunks":[{"output":".setup/searxng-client-BdCQ2r3O.mjs","sha256":"6e0c606f60e6460606beabea5e6ef14faa273a8a35bc5072b91b158b14a9661a","facadeSource":"extensions/searxng/src/searxng-client.ts","sources":["extensions/searxng/src/searxng-client.ts"]},{"output":".setup/searxng-search-provider-Ba_nS_GN.mjs","sha256":"cbba2b70eb2262d2658b991c5574aa2eae6361dfaebeb2ee42354ecfc3160508","facadeSource":null,"sources":["extensions/searxng/src/searxng-search-provider.ts"]},{"output":"index.js","sha256":"667499a819b55add4766a8ade78a4523f0cc20a44057b0bfc43ed5f47adc313e","facadeSource":"extensions/searxng/index.ts","sources":["extensions/searxng/index.ts"]},{"output":"web-search-provider.js","sha256":"392d28986650a3ce725f054018b6ffa287bc09509f2637ee69fc8351b86bfe16","facadeSource":"extensions/searxng/web-search-provider.ts","sources":[]}],"unrenderedSources":["extensions/searxng/web-search-provider.ts"]}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as createSearxngWebSearchProvider } from "./.setup/searxng-search-provider-
|
|
1
|
+
import { t as createSearxngWebSearchProvider } from "./.setup/searxng-search-provider-Ba_nS_GN.mjs";
|
|
2
2
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
-
|
|
3
|
+
// extensions/searxng/index.ts
|
|
4
4
|
var searxng_default = definePluginEntry({
|
|
5
5
|
id: "searxng",
|
|
6
6
|
name: "SearXNG Plugin",
|
|
@@ -9,5 +9,4 @@ var searxng_default = definePluginEntry({
|
|
|
9
9
|
api.registerWebSearchProvider(createSearxngWebSearchProvider());
|
|
10
10
|
}
|
|
11
11
|
});
|
|
12
|
-
//#endregion
|
|
13
12
|
export { searxng_default as default };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as createSearxngWebSearchProvider } from "./.setup/searxng-search-provider-
|
|
1
|
+
export { t as createSearxngWebSearchProvider } from "./.setup/searxng-search-provider-Ba_nS_GN.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/searxng-plugin",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.11.5-beta.1",
|
|
4
4
|
"description": "OpenClaw SearXNG plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"openclaw": {
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
"allowInvalidConfigRecovery": true
|
|
16
16
|
},
|
|
17
17
|
"compat": {
|
|
18
|
-
"pluginApi": ">=2026.
|
|
18
|
+
"pluginApi": ">=2026.11.5-beta.1"
|
|
19
19
|
},
|
|
20
20
|
"build": {
|
|
21
|
-
"openclawVersion": "2026.
|
|
21
|
+
"openclawVersion": "2026.11.5-beta.1",
|
|
22
22
|
"bundledDist": false
|
|
23
23
|
},
|
|
24
24
|
"release": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"assets/activity.svg"
|
|
42
42
|
],
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"openclaw": ">=2026.
|
|
44
|
+
"openclaw": ">=2026.11.5-beta.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"openclaw": {
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { ProviderHttpError } from "openclaw/plugin-sdk/provider-http";
|
|
2
|
-
import { DEFAULT_CACHE_TTL_MINUTES, DEFAULT_SEARCH_COUNT, normalizeCacheKey, readCache, readResponseText, resolveCacheTtlMs, resolveSearchCount, resolveSiteName, resolveTimeoutSeconds, withSelfHostedWebSearchEndpoint, withTrustedWebSearchEndpoint, wrapWebContent, writeCache } from "openclaw/plugin-sdk/provider-web-search";
|
|
3
|
-
import { assertHttpUrlTargetsPrivateNetwork, isBlockedHostnameOrIp, isPrivateIpAddress, resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
4
|
-
import { asOptionalObjectRecord, asOptionalRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
|
-
import { normalizeSecretInput } from "openclaw/plugin-sdk/secret-input";
|
|
6
|
-
import { resolveReadOnlyEnvSecretRef } from "openclaw/plugin-sdk/secret-ref-readonly";
|
|
7
|
-
//#region extensions/searxng/src/config.ts
|
|
8
|
-
const SEARXNG_BASE_URL_ENV_VAR = "SEARXNG_BASE_URL";
|
|
9
|
-
const SEARXNG_BASE_URL_PATH = "plugins.entries.searxng.config.webSearch.baseUrl";
|
|
10
|
-
function normalizeBaseUrl(value) {
|
|
11
|
-
return normalizeSecretInput(value)?.replace(/\/+$/u, "") || void 0;
|
|
12
|
-
}
|
|
13
|
-
function resolveSearxngWebSearchConfig(config) {
|
|
14
|
-
return asOptionalRecord(config?.plugins?.entries?.searxng?.config?.webSearch);
|
|
15
|
-
}
|
|
16
|
-
function resolveSearxngBaseUrl(config) {
|
|
17
|
-
const webSearch = resolveSearxngWebSearchConfig(config);
|
|
18
|
-
const resolved = resolveReadOnlyEnvSecretRef({
|
|
19
|
-
value: webSearch?.baseUrl,
|
|
20
|
-
path: SEARXNG_BASE_URL_PATH,
|
|
21
|
-
cfg: config,
|
|
22
|
-
expectedEnvId: SEARXNG_BASE_URL_ENV_VAR,
|
|
23
|
-
normalizeValue: normalizeBaseUrl
|
|
24
|
-
});
|
|
25
|
-
if (resolved.status === "available") return resolved.value;
|
|
26
|
-
if (resolved.status === "blocked") return;
|
|
27
|
-
return normalizeBaseUrl(process.env[SEARXNG_BASE_URL_ENV_VAR]);
|
|
28
|
-
}
|
|
29
|
-
function resolveSearxngCategories(config) {
|
|
30
|
-
return normalizeOptionalString(resolveSearxngWebSearchConfig(config)?.categories);
|
|
31
|
-
}
|
|
32
|
-
function resolveSearxngLanguage(config) {
|
|
33
|
-
return normalizeOptionalString(resolveSearxngWebSearchConfig(config)?.language);
|
|
34
|
-
}
|
|
35
|
-
//#endregion
|
|
36
|
-
//#region extensions/searxng/src/searxng-client.ts
|
|
37
|
-
const DEFAULT_TIMEOUT_SECONDS = 20;
|
|
38
|
-
const MAX_RESPONSE_BYTES = 1e6;
|
|
39
|
-
const SEARXNG_SEARCH_CACHE = /* @__PURE__ */ new Map();
|
|
40
|
-
function normalizeSearxngResult(value) {
|
|
41
|
-
const candidate = asOptionalObjectRecord(value);
|
|
42
|
-
if (typeof candidate?.url !== "string" || typeof candidate.title !== "string") return null;
|
|
43
|
-
return {
|
|
44
|
-
url: candidate.url,
|
|
45
|
-
title: candidate.title,
|
|
46
|
-
content: typeof candidate.content === "string" ? candidate.content : void 0,
|
|
47
|
-
img_src: typeof candidate.img_src === "string" ? candidate.img_src : void 0
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function buildSearxngSearchUrl(params) {
|
|
51
|
-
const url = new URL(params.baseUrl);
|
|
52
|
-
const basePathname = url.pathname.replace(/\/+$/u, "");
|
|
53
|
-
url.pathname = basePathname.endsWith("/search") ? basePathname : `${basePathname}/search`;
|
|
54
|
-
url.search = "";
|
|
55
|
-
url.searchParams.set("q", params.query);
|
|
56
|
-
url.searchParams.set("format", "json");
|
|
57
|
-
if (params.categories) url.searchParams.set("categories", params.categories);
|
|
58
|
-
if (params.language) url.searchParams.set("language", params.language);
|
|
59
|
-
return url.toString();
|
|
60
|
-
}
|
|
61
|
-
function shouldRetryEmptyCategorySearchWithGeneral(categories) {
|
|
62
|
-
if (!categories) return false;
|
|
63
|
-
const normalized = categories.split(",").map((category) => category.trim().toLowerCase()).filter((category) => category.length > 0);
|
|
64
|
-
return normalized.length > 0 && !normalized.includes("general");
|
|
65
|
-
}
|
|
66
|
-
async function searxngEndpointTargetsPrivateNetwork(url) {
|
|
67
|
-
if (isBlockedHostnameOrIp(url.hostname)) return true;
|
|
68
|
-
try {
|
|
69
|
-
return (await resolvePinnedHostnameWithPolicy(url.hostname, { policy: {
|
|
70
|
-
allowPrivateNetwork: true,
|
|
71
|
-
allowRfc2544BenchmarkRange: true
|
|
72
|
-
} })).addresses.every((address) => isPrivateIpAddress(address));
|
|
73
|
-
} catch {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
async function validateSearxngBaseUrl(baseUrl) {
|
|
78
|
-
let parsed;
|
|
79
|
-
try {
|
|
80
|
-
parsed = new URL(baseUrl);
|
|
81
|
-
} catch {
|
|
82
|
-
throw new Error("SearXNG base URL must be a valid http:// or https:// URL.");
|
|
83
|
-
}
|
|
84
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("SearXNG base URL must use http:// or https://.");
|
|
85
|
-
if (parsed.protocol === "http:") {
|
|
86
|
-
await assertHttpUrlTargetsPrivateNetwork(parsed.toString(), {
|
|
87
|
-
dangerouslyAllowPrivateNetwork: true,
|
|
88
|
-
errorMessage: "SearXNG HTTP base URL must target a trusted private or loopback host. Use https:// for public hosts."
|
|
89
|
-
});
|
|
90
|
-
return "selfHosted";
|
|
91
|
-
}
|
|
92
|
-
return await searxngEndpointTargetsPrivateNetwork(parsed) ? "selfHosted" : "strict";
|
|
93
|
-
}
|
|
94
|
-
function parseSearxngResponseText(text, count) {
|
|
95
|
-
let parsed;
|
|
96
|
-
try {
|
|
97
|
-
parsed = JSON.parse(text);
|
|
98
|
-
} catch {
|
|
99
|
-
throw new Error("SearXNG returned invalid JSON.");
|
|
100
|
-
}
|
|
101
|
-
const rawResults = asOptionalObjectRecord(parsed)?.results;
|
|
102
|
-
const results = [];
|
|
103
|
-
for (const rawResult of Array.isArray(rawResults) ? rawResults : []) {
|
|
104
|
-
const result = normalizeSearxngResult(rawResult);
|
|
105
|
-
if (result) results.push(result);
|
|
106
|
-
if (results.length >= count) break;
|
|
107
|
-
}
|
|
108
|
-
return results;
|
|
109
|
-
}
|
|
110
|
-
async function fetchSearxngResults(params) {
|
|
111
|
-
const url = buildSearxngSearchUrl({
|
|
112
|
-
baseUrl: params.baseUrl,
|
|
113
|
-
query: params.query,
|
|
114
|
-
categories: params.categories,
|
|
115
|
-
language: params.language
|
|
116
|
-
});
|
|
117
|
-
return await (params.endpointMode === "selfHosted" ? withSelfHostedWebSearchEndpoint : withTrustedWebSearchEndpoint)({
|
|
118
|
-
url,
|
|
119
|
-
timeoutSeconds: params.timeoutSeconds,
|
|
120
|
-
signal: params.signal,
|
|
121
|
-
init: {
|
|
122
|
-
method: "GET",
|
|
123
|
-
headers: { Accept: "application/json" }
|
|
124
|
-
}
|
|
125
|
-
}, async (response) => {
|
|
126
|
-
if (!response.ok) {
|
|
127
|
-
const detail = (await readResponseText(response, { maxBytes: 64e3 })).text;
|
|
128
|
-
throw new ProviderHttpError(`SearXNG search error (${response.status}): ${detail || response.statusText}`, { status: response.status });
|
|
129
|
-
}
|
|
130
|
-
const body = await readResponseText(response, { maxBytes: MAX_RESPONSE_BYTES });
|
|
131
|
-
if (body.truncated) throw new Error(`SearXNG response incomplete after ${body.bytesRead} bytes.`);
|
|
132
|
-
return parseSearxngResponseText(body.text, params.count);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
async function runSearxngSearch(params) {
|
|
136
|
-
params.signal?.throwIfAborted();
|
|
137
|
-
const count = resolveSearchCount(params.count, DEFAULT_SEARCH_COUNT);
|
|
138
|
-
const categories = params.categories ?? resolveSearxngCategories(params.config);
|
|
139
|
-
const language = params.language ?? resolveSearxngLanguage(params.config);
|
|
140
|
-
const baseUrl = params.baseUrl ?? resolveSearxngBaseUrl(params.config);
|
|
141
|
-
const timeoutSeconds = resolveTimeoutSeconds(params.timeoutSeconds, DEFAULT_TIMEOUT_SECONDS);
|
|
142
|
-
const cacheTtlMs = resolveCacheTtlMs(params.cacheTtlMinutes ?? params.config?.tools?.web?.search?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES);
|
|
143
|
-
if (!baseUrl) throw new Error("SearXNG base URL is not configured. Set SEARXNG_BASE_URL or configure plugins.entries.searxng.config.webSearch.baseUrl.");
|
|
144
|
-
const endpointMode = await validateSearxngBaseUrl(baseUrl);
|
|
145
|
-
params.signal?.throwIfAborted();
|
|
146
|
-
const cacheKey = normalizeCacheKey(JSON.stringify({
|
|
147
|
-
provider: "searxng",
|
|
148
|
-
query: params.query,
|
|
149
|
-
count,
|
|
150
|
-
categories: categories ?? "",
|
|
151
|
-
language: language ?? "",
|
|
152
|
-
baseUrl
|
|
153
|
-
}));
|
|
154
|
-
const cached = readCache(SEARXNG_SEARCH_CACHE, cacheKey, cacheTtlMs);
|
|
155
|
-
if (cached) return {
|
|
156
|
-
...cached.value,
|
|
157
|
-
cached: true
|
|
158
|
-
};
|
|
159
|
-
const startedAt = Date.now();
|
|
160
|
-
const request = {
|
|
161
|
-
baseUrl,
|
|
162
|
-
query: params.query,
|
|
163
|
-
categories,
|
|
164
|
-
language,
|
|
165
|
-
timeoutSeconds,
|
|
166
|
-
count,
|
|
167
|
-
endpointMode,
|
|
168
|
-
signal: params.signal
|
|
169
|
-
};
|
|
170
|
-
let results = await fetchSearxngResults(request);
|
|
171
|
-
params.signal?.throwIfAborted();
|
|
172
|
-
if (results.length === 0 && shouldRetryEmptyCategorySearchWithGeneral(categories)) {
|
|
173
|
-
results = await fetchSearxngResults({
|
|
174
|
-
...request,
|
|
175
|
-
categories: "general"
|
|
176
|
-
});
|
|
177
|
-
params.signal?.throwIfAborted();
|
|
178
|
-
}
|
|
179
|
-
const payload = {
|
|
180
|
-
query: params.query,
|
|
181
|
-
provider: "searxng",
|
|
182
|
-
count: results.length,
|
|
183
|
-
tookMs: Date.now() - startedAt,
|
|
184
|
-
externalContent: {
|
|
185
|
-
untrusted: true,
|
|
186
|
-
source: "web_search",
|
|
187
|
-
provider: "searxng",
|
|
188
|
-
wrapped: true
|
|
189
|
-
},
|
|
190
|
-
results: results.map((result) => ({
|
|
191
|
-
title: wrapWebContent(result.title, "web_search"),
|
|
192
|
-
url: result.url,
|
|
193
|
-
snippet: result.content ? wrapWebContent(result.content, "web_search") : "",
|
|
194
|
-
siteName: resolveSiteName(result.url) || void 0,
|
|
195
|
-
img_src: result.img_src || void 0
|
|
196
|
-
}))
|
|
197
|
-
};
|
|
198
|
-
writeCache(SEARXNG_SEARCH_CACHE, cacheKey, payload, cacheTtlMs);
|
|
199
|
-
return payload;
|
|
200
|
-
}
|
|
201
|
-
const testing = { SEARXNG_SEARCH_CACHE };
|
|
202
|
-
//#endregion
|
|
203
|
-
export { runSearxngSearch, testing };
|