diff --git a/extensions/lobster/src/lobster-tool.test.ts b/extensions/lobster/src/lobster-tool.test.ts index bde3767845c..21d090846b0 100644 --- a/extensions/lobster/src/lobster-tool.test.ts +++ b/extensions/lobster/src/lobster-tool.test.ts @@ -44,6 +44,7 @@ function fakeApi(overrides: Partial = {}): OpenClawPluginApi registerCli() {}, registerService() {}, registerProvider() {}, + registerWebSearchProvider() {}, registerInteractiveHandler() {}, registerHook() {}, registerHttpRoute() {}, diff --git a/extensions/test-utils/plugin-api.ts b/extensions/test-utils/plugin-api.ts index c2eaeced2e5..5c621700602 100644 --- a/extensions/test-utils/plugin-api.ts +++ b/extensions/test-utils/plugin-api.ts @@ -15,6 +15,7 @@ export function createTestPluginApi(api: TestPluginApiInput): OpenClawPluginApi registerCli() {}, registerService() {}, registerProvider() {}, + registerWebSearchProvider() {}, registerInteractiveHandler() {}, registerCommand() {}, registerContextEngine() {}, diff --git a/extensions/whatsapp/src/channel.ts b/extensions/whatsapp/src/channel.ts index 63c01bca05c..d73c951a054 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -58,12 +58,12 @@ const whatsappSetupWizardProxy = { cfg, }), resolveStatusLines: async ({ cfg, configured }) => - await ( + (await ( await loadWhatsAppChannelRuntime() ).whatsappSetupWizard.status.resolveStatusLines?.({ cfg, configured, - }), + })) ?? [], }, resolveShouldPromptAccountIds: (params) => (params.shouldPromptAccountIds || params.options?.promptWhatsAppAccountId) ?? false, diff --git a/src/agents/tools/web-search-plugin-factory.ts b/src/agents/tools/web-search-plugin-factory.ts index 8022b2e354d..ab80702a6ed 100644 --- a/src/agents/tools/web-search-plugin-factory.ts +++ b/src/agents/tools/web-search-plugin-factory.ts @@ -2,6 +2,10 @@ import type { OpenClawConfig } from "../../config/config.js"; import type { WebSearchProviderPlugin } from "../../plugins/types.js"; import { createWebSearchTool as createLegacyWebSearchTool } from "./web-search-core.js"; +type ConfiguredWebSearchProvider = NonNullable< + NonNullable["web"]>["search"] +>["provider"]; + function cloneWithDescriptors(value: T | undefined): T { const next = Object.create(Object.getPrototypeOf(value ?? {})) as T; if (value) { @@ -10,7 +14,10 @@ function cloneWithDescriptors(value: T | undefined): T { return next; } -function withForcedProvider(config: OpenClawConfig | undefined, provider: string): OpenClawConfig { +function withForcedProvider( + config: OpenClawConfig | undefined, + provider: ConfiguredWebSearchProvider, +): OpenClawConfig { const next = cloneWithDescriptors(config ?? {}); const tools = cloneWithDescriptors(next.tools ?? {}); const web = cloneWithDescriptors(tools.web ?? {}); @@ -25,7 +32,9 @@ function withForcedProvider(config: OpenClawConfig | undefined, provider: string } export function createPluginBackedWebSearchProvider( - provider: Omit, + provider: Omit & { + id: ConfiguredWebSearchProvider; + }, ): WebSearchProviderPlugin { return { ...provider, diff --git a/src/auto-reply/reply/route-reply.test.ts b/src/auto-reply/reply/route-reply.test.ts index ed507607c83..0a717f9bfc7 100644 --- a/src/auto-reply/reply/route-reply.test.ts +++ b/src/auto-reply/reply/route-reply.test.ts @@ -88,6 +88,7 @@ const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => enabled: true, })), providers: [], + webSearchProviders: [], gatewayHandlers: {}, httpRoutes: [], cliRegistrars: [], diff --git a/src/commands/configure.wizard.ts b/src/commands/configure.wizard.ts index 80af67043ab..6e5f0203be0 100644 --- a/src/commands/configure.wizard.ts +++ b/src/commands/configure.wizard.ts @@ -174,6 +174,10 @@ async function promptWebToolsConfig( hasKeyInEnv, } = await import("./onboard-search.js"); type SP = (typeof SEARCH_PROVIDER_OPTIONS)[number]["value"]; + const defaultProvider = SEARCH_PROVIDER_OPTIONS[0]?.value; + if (!defaultProvider) { + throw new Error("No web search providers are registered."); + } const hasKeyForProvider = (provider: string): boolean => { const entry = SEARCH_PROVIDER_OPTIONS.find((e) => e.value === provider); @@ -183,14 +187,13 @@ async function promptWebToolsConfig( return hasExistingKey(nextConfig, provider as SP) || hasKeyInEnv(entry); }; - const existingProvider: string = (() => { + const existingProvider: SP = (() => { const stored = existingSearch?.provider; if (stored && SEARCH_PROVIDER_OPTIONS.some((e) => e.value === stored)) { - return stored; + return stored as SP; } return ( - SEARCH_PROVIDER_OPTIONS.find((e) => hasKeyForProvider(e.value))?.value ?? - SEARCH_PROVIDER_OPTIONS[0].value + SEARCH_PROVIDER_OPTIONS.find((e) => hasKeyForProvider(e.value))?.value ?? defaultProvider ); })(); diff --git a/src/commands/onboard-search.ts b/src/commands/onboard-search.ts index d1281fe3fc7..af5f3cd9a8f 100644 --- a/src/commands/onboard-search.ts +++ b/src/commands/onboard-search.ts @@ -11,7 +11,21 @@ import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import type { SecretInputMode } from "./onboard-types.js"; -export type SearchProvider = string; +export type SearchProvider = NonNullable< + NonNullable["web"]>["search"]>["provider"] +>; + +const SEARCH_PROVIDER_IDS = ["brave", "gemini", "grok", "kimi", "perplexity"] as const; + +function isSearchProvider(value: string): value is SearchProvider { + return (SEARCH_PROVIDER_IDS as readonly string[]).includes(value); +} + +function hasSearchProviderId( + provider: T, +): provider is T & { id: SearchProvider } { + return isSearchProvider(provider.id); +} type SearchProviderEntry = { value: SearchProvider; @@ -25,14 +39,16 @@ type SearchProviderEntry = { export const SEARCH_PROVIDER_OPTIONS: readonly SearchProviderEntry[] = resolvePluginWebSearchProviders({ bundledAllowlistCompat: true, - }).map((provider) => ({ - value: provider.id, - label: provider.label, - hint: provider.hint, - envKeys: provider.envVars, - placeholder: provider.placeholder, - signupUrl: provider.signupUrl, - })); + }) + .filter(hasSearchProviderId) + .map((provider) => ({ + value: provider.id, + label: provider.label, + hint: provider.hint, + envKeys: provider.envVars, + placeholder: provider.placeholder, + signupUrl: provider.signupUrl, + })); export function hasKeyInEnv(entry: SearchProviderEntry): boolean { return entry.envKeys.some((k) => Boolean(process.env[k]?.trim())); @@ -178,7 +194,7 @@ export async function setupSearch( return SEARCH_PROVIDER_OPTIONS[0].value; })(); - type PickerValue = string; + type PickerValue = SearchProvider | "__skip__"; const choice = await prompter.select({ message: "Search provider", options: [ diff --git a/src/commands/onboarding/plugin-install.test.ts b/src/commands/onboarding/plugin-install.test.ts index d2c55d330c7..1cd9e530b86 100644 --- a/src/commands/onboarding/plugin-install.test.ts +++ b/src/commands/onboarding/plugin-install.test.ts @@ -335,6 +335,7 @@ describe("ensureOnboardingPluginInstalled", () => { hookNames: [], channelIds: [], providerIds: [], + webSearchProviderIds: [], gatewayMethods: [], cliCommands: [], services: [], diff --git a/src/config/test-helpers.ts b/src/config/test-helpers.ts index 69e7745a85b..5809a37da2d 100644 --- a/src/config/test-helpers.ts +++ b/src/config/test-helpers.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; +import type { OpenClawConfig } from "./config.js"; export async function withTempHome(fn: (home: string) => Promise): Promise { return withTempHomeBase(fn, { prefix: "openclaw-config-" }); @@ -53,7 +54,9 @@ export async function withEnvOverride( } export function buildWebSearchProviderConfig(params: { - provider: string; + provider: NonNullable< + NonNullable["web"]>["search"]>["provider"] + >; enabled?: boolean; providerConfig?: Record; }): Record { diff --git a/src/gateway/server-plugins.test.ts b/src/gateway/server-plugins.test.ts index 560392499c1..2db21cccde1 100644 --- a/src/gateway/server-plugins.test.ts +++ b/src/gateway/server-plugins.test.ts @@ -29,6 +29,7 @@ const createRegistry = (diagnostics: PluginDiagnostic[]): PluginRegistry => ({ channelSetups: [], commands: [], providers: [], + webSearchProviders: [], gatewayHandlers: {}, httpRoutes: [], cliRegistrars: [], diff --git a/src/gateway/server.agent.gateway-server-agent.mocks.ts b/src/gateway/server.agent.gateway-server-agent.mocks.ts index 0e1f779ef4f..acf507dbde2 100644 --- a/src/gateway/server.agent.gateway-server-agent.mocks.ts +++ b/src/gateway/server.agent.gateway-server-agent.mocks.ts @@ -11,6 +11,7 @@ export const registryState: { registry: PluginRegistry } = { channels: [], channelSetups: [], providers: [], + webSearchProviders: [], gatewayHandlers: {}, httpHandlers: [], httpRoutes: [], diff --git a/src/gateway/test-helpers.mocks.ts b/src/gateway/test-helpers.mocks.ts index 17868ae0bca..59ad8a9cedc 100644 --- a/src/gateway/test-helpers.mocks.ts +++ b/src/gateway/test-helpers.mocks.ts @@ -146,6 +146,7 @@ const createStubPluginRegistry = (): PluginRegistry => ({ ], channelSetups: [], providers: [], + webSearchProviders: [], gatewayHandlers: {}, httpRoutes: [], cliRegistrars: [], diff --git a/src/test-utils/channel-plugins.ts b/src/test-utils/channel-plugins.ts index ebec4f2c747..2af1191feba 100644 --- a/src/test-utils/channel-plugins.ts +++ b/src/test-utils/channel-plugins.ts @@ -25,6 +25,7 @@ export const createTestRegistry = (channels: TestChannelRegistration[] = []): Pl enabled: true, })), providers: [], + webSearchProviders: [], gatewayHandlers: {}, httpRoutes: [], cliRegistrars: [],