mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 14:45:46 +00:00
feat(plugins): derive bundled web search providers from plugins
This commit is contained in:
@@ -1,36 +1,27 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withBundledPluginAllowlistCompat } from "../bundled-compat.js";
|
||||
import { __testing as providerTesting } from "../providers.js";
|
||||
import { resolvePluginWebSearchProviders } from "../web-search-providers.js";
|
||||
import { providerContractRegistry, webSearchProviderContractRegistry } from "./registry.js";
|
||||
|
||||
const loadOpenClawPluginsMock = vi.fn();
|
||||
|
||||
vi.mock("../loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args),
|
||||
}));
|
||||
|
||||
const { resolvePluginProviders } = await import("../providers.js");
|
||||
const { resolvePluginWebSearchProviders } = await import("../web-search-providers.js");
|
||||
|
||||
function uniqueSortedPluginIds(values: string[]) {
|
||||
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function normalizeProviderContractPluginId(pluginId: string) {
|
||||
return pluginId === "kimi-coding" ? "kimi" : pluginId;
|
||||
}
|
||||
|
||||
describe("plugin loader contract", () => {
|
||||
beforeEach(() => {
|
||||
loadOpenClawPluginsMock.mockReset();
|
||||
loadOpenClawPluginsMock.mockReturnValue({
|
||||
providers: [],
|
||||
mediaUnderstandingProviders: [],
|
||||
webSearchProviders: [],
|
||||
});
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("keeps bundled provider compatibility wired to the provider registry", () => {
|
||||
const providerPluginIds = uniqueSortedPluginIds(
|
||||
providerContractRegistry.map((entry) => entry.pluginId),
|
||||
providerContractRegistry.map((entry) => normalizeProviderContractPluginId(entry.pluginId)),
|
||||
);
|
||||
|
||||
resolvePluginProviders({
|
||||
bundledProviderAllowlistCompat: true,
|
||||
const compatPluginIds = providerTesting.resolveBundledProviderCompatPluginIds({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["openrouter"],
|
||||
@@ -38,37 +29,35 @@ describe("plugin loader contract", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
allow: expect.arrayContaining(providerPluginIds),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
const compatConfig = withBundledPluginAllowlistCompat({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["openrouter"],
|
||||
},
|
||||
},
|
||||
pluginIds: compatPluginIds,
|
||||
});
|
||||
|
||||
expect(uniqueSortedPluginIds(compatPluginIds)).toEqual(
|
||||
expect.arrayContaining(providerPluginIds),
|
||||
);
|
||||
expect(compatConfig?.plugins?.allow).toEqual(expect.arrayContaining(providerPluginIds));
|
||||
});
|
||||
|
||||
it("keeps vitest bundled provider enablement wired to the provider registry", () => {
|
||||
const providerPluginIds = uniqueSortedPluginIds(
|
||||
providerContractRegistry.map((entry) => entry.pluginId),
|
||||
providerContractRegistry.map((entry) => normalizeProviderContractPluginId(entry.pluginId)),
|
||||
);
|
||||
|
||||
resolvePluginProviders({
|
||||
bundledProviderVitestCompat: true,
|
||||
const compatConfig = providerTesting.withBundledProviderVitestCompat({
|
||||
config: undefined,
|
||||
pluginIds: providerPluginIds,
|
||||
env: { VITEST: "1" } as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
enabled: true,
|
||||
allow: expect.arrayContaining(providerPluginIds),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(compatConfig?.plugins).toMatchObject({
|
||||
enabled: true,
|
||||
allow: expect.arrayContaining(providerPluginIds),
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps bundled web search loading scoped to the web search registry", () => {
|
||||
@@ -81,7 +70,6 @@ describe("plugin loader contract", () => {
|
||||
expect(uniqueSortedPluginIds(providers.map((provider) => provider.pluginId))).toEqual(
|
||||
webSearchPluginIds,
|
||||
);
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps bundled web search allowlist compatibility wired to the web search registry", () => {
|
||||
@@ -101,6 +89,5 @@ describe("plugin loader contract", () => {
|
||||
expect(uniqueSortedPluginIds(providers.map((provider) => provider.pluginId))).toEqual(
|
||||
webSearchPluginIds,
|
||||
);
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,6 +82,11 @@ function resolveBundledProviderCompatPluginIds(params: {
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
resolveBundledProviderCompatPluginIds,
|
||||
withBundledProviderVitestCompat,
|
||||
} as const;
|
||||
|
||||
export function resolveOwningPluginIdsForProvider(params: {
|
||||
provider: string;
|
||||
config?: PluginLoadOptions["config"];
|
||||
|
||||
@@ -1,148 +1,37 @@
|
||||
import { createFirecrawlWebSearchProvider } from "../../extensions/firecrawl/src/firecrawl-search-provider.js";
|
||||
import {
|
||||
createPluginBackedWebSearchProvider,
|
||||
getScopedCredentialValue,
|
||||
getTopLevelCredentialValue,
|
||||
setScopedCredentialValue,
|
||||
setTopLevelCredentialValue,
|
||||
} from "../agents/tools/web-search-plugin-factory.js";
|
||||
import bravePlugin from "../../extensions/brave/index.js";
|
||||
import firecrawlPlugin from "../../extensions/firecrawl/index.js";
|
||||
import googlePlugin from "../../extensions/google/index.js";
|
||||
import moonshotPlugin from "../../extensions/moonshot/index.js";
|
||||
import perplexityPlugin from "../../extensions/perplexity/index.js";
|
||||
import xaiPlugin from "../../extensions/xai/index.js";
|
||||
import {
|
||||
withBundledPluginAllowlistCompat,
|
||||
withBundledPluginEnablementCompat,
|
||||
} from "./bundled-compat.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import type { PluginWebSearchProviderRegistration } from "./registry.js";
|
||||
import { getActivePluginRegistry } from "./runtime.js";
|
||||
import type { OpenClawPluginApi, WebSearchProviderPlugin } from "./types.js";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
|
||||
const BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS = [
|
||||
"brave",
|
||||
"firecrawl",
|
||||
"google",
|
||||
"moonshot",
|
||||
"perplexity",
|
||||
"xai",
|
||||
] as const;
|
||||
type RegistrablePlugin = {
|
||||
id: string;
|
||||
name: string;
|
||||
register: (api: OpenClawPluginApi) => void;
|
||||
};
|
||||
|
||||
const BUNDLED_WEB_SEARCH_PROVIDER_REGISTRY = [
|
||||
{
|
||||
pluginId: "brave",
|
||||
provider: createPluginBackedWebSearchProvider({
|
||||
id: "brave",
|
||||
label: "Brave Search",
|
||||
hint: "Structured results · country/language/time filters",
|
||||
envVars: ["BRAVE_API_KEY"],
|
||||
placeholder: "BSA...",
|
||||
signupUrl: "https://brave.com/search/api/",
|
||||
docsUrl: "https://docs.openclaw.ai/brave-search",
|
||||
autoDetectOrder: 10,
|
||||
getCredentialValue: getTopLevelCredentialValue,
|
||||
setCredentialValue: setTopLevelCredentialValue,
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "google",
|
||||
provider: createPluginBackedWebSearchProvider({
|
||||
id: "gemini",
|
||||
label: "Gemini (Google Search)",
|
||||
hint: "Google Search grounding · AI-synthesized",
|
||||
envVars: ["GEMINI_API_KEY"],
|
||||
placeholder: "AIza...",
|
||||
signupUrl: "https://aistudio.google.com/apikey",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/web",
|
||||
autoDetectOrder: 20,
|
||||
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "gemini"),
|
||||
setCredentialValue: (searchConfigTarget, value) =>
|
||||
setScopedCredentialValue(searchConfigTarget, "gemini", value),
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "xai",
|
||||
provider: createPluginBackedWebSearchProvider({
|
||||
id: "grok",
|
||||
label: "Grok (xAI)",
|
||||
hint: "xAI web-grounded responses",
|
||||
envVars: ["XAI_API_KEY"],
|
||||
placeholder: "xai-...",
|
||||
signupUrl: "https://console.x.ai/",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/web",
|
||||
autoDetectOrder: 30,
|
||||
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "grok"),
|
||||
setCredentialValue: (searchConfigTarget, value) =>
|
||||
setScopedCredentialValue(searchConfigTarget, "grok", value),
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "moonshot",
|
||||
provider: createPluginBackedWebSearchProvider({
|
||||
id: "kimi",
|
||||
label: "Kimi (Moonshot)",
|
||||
hint: "Moonshot web search",
|
||||
envVars: ["KIMI_API_KEY", "MOONSHOT_API_KEY"],
|
||||
placeholder: "sk-...",
|
||||
signupUrl: "https://platform.moonshot.cn/",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/web",
|
||||
autoDetectOrder: 40,
|
||||
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "kimi"),
|
||||
setCredentialValue: (searchConfigTarget, value) =>
|
||||
setScopedCredentialValue(searchConfigTarget, "kimi", value),
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "perplexity",
|
||||
provider: createPluginBackedWebSearchProvider({
|
||||
id: "perplexity",
|
||||
label: "Perplexity Search",
|
||||
hint: "Structured results · domain/country/language/time filters",
|
||||
envVars: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
|
||||
placeholder: "pplx-...",
|
||||
signupUrl: "https://www.perplexity.ai/settings/api",
|
||||
docsUrl: "https://docs.openclaw.ai/perplexity",
|
||||
autoDetectOrder: 50,
|
||||
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "perplexity"),
|
||||
setCredentialValue: (searchConfigTarget, value) =>
|
||||
setScopedCredentialValue(searchConfigTarget, "perplexity", value),
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "firecrawl",
|
||||
provider: createFirecrawlWebSearchProvider(),
|
||||
},
|
||||
] as const;
|
||||
const BUNDLED_WEB_SEARCH_PLUGINS: readonly RegistrablePlugin[] = [
|
||||
bravePlugin,
|
||||
firecrawlPlugin,
|
||||
googlePlugin,
|
||||
moonshotPlugin,
|
||||
perplexityPlugin,
|
||||
xaiPlugin,
|
||||
];
|
||||
|
||||
export function resolvePluginWebSearchProviders(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const allowlistCompat = params.bundledAllowlistCompat
|
||||
? withBundledPluginAllowlistCompat({
|
||||
config: params.config,
|
||||
pluginIds: BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS,
|
||||
})
|
||||
: params.config;
|
||||
const config = withBundledPluginEnablementCompat({
|
||||
config: allowlistCompat,
|
||||
pluginIds: BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS,
|
||||
});
|
||||
const normalizedPlugins = normalizePluginsConfig(config?.plugins);
|
||||
|
||||
return sortWebSearchProviders(
|
||||
BUNDLED_WEB_SEARCH_PROVIDER_REGISTRY.filter(
|
||||
({ pluginId }) =>
|
||||
resolveEffectiveEnableState({
|
||||
id: pluginId,
|
||||
origin: "bundled",
|
||||
config: normalizedPlugins,
|
||||
rootConfig: config,
|
||||
}).enabled,
|
||||
).map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
const BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS = BUNDLED_WEB_SEARCH_PLUGINS.map(
|
||||
(plugin) => plugin.id,
|
||||
);
|
||||
|
||||
function sortWebSearchProviders(
|
||||
providers: PluginWebSearchProviderEntry[],
|
||||
@@ -157,18 +46,95 @@ function sortWebSearchProviders(
|
||||
});
|
||||
}
|
||||
|
||||
function mapWebSearchProviderEntries(
|
||||
entries: PluginWebSearchProviderRegistration[],
|
||||
): PluginWebSearchProviderEntry[] {
|
||||
return sortWebSearchProviders(
|
||||
entries.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeWebSearchPluginConfig(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
}): PluginLoadOptions["config"] {
|
||||
const allowlistCompat = params.bundledAllowlistCompat
|
||||
? withBundledPluginAllowlistCompat({
|
||||
config: params.config,
|
||||
pluginIds: BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS,
|
||||
})
|
||||
: params.config;
|
||||
return withBundledPluginEnablementCompat({
|
||||
config: allowlistCompat,
|
||||
pluginIds: BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS,
|
||||
});
|
||||
}
|
||||
|
||||
function captureBundledWebSearchProviders(
|
||||
plugin: RegistrablePlugin,
|
||||
): PluginWebSearchProviderRegistration[] {
|
||||
const providers: WebSearchProviderPlugin[] = [];
|
||||
const api = {
|
||||
registerProvider() {},
|
||||
registerSpeechProvider() {},
|
||||
registerMediaUnderstandingProvider() {},
|
||||
registerWebSearchProvider(provider: WebSearchProviderPlugin) {
|
||||
providers.push(provider);
|
||||
},
|
||||
registerTool() {},
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
return providers.map((provider) => ({
|
||||
pluginId: plugin.id,
|
||||
pluginName: plugin.name,
|
||||
provider,
|
||||
source: "bundled",
|
||||
}));
|
||||
}
|
||||
|
||||
function resolveBundledWebSearchRegistrations(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
}): PluginWebSearchProviderRegistration[] {
|
||||
const config = normalizeWebSearchPluginConfig(params);
|
||||
if (config?.plugins?.enabled === false) {
|
||||
return [];
|
||||
}
|
||||
const allowlist = config?.plugins?.allow
|
||||
? new Set(config.plugins.allow.map((entry) => entry.trim()).filter(Boolean))
|
||||
: null;
|
||||
return BUNDLED_WEB_SEARCH_PLUGINS.flatMap((plugin) => {
|
||||
if (allowlist && !allowlist.has(plugin.id)) {
|
||||
return [];
|
||||
}
|
||||
if (config?.plugins?.entries?.[plugin.id]?.enabled === false) {
|
||||
return [];
|
||||
}
|
||||
return captureBundledWebSearchProviders(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePluginWebSearchProviders(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
return mapWebSearchProviderEntries(resolveBundledWebSearchRegistrations(params));
|
||||
}
|
||||
|
||||
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 mapWebSearchProviderEntries(runtimeProviders);
|
||||
}
|
||||
return resolvePluginWebSearchProviders(params);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user