@openclaw/duckduckgo-plugin 2026.10.10 → 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/ddg-client-DoTF0OSq.mjs +122 -0
- package/dist/.setup/{ddg-search-provider-DRW7lyhV.mjs → ddg-search-provider-dPkRSeDL.mjs} +5 -13
- package/dist/.source-module-urls/56b99c5c7b8ddc9b15b6a35ec0062fd94a251bd7acf15a92605daaffd1348ca9.json +1 -1
- package/dist/index.js +1 -1
- package/dist/web-search-contract-api.js +1 -1
- package/dist/web-search-provider.js +1 -1
- package/package.json +4 -4
- package/dist/.setup/ddg-client-BtO8ZJQJ.mjs +0 -150
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { readPluginPackageVersion } from "openclaw/plugin-sdk/extension-shared";
|
|
3
|
+
import { decodeHtmlEntities } from "openclaw/plugin-sdk/html-entity-runtime";
|
|
4
|
+
import { ProviderHttpError, readProviderTextResponse } from "openclaw/plugin-sdk/provider-http";
|
|
5
|
+
import { DEFAULT_SEARCH_COUNT, externalContentStamp, readCachedSearchPayload, readResponseText, resolveSearchCacheTtlMs, resolveSearchCount, resolveSiteName, resolveTimeoutSeconds, withTrustedWebSearchEndpoint, wrapWebContent, writeCachedSearchPayload } from "openclaw/plugin-sdk/provider-web-search";
|
|
6
|
+
import { resolveProviderWebSearchPluginConfig } from "openclaw/plugin-sdk/provider-web-search-config-contract";
|
|
7
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
|
+
// extensions/duckduckgo/src/ddg-client.ts
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const DDG_USER_AGENT = `openclaw-duckduckgo/${readPluginPackageVersion({ require })} (+https://docs.openclaw.ai)`;
|
|
11
|
+
const DDG_HTML_ENDPOINT = "https://html.duckduckgo.com/html";
|
|
12
|
+
const DEFAULT_TIMEOUT_SECONDS = 20;
|
|
13
|
+
const DDG_HTML_ENTITY_RE = /&(?:lt|gt|quot|apos|#39|#x27|#x2F|nbsp|ndash|mdash|hellip|amp|#\d+|#x[0-9a-f]+);/gi;
|
|
14
|
+
const DDG_ENTITY_TEXT = {
|
|
15
|
+
"\xA0": " ",
|
|
16
|
+
"–": "-",
|
|
17
|
+
"—": "--",
|
|
18
|
+
"…": "..."
|
|
19
|
+
};
|
|
20
|
+
const DDG_SAFE_SEARCH_PARAM = {
|
|
21
|
+
strict: "1",
|
|
22
|
+
moderate: "-1",
|
|
23
|
+
off: "-2"
|
|
24
|
+
};
|
|
25
|
+
function decodeHtmlEntities$1(text) {
|
|
26
|
+
return text.replace(DDG_HTML_ENTITY_RE, (entity) => {
|
|
27
|
+
const decoded = decodeHtmlEntities(entity.startsWith("&#") ? entity : entity.toLowerCase());
|
|
28
|
+
return DDG_ENTITY_TEXT[decoded] ?? decoded;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function stripHtml(html) {
|
|
32
|
+
return html.replace(/<\/?b\b[^>]*>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
33
|
+
}
|
|
34
|
+
async function runDuckDuckGoSearch(params) {
|
|
35
|
+
const count = resolveSearchCount(params.count, DEFAULT_SEARCH_COUNT);
|
|
36
|
+
const region = params.region ?? normalizeOptionalString(resolveProviderWebSearchPluginConfig(params.config, "duckduckgo")?.region);
|
|
37
|
+
let safeSearch = params.safeSearch;
|
|
38
|
+
if (safeSearch == null) {
|
|
39
|
+
const normalized = normalizeLowercaseStringOrEmpty(resolveProviderWebSearchPluginConfig(params.config, "duckduckgo")?.safeSearch);
|
|
40
|
+
safeSearch = normalized === "strict" || normalized === "off" ? normalized : "moderate";
|
|
41
|
+
}
|
|
42
|
+
const timeoutSeconds = resolveTimeoutSeconds(params.timeoutSeconds, DEFAULT_TIMEOUT_SECONDS);
|
|
43
|
+
const cacheTtlMs = resolveSearchCacheTtlMs(params.config?.tools?.web?.search);
|
|
44
|
+
const cacheKey = JSON.stringify({
|
|
45
|
+
provider: "duckduckgo",
|
|
46
|
+
query: params.query,
|
|
47
|
+
count,
|
|
48
|
+
region: region ?? "",
|
|
49
|
+
safeSearch
|
|
50
|
+
});
|
|
51
|
+
const cached = readCachedSearchPayload(cacheKey, cacheTtlMs);
|
|
52
|
+
if (cached) return cached;
|
|
53
|
+
const url = new URL(DDG_HTML_ENDPOINT);
|
|
54
|
+
url.searchParams.set("q", params.query);
|
|
55
|
+
if (region) url.searchParams.set("kl", region);
|
|
56
|
+
url.searchParams.set("kp", DDG_SAFE_SEARCH_PARAM[safeSearch]);
|
|
57
|
+
const startedAt = Date.now();
|
|
58
|
+
const results = await withTrustedWebSearchEndpoint({
|
|
59
|
+
url: url.toString(),
|
|
60
|
+
timeoutSeconds,
|
|
61
|
+
signal: params.signal,
|
|
62
|
+
init: {
|
|
63
|
+
method: "GET",
|
|
64
|
+
headers: { "User-Agent": DDG_USER_AGENT }
|
|
65
|
+
}
|
|
66
|
+
}, async (response) => {
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const detail = (await readResponseText(response, { maxBytes: 64e3 })).text;
|
|
69
|
+
throw new ProviderHttpError(`DuckDuckGo search error (${response.status}): ${detail || response.statusText}`, { status: response.status });
|
|
70
|
+
}
|
|
71
|
+
const html = await readProviderTextResponse(response, "DuckDuckGo search");
|
|
72
|
+
if (!/class="[^"]*\bresult__a\b[^"]*"/i.test(html) && /g-recaptcha|are you a human|id="challenge-form"|name="challenge"/i.test(html)) throw new Error("DuckDuckGo returned a bot-detection challenge.");
|
|
73
|
+
const parsedResults = [];
|
|
74
|
+
const resultRegex = /<a\b(?=[^>]*\bclass="[^"]*\bresult__a\b[^"]*")([^>]*)>([\s\S]*?)<\/a>/gi;
|
|
75
|
+
const nextResultRegex = /<a\b(?=[^>]*\bclass="[^"]*\bresult__a\b[^"]*")[^>]*>/i;
|
|
76
|
+
const snippetRegex = /<a\b(?=[^>]*\bclass="[^"]*\bresult__snippet\b[^"]*")[^>]*>([\s\S]*?)<\/a>/i;
|
|
77
|
+
for (const match of html.matchAll(resultRegex)) {
|
|
78
|
+
const rawAttributes = match[1] ?? "";
|
|
79
|
+
const rawTitle = match[2] ?? "";
|
|
80
|
+
const rawUrl = /\bhref="([^"]*)"/i.exec(rawAttributes)?.[1] ?? "";
|
|
81
|
+
const matchEnd = (match.index ?? 0) + match[0].length;
|
|
82
|
+
const trailingHtml = html.slice(matchEnd);
|
|
83
|
+
const nextResultIndex = trailingHtml.search(nextResultRegex);
|
|
84
|
+
const scopedTrailingHtml = nextResultIndex >= 0 ? trailingHtml.slice(0, nextResultIndex) : trailingHtml;
|
|
85
|
+
const rawSnippet = snippetRegex.exec(scopedTrailingHtml)?.[1] ?? "";
|
|
86
|
+
const title = decodeHtmlEntities$1(stripHtml(rawTitle));
|
|
87
|
+
let resultUrl = decodeHtmlEntities$1(rawUrl);
|
|
88
|
+
try {
|
|
89
|
+
const normalized = resultUrl.startsWith("//") ? `https:${resultUrl}` : resultUrl;
|
|
90
|
+
const uddg = new URL(normalized).searchParams.get("uddg");
|
|
91
|
+
if (uddg) resultUrl = uddg;
|
|
92
|
+
} catch {}
|
|
93
|
+
const snippet = decodeHtmlEntities$1(stripHtml(rawSnippet));
|
|
94
|
+
if (title && resultUrl) {
|
|
95
|
+
parsedResults.push({
|
|
96
|
+
title,
|
|
97
|
+
url: resultUrl,
|
|
98
|
+
snippet
|
|
99
|
+
});
|
|
100
|
+
if (parsedResults.length >= count) break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return parsedResults;
|
|
104
|
+
});
|
|
105
|
+
params.signal?.throwIfAborted();
|
|
106
|
+
const payload = {
|
|
107
|
+
query: params.query,
|
|
108
|
+
provider: "duckduckgo",
|
|
109
|
+
count: results.length,
|
|
110
|
+
tookMs: Date.now() - startedAt,
|
|
111
|
+
externalContent: externalContentStamp("duckduckgo"),
|
|
112
|
+
results: results.map((result) => ({
|
|
113
|
+
title: wrapWebContent(result.title, "web_search"),
|
|
114
|
+
url: result.url,
|
|
115
|
+
snippet: result.snippet ? wrapWebContent(result.snippet, "web_search") : "",
|
|
116
|
+
siteName: resolveSiteName(result.url) || void 0
|
|
117
|
+
}))
|
|
118
|
+
};
|
|
119
|
+
writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
|
|
120
|
+
return payload;
|
|
121
|
+
}
|
|
122
|
+
export { runDuckDuckGoSearch };
|
|
@@ -1,21 +1,12 @@
|
|
|
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";
|
|
3
|
+
import { WEB_SEARCH_QUERY_COUNT_PROPERTIES, createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-contract";
|
|
4
4
|
// extensions/duckduckgo/src/ddg-search-provider.ts
|
|
5
|
-
const loadDuckDuckGoClientModule = createLazyRuntimeModule(() => import("./ddg-client-
|
|
5
|
+
const loadDuckDuckGoClientModule = createLazyRuntimeModule(() => import("./ddg-client-DoTF0OSq.mjs"));
|
|
6
6
|
const DuckDuckGoSearchSchema = {
|
|
7
7
|
type: "object",
|
|
8
8
|
properties: {
|
|
9
|
-
|
|
10
|
-
type: "string",
|
|
11
|
-
description: "Search query string."
|
|
12
|
-
},
|
|
13
|
-
count: {
|
|
14
|
-
type: "integer",
|
|
15
|
-
description: "Number of results to return (1-10).",
|
|
16
|
-
minimum: 1,
|
|
17
|
-
maximum: 10
|
|
18
|
-
},
|
|
9
|
+
...WEB_SEARCH_QUERY_COUNT_PROPERTIES,
|
|
19
10
|
region: {
|
|
20
11
|
type: "string",
|
|
21
12
|
description: "Optional DuckDuckGo region code such as us-en, uk-en, or de-de."
|
|
@@ -54,6 +45,7 @@ function createDuckDuckGoWebSearchProvider() {
|
|
|
54
45
|
execute: async (args, context) => {
|
|
55
46
|
context?.signal?.throwIfAborted();
|
|
56
47
|
const { runDuckDuckGoSearch } = await loadDuckDuckGoClientModule();
|
|
48
|
+
const safeSearch = readStringParam(args, "safeSearch");
|
|
57
49
|
const searchConfig = ctx.searchConfig;
|
|
58
50
|
return await runDuckDuckGoSearch({
|
|
59
51
|
config: ctx.config,
|
|
@@ -64,7 +56,7 @@ function createDuckDuckGoWebSearchProvider() {
|
|
|
64
56
|
}) ?? searchConfig?.maxResults,
|
|
65
57
|
timeoutSeconds: searchConfig?.timeoutSeconds,
|
|
66
58
|
region: readStringParam(args, "region"),
|
|
67
|
-
safeSearch:
|
|
59
|
+
safeSearch: safeSearch === "strict" || safeSearch === "moderate" || safeSearch === "off" ? safeSearch : void 0,
|
|
68
60
|
...context?.signal ? { signal: context.signal } : {}
|
|
69
61
|
});
|
|
70
62
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"entries":[{"output":"index.js","source":"extensions/duckduckgo/index.ts"},{"output":"web-search-contract-api.js","source":"extensions/duckduckgo/web-search-contract-api.ts"},{"output":"web-search-provider.js","source":"extensions/duckduckgo/web-search-provider.ts"}],"chunks":[{"output":".setup/ddg-client-
|
|
1
|
+
{"version":3,"entries":[{"output":"index.js","source":"extensions/duckduckgo/index.ts"},{"output":"web-search-contract-api.js","source":"extensions/duckduckgo/web-search-contract-api.ts"},{"output":"web-search-provider.js","source":"extensions/duckduckgo/web-search-provider.ts"}],"chunks":[{"output":".setup/ddg-client-DoTF0OSq.mjs","sha256":"1f7f5ac52f08da9f333fc9d9e09c3ff6993ccfc055adfb255e205e772d1aac7b","facadeSource":"extensions/duckduckgo/src/ddg-client.ts","sources":["extensions/duckduckgo/src/ddg-client.ts"]},{"output":".setup/ddg-search-provider-dPkRSeDL.mjs","sha256":"f13b7eb7f8d724d4958b7b7ed2a266b22eadacc14d14cd4eb19e5ba5f9f0c5c1","facadeSource":null,"sources":["extensions/duckduckgo/src/ddg-search-provider.ts"]},{"output":"index.js","sha256":"25d7f28cbc889799ee744904d7f62326557f2e7b3bc1749af3080740dd96d356","facadeSource":"extensions/duckduckgo/index.ts","sources":["extensions/duckduckgo/index.ts"]},{"output":"web-search-contract-api.js","sha256":"de98f62523f6b143688c9296911fb33c8bfe84fdcbf3e041618e1c75ec52b87d","facadeSource":"extensions/duckduckgo/web-search-contract-api.ts","sources":[]},{"output":"web-search-provider.js","sha256":"05a4b25f3e9b95ae20209f5343c971d90e4164e615835595af378987a14368e1","facadeSource":"extensions/duckduckgo/web-search-provider.ts","sources":[]}],"unrenderedSources":["extensions/duckduckgo/web-search-contract-api.ts","extensions/duckduckgo/web-search-provider.ts"]}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as createDuckDuckGoWebSearchProvider } from "./.setup/ddg-search-provider-
|
|
1
|
+
import { t as createDuckDuckGoWebSearchProvider } from "./.setup/ddg-search-provider-dPkRSeDL.mjs";
|
|
2
2
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
3
|
// extensions/duckduckgo/index.ts
|
|
4
4
|
var duckduckgo_default = definePluginEntry({
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { t as createDuckDuckGoWebSearchProvider } from "./.setup/ddg-search-provider-
|
|
1
|
+
export { t as createDuckDuckGoWebSearchProvider } from "./.setup/ddg-search-provider-dPkRSeDL.mjs";
|
|
2
2
|
import "./web-search-provider.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as createDuckDuckGoWebSearchProvider } from "./.setup/ddg-search-provider-
|
|
1
|
+
export { t as createDuckDuckGoWebSearchProvider } from "./.setup/ddg-search-provider-dPkRSeDL.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/duckduckgo-plugin",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.11.5-beta.1",
|
|
4
4
|
"description": "OpenClaw DuckDuckGo plugin.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
"minHostVersion": ">=2026.7.2"
|
|
19
19
|
},
|
|
20
20
|
"compat": {
|
|
21
|
-
"pluginApi": ">=2026.
|
|
21
|
+
"pluginApi": ">=2026.11.5-beta.1"
|
|
22
22
|
},
|
|
23
23
|
"build": {
|
|
24
|
-
"openclawVersion": "2026.
|
|
24
|
+
"openclawVersion": "2026.11.5-beta.1",
|
|
25
25
|
"bundledDist": false
|
|
26
26
|
},
|
|
27
27
|
"release": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"assets/activity.svg"
|
|
41
41
|
],
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"openclaw": ">=2026.
|
|
43
|
+
"openclaw": ">=2026.11.5-beta.1"
|
|
44
44
|
},
|
|
45
45
|
"peerDependenciesMeta": {
|
|
46
46
|
"openclaw": {
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import { readPluginPackageVersion } from "openclaw/plugin-sdk/extension-shared";
|
|
3
|
-
import { decodeHtmlEntities } from "openclaw/plugin-sdk/html-entity-runtime";
|
|
4
|
-
import { ProviderHttpError, readProviderTextResponse } from "openclaw/plugin-sdk/provider-http";
|
|
5
|
-
import { DEFAULT_CACHE_TTL_MINUTES, DEFAULT_SEARCH_COUNT, normalizeCacheKey, readCache, readResponseText, resolveCacheTtlMs, resolveSearchCount, resolveSiteName, resolveTimeoutSeconds, withTrustedWebSearchEndpoint, wrapWebContent, writeCache } from "openclaw/plugin-sdk/provider-web-search";
|
|
6
|
-
import { asOptionalRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
|
-
// extensions/duckduckgo/src/config.ts
|
|
8
|
-
const DEFAULT_DDG_SAFE_SEARCH = "moderate";
|
|
9
|
-
function resolveDdgWebSearchConfig(config) {
|
|
10
|
-
return asOptionalRecord(config?.plugins?.entries?.duckduckgo?.config?.webSearch);
|
|
11
|
-
}
|
|
12
|
-
function resolveDdgRegion(config) {
|
|
13
|
-
return normalizeOptionalString(resolveDdgWebSearchConfig(config)?.region);
|
|
14
|
-
}
|
|
15
|
-
function resolveDdgSafeSearch(config) {
|
|
16
|
-
const safeSearch = resolveDdgWebSearchConfig(config)?.safeSearch;
|
|
17
|
-
const normalized = normalizeLowercaseStringOrEmpty(safeSearch);
|
|
18
|
-
if (normalized === "strict" || normalized === "off") return normalized;
|
|
19
|
-
return DEFAULT_DDG_SAFE_SEARCH;
|
|
20
|
-
}
|
|
21
|
-
// extensions/duckduckgo/src/ddg-client.ts
|
|
22
|
-
const require = createRequire(import.meta.url);
|
|
23
|
-
const DDG_USER_AGENT = `openclaw-duckduckgo/${readPluginPackageVersion({ require })} (+https://docs.openclaw.ai)`;
|
|
24
|
-
const DDG_HTML_ENDPOINT = "https://html.duckduckgo.com/html";
|
|
25
|
-
const DEFAULT_TIMEOUT_SECONDS = 20;
|
|
26
|
-
const DDG_HTML_ENTITY_RE = /&(?:lt|gt|quot|apos|#39|#x27|#x2F|nbsp|ndash|mdash|hellip|amp|#\d+|#x[0-9a-f]+);/gi;
|
|
27
|
-
const DDG_ENTITY_TEXT = {
|
|
28
|
-
"\xA0": " ",
|
|
29
|
-
"–": "-",
|
|
30
|
-
"—": "--",
|
|
31
|
-
"…": "..."
|
|
32
|
-
};
|
|
33
|
-
const DDG_SAFE_SEARCH_PARAM = {
|
|
34
|
-
strict: "1",
|
|
35
|
-
moderate: "-1",
|
|
36
|
-
off: "-2"
|
|
37
|
-
};
|
|
38
|
-
const DDG_SEARCH_CACHE = /* @__PURE__ */ new Map();
|
|
39
|
-
function decodeHtmlEntities$1(text) {
|
|
40
|
-
return text.replace(DDG_HTML_ENTITY_RE, (entity) => {
|
|
41
|
-
const decoded = decodeHtmlEntities(entity.startsWith("&#") ? entity : entity.toLowerCase());
|
|
42
|
-
return DDG_ENTITY_TEXT[decoded] ?? decoded;
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
function stripHtml(html) {
|
|
46
|
-
return html.replace(/<\/?b\b[^>]*>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
47
|
-
}
|
|
48
|
-
function decodeDuckDuckGoUrl(rawUrl) {
|
|
49
|
-
try {
|
|
50
|
-
const normalized = rawUrl.startsWith("//") ? `https:${rawUrl}` : rawUrl;
|
|
51
|
-
const uddg = new URL(normalized).searchParams.get("uddg");
|
|
52
|
-
if (uddg) return uddg;
|
|
53
|
-
} catch {}
|
|
54
|
-
return rawUrl;
|
|
55
|
-
}
|
|
56
|
-
function isBotChallenge(html) {
|
|
57
|
-
if (/class="[^"]*\bresult__a\b[^"]*"/i.test(html)) return false;
|
|
58
|
-
return /g-recaptcha|are you a human|id="challenge-form"|name="challenge"/i.test(html);
|
|
59
|
-
}
|
|
60
|
-
function parseDuckDuckGoHtml(html, count) {
|
|
61
|
-
const results = [];
|
|
62
|
-
const resultRegex = /<a\b(?=[^>]*\bclass="[^"]*\bresult__a\b[^"]*")([^>]*)>([\s\S]*?)<\/a>/gi;
|
|
63
|
-
const nextResultRegex = /<a\b(?=[^>]*\bclass="[^"]*\bresult__a\b[^"]*")[^>]*>/i;
|
|
64
|
-
const snippetRegex = /<a\b(?=[^>]*\bclass="[^"]*\bresult__snippet\b[^"]*")[^>]*>([\s\S]*?)<\/a>/i;
|
|
65
|
-
for (const match of html.matchAll(resultRegex)) {
|
|
66
|
-
const rawAttributes = match[1] ?? "";
|
|
67
|
-
const rawTitle = match[2] ?? "";
|
|
68
|
-
const rawUrl = /\bhref="([^"]*)"/i.exec(rawAttributes)?.[1] ?? "";
|
|
69
|
-
const matchEnd = (match.index ?? 0) + match[0].length;
|
|
70
|
-
const trailingHtml = html.slice(matchEnd);
|
|
71
|
-
const nextResultIndex = trailingHtml.search(nextResultRegex);
|
|
72
|
-
const scopedTrailingHtml = nextResultIndex >= 0 ? trailingHtml.slice(0, nextResultIndex) : trailingHtml;
|
|
73
|
-
const rawSnippet = snippetRegex.exec(scopedTrailingHtml)?.[1] ?? "";
|
|
74
|
-
const title = decodeHtmlEntities$1(stripHtml(rawTitle));
|
|
75
|
-
const url = decodeDuckDuckGoUrl(decodeHtmlEntities$1(rawUrl));
|
|
76
|
-
const snippet = decodeHtmlEntities$1(stripHtml(rawSnippet));
|
|
77
|
-
if (title && url) {
|
|
78
|
-
results.push({
|
|
79
|
-
title,
|
|
80
|
-
url,
|
|
81
|
-
snippet
|
|
82
|
-
});
|
|
83
|
-
if (results.length >= count) break;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return results;
|
|
87
|
-
}
|
|
88
|
-
async function runDuckDuckGoSearch(params) {
|
|
89
|
-
const count = resolveSearchCount(params.count, DEFAULT_SEARCH_COUNT);
|
|
90
|
-
const region = params.region ?? resolveDdgRegion(params.config);
|
|
91
|
-
const safeSearch = params.safeSearch === "strict" || params.safeSearch === "moderate" || params.safeSearch === "off" ? params.safeSearch : resolveDdgSafeSearch(params.config);
|
|
92
|
-
const timeoutSeconds = resolveTimeoutSeconds(params.timeoutSeconds, DEFAULT_TIMEOUT_SECONDS);
|
|
93
|
-
const cacheTtlMs = resolveCacheTtlMs(params.cacheTtlMinutes ?? params.config?.tools?.web?.search?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES);
|
|
94
|
-
const cacheKey = normalizeCacheKey(JSON.stringify({
|
|
95
|
-
provider: "duckduckgo",
|
|
96
|
-
query: params.query,
|
|
97
|
-
count,
|
|
98
|
-
region: region ?? "",
|
|
99
|
-
safeSearch
|
|
100
|
-
}));
|
|
101
|
-
const cached = readCache(DDG_SEARCH_CACHE, cacheKey, cacheTtlMs);
|
|
102
|
-
if (cached) return {
|
|
103
|
-
...cached.value,
|
|
104
|
-
cached: true
|
|
105
|
-
};
|
|
106
|
-
const url = new URL(DDG_HTML_ENDPOINT);
|
|
107
|
-
url.searchParams.set("q", params.query);
|
|
108
|
-
if (region) url.searchParams.set("kl", region);
|
|
109
|
-
url.searchParams.set("kp", DDG_SAFE_SEARCH_PARAM[safeSearch]);
|
|
110
|
-
const startedAt = Date.now();
|
|
111
|
-
const results = await withTrustedWebSearchEndpoint({
|
|
112
|
-
url: url.toString(),
|
|
113
|
-
timeoutSeconds,
|
|
114
|
-
signal: params.signal,
|
|
115
|
-
init: {
|
|
116
|
-
method: "GET",
|
|
117
|
-
headers: { "User-Agent": DDG_USER_AGENT }
|
|
118
|
-
}
|
|
119
|
-
}, async (response) => {
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
const detail = (await readResponseText(response, { maxBytes: 64e3 })).text;
|
|
122
|
-
throw new ProviderHttpError(`DuckDuckGo search error (${response.status}): ${detail || response.statusText}`, { status: response.status });
|
|
123
|
-
}
|
|
124
|
-
const html = await readProviderTextResponse(response, "DuckDuckGo search");
|
|
125
|
-
if (isBotChallenge(html)) throw new Error("DuckDuckGo returned a bot-detection challenge.");
|
|
126
|
-
return parseDuckDuckGoHtml(html, count);
|
|
127
|
-
});
|
|
128
|
-
params.signal?.throwIfAborted();
|
|
129
|
-
const payload = {
|
|
130
|
-
query: params.query,
|
|
131
|
-
provider: "duckduckgo",
|
|
132
|
-
count: results.length,
|
|
133
|
-
tookMs: Date.now() - startedAt,
|
|
134
|
-
externalContent: {
|
|
135
|
-
untrusted: true,
|
|
136
|
-
source: "web_search",
|
|
137
|
-
provider: "duckduckgo",
|
|
138
|
-
wrapped: true
|
|
139
|
-
},
|
|
140
|
-
results: results.map((result) => ({
|
|
141
|
-
title: wrapWebContent(result.title, "web_search"),
|
|
142
|
-
url: result.url,
|
|
143
|
-
snippet: result.snippet ? wrapWebContent(result.snippet, "web_search") : "",
|
|
144
|
-
siteName: resolveSiteName(result.url) || void 0
|
|
145
|
-
}))
|
|
146
|
-
};
|
|
147
|
-
writeCache(DDG_SEARCH_CACHE, cacheKey, payload, cacheTtlMs);
|
|
148
|
-
return payload;
|
|
149
|
-
}
|
|
150
|
-
export { runDuckDuckGoSearch };
|