Overhaul docs retrieval and web search quality
Replace the abandoned llms-txt-mcp/Chroma docs backend with an in-repo MCP service: SQLite WAL + FTS5 + sentence-transformer embeddings, transactional source replacement, persisted state across restarts, singleflight refresh with conditional requests, hybrid lexical/semantic ranking with exact-duplicate collapse, source/host filters, and explicit-by-default content retrieval. Add docs_rebuild and a docs-rebuild CLI command. Add deterministic llms-full.txt snapshot generation for machine-local menus with hash-validated provenance manifests; lifecycle commands promote a local menu to its snapshot only when the manifest validates. Switch public source profiles to content-bearing llms-full.txt feeds. Improve web search: bounded provider fallback with per-attempt diagnostics and cancellation, an optional Brave Search API provider, strict SearXNG engine selection, capped link/media extraction, and a real engine=browser renderer that routes every request through the existing SSRF vetting while blocking WebSockets, non-GET traffic, and private destinations. Extend release checks with offline unit suites and isolated candidate container tests for both images.
This commit is contained in:
56
docker/web-search/overrides/duckduckgo.js
Normal file
56
docker/web-search/overrides/duckduckgo.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { JSDOM } from "jsdom";
|
||||
import { HTTP_TIMEOUT } from "../constants.js";
|
||||
import { fetchWithTimeout } from "../utils/http.js";
|
||||
import { getRandomUserAgent, getAcceptLanguageHeader } from "../utils/user-agent.js";
|
||||
import { searchCache, createCacheKey } from "../utils/cache.js";
|
||||
|
||||
export class DuckDuckGoProvider {
|
||||
name = "duckduckgo";
|
||||
|
||||
decodeDuckDuckGoRedirect(href) {
|
||||
try {
|
||||
const url = new URL(href, "https://duckduckgo.com/");
|
||||
if (url.hostname === "duckduckgo.com" && url.pathname.startsWith("/l/")) {
|
||||
const target = url.searchParams.get("uddg");
|
||||
if (target) return decodeURIComponent(target);
|
||||
}
|
||||
return url.toString();
|
||||
} catch {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
async search(q, limit, lang, signal) {
|
||||
const cacheKey = createCacheKey("ddg", q, limit, lang);
|
||||
const cached = searchCache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
const url = new URL("https://html.duckduckgo.com/html/");
|
||||
url.searchParams.set("q", q);
|
||||
const headers = { "User-Agent": getRandomUserAgent(), ...getAcceptLanguageHeader(lang) };
|
||||
const response = await fetchWithTimeout(url, { headers, signal }, HTTP_TIMEOUT);
|
||||
if (!response.ok) throw new Error(`DuckDuckGo HTML ${response.status}`);
|
||||
const dom = new JSDOM(await response.text(), { url: `https://duckduckgo.com/?q=${encodeURIComponent(q)}` });
|
||||
const anchors = Array.from(dom.window.document.querySelectorAll("a.result__a"));
|
||||
const snippets = Array.from(dom.window.document.querySelectorAll(".result__snippet"));
|
||||
const items = [];
|
||||
for (let index = 0; index < anchors.length && items.length < limit; index += 1) {
|
||||
const title = (anchors[index].textContent || "").trim();
|
||||
const href = this.decodeDuckDuckGoRedirect(anchors[index].getAttribute("href") || "");
|
||||
if (!title || !href) continue;
|
||||
try {
|
||||
items.push({
|
||||
title,
|
||||
url: new URL(href).toString(),
|
||||
snippet: (snippets[index]?.textContent || "").trim() || undefined,
|
||||
source: "duckduckgo"
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
searchCache.set(cacheKey, items);
|
||||
return items;
|
||||
}
|
||||
|
||||
async isAvailable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user