feat(web-search): add DuckDuckGo bundled plugin (#52629)

* feat(web-search): add DuckDuckGo bundled plugin

* chore(changelog): restore main changelog

* fix(web-search): harden DuckDuckGo challenge detection
This commit is contained in:
Vincent Koc
2026-03-22 22:05:33 -07:00
committed by GitHub
parent 827c441902
commit c6ca11e5a5
27 changed files with 1222 additions and 217 deletions

View File

@@ -7,7 +7,7 @@ import * as secretResolve from "./resolve.js";
import { createResolverContext } from "./runtime-shared.js";
import { resolveRuntimeWebTools } from "./runtime-web-tools.js";
type ProviderUnderTest = "brave" | "gemini" | "grok" | "kimi" | "perplexity";
type ProviderUnderTest = "brave" | "gemini" | "grok" | "kimi" | "perplexity" | "duckduckgo";
const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({
resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
@@ -36,6 +36,8 @@ function asConfig(value: unknown): OpenClawConfig {
function providerPluginId(provider: ProviderUnderTest): string {
switch (provider) {
case "duckduckgo":
return "duckduckgo";
case "gemini":
return "google";
case "grok":
@@ -81,13 +83,15 @@ function createTestProvider(params: {
id: params.provider,
label: params.provider,
hint: `${params.provider} test provider`,
envVars: [`${params.provider.toUpperCase()}_API_KEY`],
placeholder: `${params.provider}-...`,
requiresCredential: params.provider === "duckduckgo" ? false : undefined,
envVars: params.provider === "duckduckgo" ? [] : [`${params.provider.toUpperCase()}_API_KEY`],
placeholder: params.provider === "duckduckgo" ? "(no key needed)" : `${params.provider}-...`,
signupUrl: `https://example.com/${params.provider}`,
autoDetectOrder: params.order,
credentialPath,
inactiveSecretPaths: [credentialPath],
getCredentialValue: (searchConfig) => searchConfig?.apiKey,
credentialPath: params.provider === "duckduckgo" ? "" : credentialPath,
inactiveSecretPaths: params.provider === "duckduckgo" ? [] : [credentialPath],
getCredentialValue: (searchConfig) =>
params.provider === "duckduckgo" ? "duckduckgo-no-key-needed" : searchConfig?.apiKey,
setCredentialValue: (searchConfigTarget, value) => {
searchConfigTarget.apiKey = value;
},
@@ -117,6 +121,7 @@ function buildTestWebSearchProviders(): PluginWebSearchProviderEntry[] {
createTestProvider({ provider: "grok", pluginId: "xai", order: 30 }),
createTestProvider({ provider: "kimi", pluginId: "moonshot", order: 40 }),
createTestProvider({ provider: "perplexity", pluginId: "perplexity", order: 50 }),
createTestProvider({ provider: "duckduckgo", pluginId: "duckduckgo", order: 100 }),
];
}
@@ -231,6 +236,31 @@ describe("runtime web tools resolution", () => {
expect(metadata.fetch.firecrawl.apiKeySource).toBe("env");
});
it("auto-selects a keyless provider when no credentials are configured", async () => {
const { metadata } = await runRuntimeWebTools({
config: asConfig({
tools: {
web: {
search: {
enabled: true,
},
},
},
}),
});
expect(metadata.search.selectedProvider).toBe("duckduckgo");
expect(metadata.search.providerSource).toBe("auto-detect");
expect(metadata.search.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "WEB_SEARCH_AUTODETECT_SELECTED",
message: expect.stringContaining('keyless provider "duckduckgo"'),
}),
]),
);
});
it.each([
{
provider: "brave" as const,