Nineteen modules gain a broker-bound runtime container that serves the module's tools under its own scoped account: bazarr, gitea, grafana, home-assistant, icecast, influxdb, jackett, keycloak, mailu, nextcloud, nodered, nzbget, ombi, photos, portainer, qbittorrent, searxng, tautulli, verdaccio. Config is the assignment's, not the manifest's (ADR 0051): each client's fromEnv overlays a settings-merged config file (MESH_<M>_CONFIG_FILE) over its env fallbacks, so URL and credentials come from `settings set`, with the URL defaulting to the server on the node. nextcloud and mailu also mount the docker socket for their exec-based tools. Proven in the mesh-lab: assigned-grafana green — settings deliver the URL and token, the runtime reads the merged config and serves grafana's tools under the scoped account, with nothing in the manifest. Two gaps this surfaced are filed as hq issues 008 (a provider runtime's seal key) and 009 (a settings change does not restart a container runtime). Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
87 lines
2.9 KiB
TypeScript
87 lines
2.9 KiB
TypeScript
// SearXNG's API client — searxng's own code, living in the module (novox/hq ADR 0044). SearXNG is a
|
|
// privacy-respecting metasearch engine: it forwards a query to many upstream engines and returns the
|
|
// merged results. Its JSON API (`/search?q=...&format=json`) is what makes a `searxng_search` tool
|
|
// useful; the client speaks only that. No credential — the instance is reached inside the mesh.
|
|
|
|
import { readFileSync } from "node:fs";
|
|
|
|
export interface SearxResult {
|
|
title: string;
|
|
url: string;
|
|
content?: string;
|
|
engine?: string;
|
|
category?: string;
|
|
score?: number;
|
|
}
|
|
|
|
export interface SearxSearch {
|
|
query: string;
|
|
numberOfResults: number;
|
|
results: SearxResult[];
|
|
suggestions: string[];
|
|
answers: string[];
|
|
}
|
|
|
|
export interface SearxOptions {
|
|
categories?: string;
|
|
language?: string;
|
|
pageno?: number;
|
|
}
|
|
|
|
/** The settings-merged config the mesh delivers (novox/hq ADR 0051): { url, apiKey, token, password, user, ... }. */
|
|
function meshConfig(file?: string): Record<string, string> {
|
|
if (!file) return {};
|
|
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
|
catch { return {}; }
|
|
}
|
|
|
|
export class SearxngClient {
|
|
readonly baseUrl: string;
|
|
|
|
constructor(url: string) {
|
|
this.baseUrl = url.replace(/\/$/, "");
|
|
}
|
|
|
|
/** Build from the module's environment. No key: SearXNG's search API is open on the mesh, so a URL
|
|
* is all it takes — defaulting to the container's own listen port. */
|
|
static fromEnv(env: NodeJS.ProcessEnv = process.env): SearxngClient {
|
|
const cfg = meshConfig(env.MESH_SEARXNG_CONFIG_FILE);
|
|
const url = cfg.url ?? (env.MESH_SEARXNG_URL ?? `http://127.0.0.1:${env.SEARXNG_PORT ?? "8080"}`);
|
|
return new SearxngClient(url);
|
|
}
|
|
|
|
async search(query: string, opts: SearxOptions = {}): Promise<SearxSearch> {
|
|
const params = new URLSearchParams({ q: query, format: "json" });
|
|
if (opts.categories) params.set("categories", opts.categories);
|
|
if (opts.language) params.set("language", opts.language);
|
|
if (opts.pageno) params.set("pageno", String(opts.pageno));
|
|
|
|
const res = await fetch(`${this.baseUrl}/search?${params.toString()}`, {
|
|
headers: { Accept: "application/json" },
|
|
});
|
|
if (!res.ok) throw new Error(`SearXNG search: ${res.status} ${await res.text()}`);
|
|
const data = (await res.json()) as {
|
|
results?: SearxResult[];
|
|
suggestions?: string[];
|
|
answers?: string[];
|
|
number_of_results?: number;
|
|
};
|
|
const results = data.results ?? [];
|
|
return {
|
|
query,
|
|
// SearXNG's own count is often 0 even with results; fall back to what we actually got.
|
|
numberOfResults: data.number_of_results || results.length,
|
|
results: results.map((r) => ({
|
|
title: r.title,
|
|
url: r.url,
|
|
content: r.content,
|
|
engine: r.engine,
|
|
category: r.category,
|
|
score: r.score,
|
|
})),
|
|
suggestions: data.suggestions ?? [],
|
|
answers: data.answers ?? [],
|
|
};
|
|
}
|
|
}
|