feat(searxng): show setup JSON format note

This commit is contained in:
Peter Steinberger
2026-05-02 06:51:13 +01:00
parent 49dd4339ce
commit ee8f47eda7
6 changed files with 92 additions and 0 deletions

View File

@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
- Agents/models: keep legacy CLI runtime model refs such as `claude-cli/*` in the configured allowlist after canonical runtime migration, so cron `payload.model` overrides keep working. Fixes #75753. Thanks @RyanSandoval.
- Plugin SDK: re-export `isPrivateIpAddress` from `plugin-sdk/ssrf-runtime`, restoring source-checkout builds for SearXNG and Firecrawl private-network guards. Thanks @vincentkoc.
- CLI/directory: report unsupported directory operations for installed channel plugins instead of prompting to reinstall the plugin when it lacks a directory adapter. Fixes #75770. Thanks @lawong888.
- Web search/SearXNG: show the JSON API `search.formats` prerequisite during SearXNG setup before prompting for the base URL. Supersedes #65592. Thanks @evanpaul14.
- Web search: keep public provider requests on the strict SSRF guard and reserve private-network access for explicit self-hosted SearXNG/Firecrawl endpoints. Fixes #74357 and supersedes #74360. Thanks @fede-kamel.
- Web search/Firecrawl: allow self-hosted private/internal Firecrawl `baseUrl` endpoints, including HTTP for private targets, while keeping hosted Firecrawl on the strict official endpoint. Fixes #63877 and supersedes #59666, #63941, and #74013. Thanks @jhthompson12, @jzakirov, @Mlightsnow, and @shad0wca7.
- Providers/OpenRouter: strip trailing assistant prefill turns from verified OpenRouter Anthropic model requests when reasoning is enabled, so Claude 4.6 routes no longer fail with Anthropic's prefill rejection through the OpenAI-compatible adapter. Fixes #75395. Thanks @sbmilburn.

View File

@@ -145,6 +145,13 @@ describe("searxng web search provider", () => {
expect(resolveSearxngLanguage(config)).toBe("de");
});
it("exposes a credentialNote with JSON format guidance", () => {
const provider = createSearxngWebSearchProvider();
expect(provider.credentialNote).toContain("json format enabled");
expect(provider.credentialNote).toContain("search.formats");
});
it("persists base URL to plugin config via setConfiguredCredentialValue", () => {
const provider = createSearxngWebSearchProvider();
const config = {} as Record<string, unknown>;

View File

@@ -56,6 +56,10 @@ export function createSearxngWebSearchProvider(): WebSearchProviderPlugin {
configuredCredential: { pluginId: "searxng", field: "baseUrl" },
selectionPluginId: "searxng",
}),
credentialNote: [
"For the SearXNG JSON API to work, make sure your SearXNG instance",
"has the json format enabled in its settings.yml under search.formats.",
].join("\n"),
createTool: (ctx) => ({
description:
"Search the web using a self-hosted SearXNG instance. Returns titles, URLs, and snippets.",

View File

@@ -16,6 +16,7 @@ const mockGrokProvider = vi.hoisted(() => ({
envVars: ["XAI_API_KEY"],
onboardingScopes: ["text-inference"],
credentialPath: "plugins.entries.xai.config.webSearch.apiKey",
credentialNote: "Configure Grok web search prerequisites before entering the credential.",
getCredentialValue: (search?: Record<string, unknown>) => search?.apiKey,
setCredentialValue: (searchConfigTarget: Record<string, unknown>, value: unknown) => {
searchConfigTarget.apiKey = value;
@@ -128,6 +129,79 @@ describe("runSearchSetupFlow", () => {
});
});
it("shows provider credential notes before plaintext credential prompts", async () => {
const select = vi.fn().mockResolvedValueOnce("grok").mockResolvedValueOnce("no");
const text = vi.fn().mockResolvedValue("xai-test-key");
const note = vi.fn(async () => {});
const prompter = createWizardPrompter({
note: note as never,
select: select as never,
text: text as never,
});
await runSearchSetupFlow({ plugins: { allow: ["xai"] } }, createNonExitingRuntime(), prompter);
expect(note).toHaveBeenCalledWith(mockGrokProvider.credentialNote, mockGrokProvider.label);
expect(text).toHaveBeenCalledTimes(1);
expect(note.mock.invocationCallOrder[1]).toBeLessThan(text.mock.invocationCallOrder[0]);
});
it("shows provider credential notes before SecretRef setup notes", async () => {
const select = vi.fn().mockResolvedValueOnce("grok").mockResolvedValueOnce("no");
const note = vi.fn(async () => {});
const prompter = createWizardPrompter({
note: note as never,
select: select as never,
});
await runSearchSetupFlow({ plugins: { allow: ["xai"] } }, createNonExitingRuntime(), prompter, {
secretInputMode: "ref",
});
expect(note).toHaveBeenNthCalledWith(
2,
mockGrokProvider.credentialNote,
mockGrokProvider.label,
);
expect(note).toHaveBeenNthCalledWith(
3,
expect.stringContaining("Secret references enabled"),
"Web search",
);
});
it("skips provider credential notes in quickstart fast path", async () => {
const select = vi.fn().mockResolvedValueOnce("grok").mockResolvedValueOnce("no");
const note = vi.fn(async () => {});
const prompter = createWizardPrompter({
note: note as never,
select: select as never,
});
await runSearchSetupFlow(
{
plugins: {
allow: ["xai"],
entries: {
xai: {
enabled: true,
config: {
webSearch: {
apiKey: "xai-test-key",
},
},
},
},
},
},
createNonExitingRuntime(),
prompter,
{ quickstartDefaults: true },
);
expect(note).not.toHaveBeenCalledWith(mockGrokProvider.credentialNote, mockGrokProvider.label);
});
it("preserves disabled web_search state while still allowing provider-owned x_search setup", async () => {
const select = vi
.fn()

View File

@@ -434,6 +434,10 @@ export async function runSearchSetupFlow(
});
}
if (entry.credentialNote) {
await prompter.note(entry.credentialNote, entry.label);
}
const useSecretRefMode = opts?.secretInputMode === "ref"; // pragma: allowlist secret
if (useSecretRefMode) {
if (keyConfigured) {

View File

@@ -92,6 +92,8 @@ export type WebSearchProviderPlugin = {
placeholder: string;
signupUrl: string;
docsUrl?: string;
/** Optional note shown before credential collection for provider-specific prerequisites. */
credentialNote?: string;
autoDetectOrder?: number;
credentialPath: string;
inactiveSecretPaths?: string[];