diff --git a/src/commands/onboard-search.ts b/src/commands/onboard-search.ts index 0d414017c31..2047328433f 100644 --- a/src/commands/onboard-search.ts +++ b/src/commands/onboard-search.ts @@ -7,7 +7,7 @@ import { normalizeSecretInputString, } from "../config/types.secrets.js"; import type { PluginWebSearchProviderEntry } from "../plugins/types.js"; -import { resolvePluginWebSearchProviders } from "../plugins/web-search-providers.js"; +import { resolvePluginWebSearchProviders } from "../plugins/web-search-providers.runtime.js"; import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import type { SecretInputMode } from "./onboard-types.js"; diff --git a/src/config/config.web-search-provider.test.ts b/src/config/config.web-search-provider.test.ts index d89d913fcba..decb5e68e3b 100644 --- a/src/config/config.web-search-provider.test.ts +++ b/src/config/config.web-search-provider.test.ts @@ -16,6 +16,57 @@ vi.mock("../plugins/web-search-providers.js", () => { | undefined )?.entries?.[pluginId]?.config?.webSearch?.apiKey; return { + resolveBundledPluginWebSearchProviders: () => [ + { + id: "brave", + envVars: ["BRAVE_API_KEY"], + credentialPath: "plugins.entries.brave.config.webSearch.apiKey", + getCredentialValue: (search?: Record) => search?.apiKey, + getConfiguredCredentialValue: getConfigured("brave"), + }, + { + id: "firecrawl", + envVars: ["FIRECRAWL_API_KEY"], + credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey", + getCredentialValue: getScoped("firecrawl"), + getConfiguredCredentialValue: getConfigured("firecrawl"), + }, + { + id: "gemini", + envVars: ["GEMINI_API_KEY"], + credentialPath: "plugins.entries.google.config.webSearch.apiKey", + getCredentialValue: getScoped("gemini"), + getConfiguredCredentialValue: getConfigured("google"), + }, + { + id: "grok", + envVars: ["XAI_API_KEY"], + credentialPath: "plugins.entries.xai.config.webSearch.apiKey", + getCredentialValue: getScoped("grok"), + getConfiguredCredentialValue: getConfigured("xai"), + }, + { + id: "kimi", + envVars: ["KIMI_API_KEY", "MOONSHOT_API_KEY"], + credentialPath: "plugins.entries.moonshot.config.webSearch.apiKey", + getCredentialValue: getScoped("kimi"), + getConfiguredCredentialValue: getConfigured("moonshot"), + }, + { + id: "perplexity", + envVars: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"], + credentialPath: "plugins.entries.perplexity.config.webSearch.apiKey", + getCredentialValue: getScoped("perplexity"), + getConfiguredCredentialValue: getConfigured("perplexity"), + }, + { + id: "tavily", + envVars: ["TAVILY_API_KEY"], + credentialPath: "plugins.entries.tavily.config.webSearch.apiKey", + getCredentialValue: getScoped("tavily"), + getConfiguredCredentialValue: getConfigured("tavily"), + }, + ], resolvePluginWebSearchProviders: () => [ { id: "brave", diff --git a/src/plugins/web-search-providers.runtime.test.ts b/src/plugins/web-search-providers.runtime.test.ts new file mode 100644 index 00000000000..27478cbb1a1 --- /dev/null +++ b/src/plugins/web-search-providers.runtime.test.ts @@ -0,0 +1,129 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createEmptyPluginRegistry } from "./registry.js"; +import { setActivePluginRegistry } from "./runtime.js"; +import { + resolvePluginWebSearchProviders, + resolveRuntimeWebSearchProviders, +} from "./web-search-providers.runtime.js"; + +const BUNDLED_WEB_SEARCH_PROVIDERS = [ + { pluginId: "brave", id: "brave", order: 10 }, + { pluginId: "google", id: "gemini", order: 20 }, + { pluginId: "xai", id: "grok", order: 30 }, + { pluginId: "moonshot", id: "kimi", order: 40 }, + { pluginId: "perplexity", id: "perplexity", order: 50 }, + { pluginId: "firecrawl", id: "firecrawl", order: 60 }, + { pluginId: "tavily", id: "tavily", order: 70 }, +] as const; + +const { loadOpenClawPluginsMock } = vi.hoisted(() => ({ + loadOpenClawPluginsMock: vi.fn((params?: { config?: { plugins?: Record } }) => { + const plugins = params?.config?.plugins as + | { + enabled?: boolean; + allow?: string[]; + entries?: Record; + } + | undefined; + if (plugins?.enabled === false) { + return { webSearchProviders: [] }; + } + const allow = Array.isArray(plugins?.allow) && plugins.allow.length > 0 ? plugins.allow : null; + const entries = plugins?.entries ?? {}; + const webSearchProviders = BUNDLED_WEB_SEARCH_PROVIDERS.filter((provider) => { + if (allow && !allow.includes(provider.pluginId)) { + return false; + } + if (entries[provider.pluginId]?.enabled === false) { + return false; + } + return true; + }).map((provider) => ({ + pluginId: provider.pluginId, + pluginName: provider.pluginId, + source: "test" as const, + provider: { + id: provider.id, + label: provider.id, + hint: `${provider.id} provider`, + envVars: [`${provider.id.toUpperCase()}_API_KEY`], + placeholder: `${provider.id}-...`, + signupUrl: `https://example.com/${provider.id}`, + autoDetectOrder: provider.order, + credentialPath: `plugins.entries.${provider.pluginId}.config.webSearch.apiKey`, + getCredentialValue: () => "configured", + setCredentialValue: () => {}, + createTool: () => ({ + description: provider.id, + parameters: {}, + execute: async () => ({}), + }), + }, + })); + return { webSearchProviders }; + }), +})); + +vi.mock("./loader.js", () => ({ + loadOpenClawPlugins: loadOpenClawPluginsMock, +})); + +describe("resolvePluginWebSearchProviders", () => { + beforeEach(() => { + loadOpenClawPluginsMock.mockClear(); + setActivePluginRegistry(createEmptyPluginRegistry()); + }); + + afterEach(() => { + setActivePluginRegistry(createEmptyPluginRegistry()); + }); + + it("loads bundled providers through the plugin loader in auto-detect order", () => { + const providers = resolvePluginWebSearchProviders({}); + + expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([ + "brave:brave", + "google:gemini", + "xai:grok", + "moonshot:kimi", + "perplexity:perplexity", + "firecrawl:firecrawl", + "tavily:tavily", + ]); + expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1); + }); + + it("prefers the active plugin registry for runtime resolution", () => { + const registry = createEmptyPluginRegistry(); + registry.webSearchProviders.push({ + pluginId: "custom-search", + pluginName: "Custom Search", + provider: { + id: "custom", + label: "Custom Search", + hint: "Custom runtime provider", + envVars: ["CUSTOM_SEARCH_API_KEY"], + placeholder: "custom-...", + signupUrl: "https://example.com/signup", + autoDetectOrder: 1, + credentialPath: "tools.web.search.custom.apiKey", + getCredentialValue: () => "configured", + setCredentialValue: () => {}, + createTool: () => ({ + description: "custom", + parameters: {}, + execute: async () => ({}), + }), + }, + source: "test", + }); + setActivePluginRegistry(registry); + + const providers = resolveRuntimeWebSearchProviders({}); + + expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([ + "custom-search:custom", + ]); + expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/web-search-providers.runtime.ts b/src/plugins/web-search-providers.runtime.ts new file mode 100644 index 00000000000..494936d9857 --- /dev/null +++ b/src/plugins/web-search-providers.runtime.ts @@ -0,0 +1,56 @@ +import { createSubsystemLogger } from "../logging/subsystem.js"; +import { loadOpenClawPlugins } from "./loader.js"; +import type { PluginLoadOptions } from "./loader.js"; +import { createPluginLoaderLogger } from "./logger.js"; +import { getActivePluginRegistry } from "./runtime.js"; +import type { PluginWebSearchProviderEntry } from "./types.js"; +import { + resolveBundledWebSearchResolutionConfig, + sortWebSearchProviders, +} from "./web-search-providers.shared.js"; + +const log = createSubsystemLogger("plugins"); + +export function resolvePluginWebSearchProviders(params: { + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; + bundledAllowlistCompat?: boolean; + activate?: boolean; + cache?: boolean; +}): PluginWebSearchProviderEntry[] { + const { config } = resolveBundledWebSearchResolutionConfig(params); + const registry = loadOpenClawPlugins({ + config, + workspaceDir: params.workspaceDir, + env: params.env, + cache: params.cache ?? false, + activate: params.activate ?? false, + logger: createPluginLoaderLogger(log), + }); + + return sortWebSearchProviders( + registry.webSearchProviders.map((entry) => ({ + ...entry.provider, + pluginId: entry.pluginId, + })), + ); +} + +export function resolveRuntimeWebSearchProviders(params: { + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; + bundledAllowlistCompat?: boolean; +}): PluginWebSearchProviderEntry[] { + const runtimeProviders = getActivePluginRegistry()?.webSearchProviders ?? []; + if (runtimeProviders.length > 0) { + return sortWebSearchProviders( + runtimeProviders.map((entry) => ({ + ...entry.provider, + pluginId: entry.pluginId, + })), + ); + } + return resolvePluginWebSearchProviders(params); +} diff --git a/src/plugins/web-search-providers.shared.ts b/src/plugins/web-search-providers.shared.ts new file mode 100644 index 00000000000..29ba9527590 --- /dev/null +++ b/src/plugins/web-search-providers.shared.ts @@ -0,0 +1,120 @@ +import { + withBundledPluginAllowlistCompat, + withBundledPluginEnablementCompat, +} from "./bundled-compat.js"; +import { resolveBundledWebSearchPluginIds } from "./bundled-web-search.js"; +import { normalizePluginsConfig, type NormalizedPluginsConfig } from "./config-state.js"; +import type { PluginLoadOptions } from "./loader.js"; +import type { PluginWebSearchProviderEntry } from "./types.js"; + +export function hasExplicitPluginConfig(config: PluginLoadOptions["config"]): boolean { + const plugins = config?.plugins; + if (!plugins) { + return false; + } + if (typeof plugins.enabled === "boolean") { + return true; + } + if (Array.isArray(plugins.allow) && plugins.allow.length > 0) { + return true; + } + if (Array.isArray(plugins.deny) && plugins.deny.length > 0) { + return true; + } + if (Array.isArray(plugins.load?.paths) && plugins.load.paths.length > 0) { + return true; + } + if (plugins.entries && Object.keys(plugins.entries).length > 0) { + return true; + } + if (plugins.slots && Object.keys(plugins.slots).length > 0) { + return true; + } + return false; +} + +function resolveBundledWebSearchCompatPluginIds(params: { + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; +}): string[] { + return resolveBundledWebSearchPluginIds({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }); +} + +function withBundledWebSearchVitestCompat(params: { + config: PluginLoadOptions["config"]; + pluginIds: readonly string[]; + env?: PluginLoadOptions["env"]; +}): PluginLoadOptions["config"] { + const env = params.env ?? process.env; + const isVitest = Boolean(env.VITEST || process.env.VITEST); + if (!isVitest || hasExplicitPluginConfig(params.config) || params.pluginIds.length === 0) { + return params.config; + } + + return { + ...params.config, + plugins: { + ...params.config?.plugins, + enabled: true, + allow: [...params.pluginIds], + slots: { + ...params.config?.plugins?.slots, + memory: "none", + }, + }, + }; +} + +export function sortWebSearchProviders( + providers: PluginWebSearchProviderEntry[], +): PluginWebSearchProviderEntry[] { + return providers.toSorted((a, b) => { + const aOrder = a.autoDetectOrder ?? Number.MAX_SAFE_INTEGER; + const bOrder = b.autoDetectOrder ?? Number.MAX_SAFE_INTEGER; + if (aOrder !== bOrder) { + return aOrder - bOrder; + } + return a.id.localeCompare(b.id); + }); +} + +export function resolveBundledWebSearchResolutionConfig(params: { + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; + bundledAllowlistCompat?: boolean; +}): { + config: PluginLoadOptions["config"]; + normalized: NormalizedPluginsConfig; +} { + const bundledCompatPluginIds = resolveBundledWebSearchCompatPluginIds({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }); + const allowlistCompat = params.bundledAllowlistCompat + ? withBundledPluginAllowlistCompat({ + config: params.config, + pluginIds: bundledCompatPluginIds, + }) + : params.config; + const enablementCompat = withBundledPluginEnablementCompat({ + config: allowlistCompat, + pluginIds: bundledCompatPluginIds, + }); + const config = withBundledWebSearchVitestCompat({ + config: enablementCompat, + pluginIds: bundledCompatPluginIds, + env: params.env, + }); + + return { + config, + normalized: normalizePluginsConfig(config?.plugins), + }; +} diff --git a/src/plugins/web-search-providers.test.ts b/src/plugins/web-search-providers.test.ts index 87a4da1973c..85339014380 100644 --- a/src/plugins/web-search-providers.test.ts +++ b/src/plugins/web-search-providers.test.ts @@ -1,94 +1,9 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; -import { createEmptyPluginRegistry } from "./registry.js"; -import { setActivePluginRegistry } from "./runtime.js"; -import { - resolveBundledPluginWebSearchProviders, - resolvePluginWebSearchProviders, - resolveRuntimeWebSearchProviders, -} from "./web-search-providers.js"; - -const BUNDLED_WEB_SEARCH_PROVIDERS = [ - { pluginId: "brave", id: "brave", order: 10 }, - { pluginId: "google", id: "gemini", order: 20 }, - { pluginId: "xai", id: "grok", order: 30 }, - { pluginId: "moonshot", id: "kimi", order: 40 }, - { pluginId: "perplexity", id: "perplexity", order: 50 }, - { pluginId: "firecrawl", id: "firecrawl", order: 60 }, - { pluginId: "tavily", id: "tavily", order: 70 }, -] as const; - -const { loadOpenClawPluginsMock } = vi.hoisted(() => ({ - loadOpenClawPluginsMock: vi.fn((params?: { config?: { plugins?: Record } }) => { - const plugins = params?.config?.plugins as - | { - enabled?: boolean; - allow?: string[]; - entries?: Record; - } - | undefined; - if (plugins?.enabled === false) { - return { webSearchProviders: [] }; - } - const allow = Array.isArray(plugins?.allow) && plugins.allow.length > 0 ? plugins.allow : null; - const entries = plugins?.entries ?? {}; - const webSearchProviders = BUNDLED_WEB_SEARCH_PROVIDERS.filter((provider) => { - if (allow && !allow.includes(provider.pluginId)) { - return false; - } - if (entries[provider.pluginId]?.enabled === false) { - return false; - } - return true; - }).map((provider) => ({ - pluginId: provider.pluginId, - pluginName: provider.pluginId, - source: "test" as const, - provider: { - id: provider.id, - label: provider.id, - hint: `${provider.id} provider`, - envVars: [`${provider.id.toUpperCase()}_API_KEY`], - placeholder: `${provider.id}-...`, - signupUrl: `https://example.com/${provider.id}`, - autoDetectOrder: provider.order, - credentialPath: `plugins.entries.${provider.pluginId}.config.webSearch.apiKey`, - getCredentialValue: () => "configured", - setCredentialValue: () => {}, - applySelectionConfig: - provider.id === "firecrawl" ? (config: OpenClawConfig) => config : undefined, - resolveRuntimeMetadata: - provider.id === "perplexity" - ? () => ({ - perplexityTransport: "search_api" as const, - }) - : undefined, - createTool: () => ({ - description: provider.id, - parameters: {}, - execute: async () => ({}), - }), - }, - })); - return { webSearchProviders }; - }), -})); - -vi.mock("./loader.js", () => ({ - loadOpenClawPlugins: loadOpenClawPluginsMock, -})); - -describe("resolvePluginWebSearchProviders", () => { - beforeEach(() => { - loadOpenClawPluginsMock.mockClear(); - }); - - afterEach(() => { - setActivePluginRegistry(createEmptyPluginRegistry()); - }); +import { describe, expect, it } from "vitest"; +import { resolveBundledPluginWebSearchProviders } from "./web-search-providers.js"; +describe("resolveBundledPluginWebSearchProviders", () => { it("returns bundled providers in auto-detect order", () => { - const providers = resolvePluginWebSearchProviders({}); + const providers = resolveBundledPluginWebSearchProviders({}); expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([ "brave:brave", @@ -117,7 +32,7 @@ describe("resolvePluginWebSearchProviders", () => { }); it("can augment restrictive allowlists for bundled compatibility", () => { - const providers = resolvePluginWebSearchProviders({ + const providers = resolveBundledPluginWebSearchProviders({ config: { plugins: { allow: ["openrouter"], @@ -138,7 +53,7 @@ describe("resolvePluginWebSearchProviders", () => { }); it("does not return bundled providers excluded by a restrictive allowlist without compat", () => { - const providers = resolvePluginWebSearchProviders({ + const providers = resolveBundledPluginWebSearchProviders({ config: { plugins: { allow: ["openrouter"], @@ -150,7 +65,7 @@ describe("resolvePluginWebSearchProviders", () => { }); it("preserves explicit bundled provider entry state", () => { - const providers = resolvePluginWebSearchProviders({ + const providers = resolveBundledPluginWebSearchProviders({ config: { plugins: { entries: { @@ -164,7 +79,7 @@ describe("resolvePluginWebSearchProviders", () => { }); it("returns no providers when plugins are globally disabled", () => { - const providers = resolvePluginWebSearchProviders({ + const providers = resolveBundledPluginWebSearchProviders({ config: { plugins: { enabled: false, @@ -189,7 +104,6 @@ describe("resolvePluginWebSearchProviders", () => { "firecrawl:firecrawl", "tavily:tavily", ]); - expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); }); it("can scope bundled resolution to one plugin id", () => { @@ -210,39 +124,5 @@ describe("resolvePluginWebSearchProviders", () => { expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([ "google:gemini", ]); - expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); - }); - - it("prefers the active plugin registry for runtime resolution", () => { - const registry = createEmptyPluginRegistry(); - registry.webSearchProviders.push({ - pluginId: "custom-search", - pluginName: "Custom Search", - provider: { - id: "custom", - label: "Custom Search", - hint: "Custom runtime provider", - envVars: ["CUSTOM_SEARCH_API_KEY"], - placeholder: "custom-...", - signupUrl: "https://example.com/signup", - autoDetectOrder: 1, - credentialPath: "tools.web.search.custom.apiKey", - getCredentialValue: () => "configured", - setCredentialValue: () => {}, - createTool: () => ({ - description: "custom", - parameters: {}, - execute: async () => ({}), - }), - }, - source: "test", - }); - setActivePluginRegistry(registry); - - const providers = resolveRuntimeWebSearchProviders({}); - - expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([ - "custom-search:custom", - ]); }); }); diff --git a/src/plugins/web-search-providers.ts b/src/plugins/web-search-providers.ts index 81acd38c827..f61cdbd5362 100644 --- a/src/plugins/web-search-providers.ts +++ b/src/plugins/web-search-providers.ts @@ -1,135 +1,11 @@ -import { createSubsystemLogger } from "../logging/subsystem.js"; -import { - withBundledPluginAllowlistCompat, - withBundledPluginEnablementCompat, -} from "./bundled-compat.js"; -import { - listBundledWebSearchProviders as listBundledWebSearchProviderEntries, - resolveBundledWebSearchPluginIds, -} from "./bundled-web-search.js"; -import { - normalizePluginsConfig, - resolveEffectiveEnableState, - type NormalizedPluginsConfig, -} from "./config-state.js"; -import { loadOpenClawPlugins, type PluginLoadOptions } from "./loader.js"; -import { createPluginLoaderLogger } from "./logger.js"; -import { getActivePluginRegistry } from "./runtime.js"; +import { listBundledWebSearchProviders as listBundledWebSearchProviderEntries } from "./bundled-web-search.js"; +import { resolveEffectiveEnableState } from "./config-state.js"; +import type { PluginLoadOptions } from "./loader.js"; import type { PluginWebSearchProviderEntry } from "./types.js"; - -const log = createSubsystemLogger("plugins"); - -function hasExplicitPluginConfig(config: PluginLoadOptions["config"]): boolean { - const plugins = config?.plugins; - if (!plugins) { - return false; - } - if (typeof plugins.enabled === "boolean") { - return true; - } - if (Array.isArray(plugins.allow) && plugins.allow.length > 0) { - return true; - } - if (Array.isArray(plugins.deny) && plugins.deny.length > 0) { - return true; - } - if (Array.isArray(plugins.load?.paths) && plugins.load.paths.length > 0) { - return true; - } - if (plugins.entries && Object.keys(plugins.entries).length > 0) { - return true; - } - if (plugins.slots && Object.keys(plugins.slots).length > 0) { - return true; - } - return false; -} - -function resolveBundledWebSearchCompatPluginIds(params: { - config?: PluginLoadOptions["config"]; - workspaceDir?: string; - env?: PluginLoadOptions["env"]; -}): string[] { - return resolveBundledWebSearchPluginIds({ - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - }); -} - -function withBundledWebSearchVitestCompat(params: { - config: PluginLoadOptions["config"]; - pluginIds: readonly string[]; - env?: PluginLoadOptions["env"]; -}): PluginLoadOptions["config"] { - const env = params.env ?? process.env; - const isVitest = Boolean(env.VITEST || process.env.VITEST); - if (!isVitest || hasExplicitPluginConfig(params.config) || params.pluginIds.length === 0) { - return params.config; - } - - return { - ...params.config, - plugins: { - ...params.config?.plugins, - enabled: true, - allow: [...params.pluginIds], - slots: { - ...params.config?.plugins?.slots, - memory: "none", - }, - }, - }; -} - -function sortWebSearchProviders( - providers: PluginWebSearchProviderEntry[], -): PluginWebSearchProviderEntry[] { - return providers.toSorted((a, b) => { - const aOrder = a.autoDetectOrder ?? Number.MAX_SAFE_INTEGER; - const bOrder = b.autoDetectOrder ?? Number.MAX_SAFE_INTEGER; - if (aOrder !== bOrder) { - return aOrder - bOrder; - } - return a.id.localeCompare(b.id); - }); -} - -function resolveBundledWebSearchResolutionConfig(params: { - config?: PluginLoadOptions["config"]; - workspaceDir?: string; - env?: PluginLoadOptions["env"]; - bundledAllowlistCompat?: boolean; -}): { - config: PluginLoadOptions["config"]; - normalized: NormalizedPluginsConfig; -} { - const bundledCompatPluginIds = resolveBundledWebSearchCompatPluginIds({ - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - }); - const allowlistCompat = params.bundledAllowlistCompat - ? withBundledPluginAllowlistCompat({ - config: params.config, - pluginIds: bundledCompatPluginIds, - }) - : params.config; - const enablementCompat = withBundledPluginEnablementCompat({ - config: allowlistCompat, - pluginIds: bundledCompatPluginIds, - }); - const config = withBundledWebSearchVitestCompat({ - config: enablementCompat, - pluginIds: bundledCompatPluginIds, - env: params.env, - }); - - return { - config, - normalized: normalizePluginsConfig(config?.plugins), - }; -} +import { + resolveBundledWebSearchResolutionConfig, + sortWebSearchProviders, +} from "./web-search-providers.shared.js"; function listBundledWebSearchProviders(): PluginWebSearchProviderEntry[] { return sortWebSearchProviders(listBundledWebSearchProviderEntries()); @@ -158,47 +34,3 @@ export function resolveBundledPluginWebSearchProviders(params: { }).enabled; }); } - -export function resolvePluginWebSearchProviders(params: { - config?: PluginLoadOptions["config"]; - workspaceDir?: string; - env?: PluginLoadOptions["env"]; - bundledAllowlistCompat?: boolean; - activate?: boolean; - cache?: boolean; -}): PluginWebSearchProviderEntry[] { - const { config } = resolveBundledWebSearchResolutionConfig(params); - const registry = loadOpenClawPlugins({ - config, - workspaceDir: params.workspaceDir, - env: params.env, - cache: params.cache ?? false, - activate: params.activate ?? false, - logger: createPluginLoaderLogger(log), - }); - - return sortWebSearchProviders( - registry.webSearchProviders.map((entry) => ({ - ...entry.provider, - pluginId: entry.pluginId, - })), - ); -} - -export function resolveRuntimeWebSearchProviders(params: { - config?: PluginLoadOptions["config"]; - workspaceDir?: string; - env?: PluginLoadOptions["env"]; - bundledAllowlistCompat?: boolean; -}): PluginWebSearchProviderEntry[] { - const runtimeProviders = getActivePluginRegistry()?.webSearchProviders ?? []; - if (runtimeProviders.length > 0) { - return sortWebSearchProviders( - runtimeProviders.map((entry) => ({ - ...entry.provider, - pluginId: entry.pluginId, - })), - ); - } - return resolvePluginWebSearchProviders(params); -} diff --git a/src/secrets/runtime-web-tools.test.ts b/src/secrets/runtime-web-tools.test.ts index e0a78fc05cc..a091ffb11b8 100644 --- a/src/secrets/runtime-web-tools.test.ts +++ b/src/secrets/runtime-web-tools.test.ts @@ -1,7 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import type { PluginWebSearchProviderEntry } from "../plugins/types.js"; -import * as webSearchProviders from "../plugins/web-search-providers.js"; +import * as bundledWebSearchProviders from "../plugins/web-search-providers.js"; +import * as runtimeWebSearchProviders from "../plugins/web-search-providers.runtime.js"; import * as secretResolve from "./resolve.js"; import { createResolverContext } from "./runtime-shared.js"; import { resolveRuntimeWebTools } from "./runtime-web-tools.js"; @@ -18,6 +19,9 @@ const { resolveBundledPluginWebSearchProvidersMock } = vi.hoisted(() => ({ vi.mock("../plugins/web-search-providers.js", () => ({ resolveBundledPluginWebSearchProviders: resolveBundledPluginWebSearchProvidersMock, +})); + +vi.mock("../plugins/web-search-providers.runtime.js", () => ({ resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock, })); @@ -181,8 +185,8 @@ function expectInactiveFirecrawlSecretRef(params: { describe("runtime web tools resolution", () => { beforeEach(() => { - vi.mocked(webSearchProviders.resolvePluginWebSearchProviders).mockClear(); - vi.mocked(webSearchProviders.resolveBundledPluginWebSearchProviders).mockClear(); + vi.mocked(bundledWebSearchProviders.resolveBundledPluginWebSearchProviders).mockClear(); + vi.mocked(runtimeWebSearchProviders.resolvePluginWebSearchProviders).mockClear(); }); afterEach(() => { @@ -190,7 +194,7 @@ describe("runtime web tools resolution", () => { }); it("skips loading web search providers when search config is absent", async () => { - const providerSpy = vi.mocked(webSearchProviders.resolvePluginWebSearchProviders); + const providerSpy = vi.mocked(runtimeWebSearchProviders.resolvePluginWebSearchProviders); const { metadata } = await runRuntimeWebTools({ config: asConfig({ @@ -538,8 +542,8 @@ describe("runtime web tools resolution", () => { }); it("uses bundled provider resolution for configured bundled providers", async () => { - const bundledSpy = vi.mocked(webSearchProviders.resolveBundledPluginWebSearchProviders); - const genericSpy = vi.mocked(webSearchProviders.resolvePluginWebSearchProviders); + const bundledSpy = vi.mocked(bundledWebSearchProviders.resolveBundledPluginWebSearchProviders); + const genericSpy = vi.mocked(runtimeWebSearchProviders.resolvePluginWebSearchProviders); const { metadata } = await runRuntimeWebTools({ config: asConfig({ diff --git a/src/secrets/runtime-web-tools.ts b/src/secrets/runtime-web-tools.ts index 5c8993829ac..8794567f98b 100644 --- a/src/secrets/runtime-web-tools.ts +++ b/src/secrets/runtime-web-tools.ts @@ -8,10 +8,8 @@ import type { PluginWebSearchProviderEntry, WebSearchCredentialResolutionSource, } from "../plugins/types.js"; -import { - resolveBundledPluginWebSearchProviders, - resolvePluginWebSearchProviders, -} from "../plugins/web-search-providers.js"; +import { resolveBundledPluginWebSearchProviders } from "../plugins/web-search-providers.js"; +import { resolvePluginWebSearchProviders } from "../plugins/web-search-providers.runtime.js"; import { normalizeSecretInput } from "../utils/normalize-secret-input.js"; import { secretRefKey } from "./ref-contract.js"; import { resolveSecretRefValues } from "./resolve.js"; diff --git a/src/secrets/runtime.coverage.test.ts b/src/secrets/runtime.coverage.test.ts index 03576f946da..bce2911b88f 100644 --- a/src/secrets/runtime.coverage.test.ts +++ b/src/secrets/runtime.coverage.test.ts @@ -16,6 +16,9 @@ const { resolveBundledPluginWebSearchProvidersMock, resolvePluginWebSearchProvid vi.mock("../plugins/web-search-providers.js", () => ({ resolveBundledPluginWebSearchProviders: resolveBundledPluginWebSearchProvidersMock, +})); + +vi.mock("../plugins/web-search-providers.runtime.js", () => ({ resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock, })); diff --git a/src/secrets/runtime.test.ts b/src/secrets/runtime.test.ts index 12792f7c2f1..40824a522af 100644 --- a/src/secrets/runtime.test.ts +++ b/src/secrets/runtime.test.ts @@ -22,6 +22,9 @@ const { resolveBundledPluginWebSearchProvidersMock, resolvePluginWebSearchProvid vi.mock("../plugins/web-search-providers.js", () => ({ resolveBundledPluginWebSearchProviders: resolveBundledPluginWebSearchProvidersMock, +})); + +vi.mock("../plugins/web-search-providers.runtime.js", () => ({ resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock, })); diff --git a/src/web-search/runtime.ts b/src/web-search/runtime.ts index 06c56f1ec27..e19ba5d6a6e 100644 --- a/src/web-search/runtime.ts +++ b/src/web-search/runtime.ts @@ -5,10 +5,8 @@ import type { PluginWebSearchProviderEntry, WebSearchProviderToolDefinition, } from "../plugins/types.js"; -import { - resolvePluginWebSearchProviders, - resolveRuntimeWebSearchProviders, -} from "../plugins/web-search-providers.js"; +import { resolveBundledPluginWebSearchProviders } from "../plugins/web-search-providers.js"; +import { resolveRuntimeWebSearchProviders } from "../plugins/web-search-providers.runtime.js"; import type { RuntimeWebSearchMetadata } from "../secrets/runtime-web-tools.types.js"; import { normalizeSecretInput } from "../utils/normalize-secret-input.js"; @@ -97,7 +95,7 @@ export function resolveWebSearchProviderId(params: { }): string { const providers = params.providers ?? - resolvePluginWebSearchProviders({ + resolveBundledPluginWebSearchProviders({ config: params.config, bundledAllowlistCompat: true, }); @@ -142,7 +140,7 @@ export function resolveWebSearchDefinition( config: options?.config, bundledAllowlistCompat: true, }) - : resolvePluginWebSearchProviders({ + : resolveBundledPluginWebSearchProviders({ config: options?.config, bundledAllowlistCompat: true, })