feat(searxng): pass through image result urls

This commit is contained in:
Peter Steinberger
2026-05-02 06:56:20 +01:00
parent 52eee27f30
commit ad85e5c64c
4 changed files with 59 additions and 1 deletions

View File

@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
- 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/SearXNG: pass through `img_src` image URLs from SearXNG image-category results. Supersedes #61416. Thanks @sghael.
- 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

@@ -112,6 +112,8 @@ key wins first).
## Notes
- **JSON API** -- uses SearXNG's native `format=json` endpoint, not HTML scraping
- **Image result URLs** -- image-category results include `img_src` when SearXNG
returns a direct image URL
- **No API key** -- works with any SearXNG instance out of the box
- **Base URL validation** -- `baseUrl` must be a valid `http://` or `https://`
URL; public hosts must use `https://`

View File

@@ -39,6 +39,53 @@ describe("searxng client", () => {
).toEqual([{ title: "One", url: "https://example.com/1", content: "A" }]);
});
it("preserves img_src from image search results", () => {
expect(
__testing.parseSearxngResponseText(
JSON.stringify({
results: [
{
title: "Kitten",
url: "https://example.com/kitten",
content: "A cute kitten",
img_src: "https://cdn.example.com/kitten.jpg",
},
{
title: "No Image",
url: "https://example.com/text",
content: "Text only",
},
{
title: "Bad Image",
url: "https://example.com/bad",
img_src: { url: "https://cdn.example.com/bad.jpg" },
},
],
}),
10,
),
).toEqual([
{
title: "Kitten",
url: "https://example.com/kitten",
content: "A cute kitten",
img_src: "https://cdn.example.com/kitten.jpg",
},
{
title: "No Image",
url: "https://example.com/text",
content: "Text only",
img_src: undefined,
},
{
title: "Bad Image",
url: "https://example.com/bad",
content: undefined,
img_src: undefined,
},
]);
});
it("drops malformed result rows instead of failing the whole response", () => {
expect(
__testing.parseSearxngResponseText(

View File

@@ -40,6 +40,7 @@ type SearxngResult = {
url: string;
title: string;
content?: string;
img_src?: string;
};
type SearxngResponse = {
@@ -51,7 +52,12 @@ function normalizeSearxngResult(value: unknown): SearxngResult | null {
return null;
}
const candidate = value as { url?: unknown; title?: unknown; content?: unknown };
const candidate = value as {
url?: unknown;
title?: unknown;
content?: unknown;
img_src?: unknown;
};
if (typeof candidate.url !== "string" || typeof candidate.title !== "string") {
return null;
}
@@ -60,6 +66,7 @@ function normalizeSearxngResult(value: unknown): SearxngResult | null {
url: candidate.url,
title: candidate.title,
content: typeof candidate.content === "string" ? candidate.content : undefined,
img_src: typeof candidate.img_src === "string" ? candidate.img_src : undefined,
};
}
@@ -254,6 +261,7 @@ export async function runSearxngSearch(params: {
url: result.url,
snippet: result.content ? wrapWebContent(result.content, "web_search") : "",
siteName: resolveSiteName(result.url) || undefined,
img_src: result.img_src || undefined,
})),
} satisfies Record<string, unknown>;