mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:40:43 +00:00
test: keep searxng web search contract light
Lazy-load the SearXNG web-search client from provider execution and reuse the shared contract helper for credential and selection wiring. Keep the shared fast-path contract focused on the single bundled manifest it checks.
This commit is contained in:
@@ -1,40 +1,32 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { readNumberParam, readStringParam } from "openclaw/plugin-sdk/param-readers";
|
||||
import {
|
||||
enablePluginInConfig,
|
||||
getScopedCredentialValue,
|
||||
readNumberParam,
|
||||
readStringParam,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setScopedCredentialValue,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
createWebSearchProviderContractFields,
|
||||
type WebSearchProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { runSearxngSearch } from "./searxng-client.js";
|
||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||
|
||||
const SearxngSearchSchema = Type.Object(
|
||||
{
|
||||
query: Type.String({ description: "Search query string." }),
|
||||
count: Type.Optional(
|
||||
Type.Number({
|
||||
description: "Number of results to return (1-10).",
|
||||
minimum: 1,
|
||||
maximum: 10,
|
||||
}),
|
||||
),
|
||||
categories: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Optional comma-separated search categories such as general, news, or science.",
|
||||
}),
|
||||
),
|
||||
language: Type.Optional(
|
||||
Type.String({
|
||||
description: "Optional language code for results such as en, de, or fr.",
|
||||
}),
|
||||
),
|
||||
const SEARXNG_CREDENTIAL_PATH = "plugins.entries.searxng.config.webSearch.baseUrl";
|
||||
|
||||
const SearxngSearchSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search query string." },
|
||||
count: {
|
||||
type: "number",
|
||||
description: "Number of results to return (1-10).",
|
||||
minimum: 1,
|
||||
maximum: 10,
|
||||
},
|
||||
categories: {
|
||||
type: "string",
|
||||
description: "Optional comma-separated search categories such as general, news, or science.",
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
description: "Optional language code for results such as en, de, or fr.",
|
||||
},
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
additionalProperties: false,
|
||||
} satisfies Record<string, unknown>;
|
||||
|
||||
export function createSearxngWebSearchProvider(): WebSearchProviderPlugin {
|
||||
return {
|
||||
@@ -48,29 +40,27 @@ export function createSearxngWebSearchProvider(): WebSearchProviderPlugin {
|
||||
placeholder: "http://localhost:8080",
|
||||
signupUrl: "https://docs.searxng.org/",
|
||||
autoDetectOrder: 200,
|
||||
credentialPath: "plugins.entries.searxng.config.webSearch.baseUrl",
|
||||
inactiveSecretPaths: ["plugins.entries.searxng.config.webSearch.baseUrl"],
|
||||
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "searxng"),
|
||||
setCredentialValue: (searchConfigTarget, value) =>
|
||||
setScopedCredentialValue(searchConfigTarget, "searxng", value),
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "searxng")?.baseUrl,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "searxng", "baseUrl", value);
|
||||
},
|
||||
applySelectionConfig: (config) => enablePluginInConfig(config, "searxng").config,
|
||||
credentialPath: SEARXNG_CREDENTIAL_PATH,
|
||||
...createWebSearchProviderContractFields({
|
||||
credentialPath: SEARXNG_CREDENTIAL_PATH,
|
||||
searchCredential: { type: "scoped", scopeId: "searxng" },
|
||||
configuredCredential: { pluginId: "searxng", field: "baseUrl" },
|
||||
selectionPluginId: "searxng",
|
||||
}),
|
||||
createTool: (ctx) => ({
|
||||
description:
|
||||
"Search the web using a self-hosted SearXNG instance. Returns titles, URLs, and snippets.",
|
||||
parameters: SearxngSearchSchema,
|
||||
execute: async (args) =>
|
||||
await runSearxngSearch({
|
||||
execute: async (args) => {
|
||||
const { runSearxngSearch } = await import("./searxng-client.js");
|
||||
return await runSearxngSearch({
|
||||
config: ctx.config,
|
||||
query: readStringParam(args, "query", { required: true }),
|
||||
count: readNumberParam(args, "count", { integer: true }),
|
||||
categories: readStringParam(args, "categories"),
|
||||
language: readStringParam(args, "language"),
|
||||
}),
|
||||
});
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { resolveManifestContractOwnerPluginId } from "../../../src/plugins/manifest-registry.js";
|
||||
import { resolveBundledPluginsDir } from "../../../src/plugins/bundled-dir.js";
|
||||
import {
|
||||
resolveBundledExplicitRuntimeWebSearchProvidersFromPublicArtifacts,
|
||||
resolveBundledExplicitWebSearchProvidersFromPublicArtifacts,
|
||||
} from "../../../src/plugins/web-provider-public-artifacts.explicit.js";
|
||||
import { normalizeOptionalLowercaseString } from "../../../src/shared/string-coerce.js";
|
||||
|
||||
type ComparableProvider = {
|
||||
pluginId: string;
|
||||
@@ -24,6 +27,64 @@ type ComparableProvider = {
|
||||
hasResolveRuntimeMetadata: boolean;
|
||||
};
|
||||
|
||||
type MinimalBundledPluginManifest = {
|
||||
id?: unknown;
|
||||
contracts?: {
|
||||
webSearchProviders?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
const bundledWebSearchManifestContracts = new Map<
|
||||
string,
|
||||
{ pluginId: string; webSearchProviderIds: string[] } | null
|
||||
>();
|
||||
|
||||
function readBundledWebSearchManifestContract(pluginId: string) {
|
||||
if (bundledWebSearchManifestContracts.has(pluginId)) {
|
||||
return bundledWebSearchManifestContracts.get(pluginId) ?? null;
|
||||
}
|
||||
|
||||
const bundledPluginsDir = resolveBundledPluginsDir();
|
||||
if (!bundledPluginsDir) {
|
||||
bundledWebSearchManifestContracts.set(pluginId, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const manifestPath = path.join(bundledPluginsDir, pluginId, "openclaw.plugin.json");
|
||||
const manifest = JSON.parse(
|
||||
fs.readFileSync(manifestPath, "utf8"),
|
||||
) as MinimalBundledPluginManifest;
|
||||
const manifestPluginId = typeof manifest.id === "string" ? manifest.id : "";
|
||||
const webSearchProviderIds = Array.isArray(manifest.contracts?.webSearchProviders)
|
||||
? manifest.contracts.webSearchProviders.filter(
|
||||
(providerId): providerId is string => typeof providerId === "string",
|
||||
)
|
||||
: [];
|
||||
const contract = { pluginId: manifestPluginId, webSearchProviderIds };
|
||||
bundledWebSearchManifestContracts.set(pluginId, contract);
|
||||
return contract;
|
||||
}
|
||||
|
||||
function resolveBundledManifestWebSearchOwnerPluginId(params: {
|
||||
pluginId: string;
|
||||
providerId: string;
|
||||
}): string | undefined {
|
||||
const normalizedProviderId = normalizeOptionalLowercaseString(params.providerId);
|
||||
if (!normalizedProviderId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const contract = readBundledWebSearchManifestContract(params.pluginId);
|
||||
if (
|
||||
!contract?.webSearchProviderIds.some(
|
||||
(candidate) => normalizeOptionalLowercaseString(candidate) === normalizedProviderId,
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return contract.pluginId || undefined;
|
||||
}
|
||||
|
||||
function toComparableEntry(params: {
|
||||
pluginId: string;
|
||||
provider: {
|
||||
@@ -87,10 +148,9 @@ export function describeBundledWebSearchFastPathContract(pluginId: string) {
|
||||
expect(providers.length).toBeGreaterThan(0);
|
||||
for (const provider of providers) {
|
||||
expect(
|
||||
resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: provider.id,
|
||||
origin: "bundled",
|
||||
resolveBundledManifestWebSearchOwnerPluginId({
|
||||
pluginId,
|
||||
providerId: provider.id,
|
||||
}),
|
||||
).toBe(pluginId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user