fix(search): bundled SearXNG sidecar actually works out of the box

This commit is contained in:
matevip 2026-04-24 22:08:42 +08:00
parent 178b9306d9
commit 52a9a785c1
3 changed files with 50 additions and 10 deletions

View File

@ -29,6 +29,10 @@ JWT_SECRET=
# 若留空,服务器会允许所有 origin 并在启动日志里 WARN。生产部署务必设置。
MATECLAW_CORS_ALLOWED_ORIGINS=
# SearXNG 会话密钥(容器内部用,留空会用开发默认值)。生产部署请设成 32+ 位随机串。
# openssl rand -hex 32
SEARXNG_SECRET=
# ==================== 浏览器工具(可选) ====================
#
# Docker 镜像已经把 Chromium 打进去了,默认零配置可用。

View File

@ -31,18 +31,30 @@ services:
timeout: 5s
retries: 5
# SearXNG 搜索引擎keyless 搜索 provider零配置可用
# SearXNG 搜索引擎keyless 搜索 provider
#
# ⚠️ deploy/searxng/settings.yml is bind-mounted because the upstream image
# ships with JSON format disabled and the anti-bot Limiter plugin enabled —
# both of which would silently break mateclaw's SearXNGSearchProvider.
# Do NOT remove that bind mount.
searxng:
image: searxng/searxng:latest
container_name: mateclaw-searxng
restart: unless-stopped
environment:
- SEARXNG_BASE_URL=http://searxng:8080
- SEARXNG_SECRET=${SEARXNG_SECRET:-mateclaw-dev-searxng-secret-change-me}
- UWSGI_WORKERS=2
- UWSGI_THREADS=4
volumes:
- searxng_data:/etc/searxng
# This override MUST be mounted AFTER the named volume so it shadows
# the image's default settings.yml (enables JSON, disables limiter).
- ./deploy/searxng/settings.yml:/etc/searxng/settings.yml:ro
ports:
- "8088:8080"
healthcheck:
# Healthz needs json format, so this also doubles as an integration check.
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/healthz"]
interval: 30s
timeout: 5s

View File

@ -81,24 +81,42 @@ public class SearXNGSearchProvider implements SearchProvider {
urlBuilder.append("&time_range=").append(searchQuery.freshness().toLowerCase());
}
String response = HttpUtil.createGet(urlBuilder.toString())
var resp = HttpUtil.createGet(urlBuilder.toString())
.header("Accept", "application/json")
.timeout(15000)
.execute()
.body();
.execute();
int status = resp.getStatus();
String response = resp.body();
String contentType = resp.header("Content-Type");
log.debug("SearXNG result for '{}': length={}", searchQuery.query(), response != null ? response.length() : 0);
return parseResponse(response, searchQuery.resolvedCount());
log.debug("SearXNG response for '{}': status={}, contentType={}, length={}",
searchQuery.query(), status, contentType, response != null ? response.length() : 0);
return parseResponse(response, status, contentType, searchQuery.resolvedCount(), urlBuilder.toString());
}
private List<SearchResult> parseResponse(String response, int limit) {
private List<SearchResult> parseResponse(String response, int status, String contentType,
int limit, String requestUrl) {
List<SearchResult> results = new ArrayList<>();
if (response == null || response.isBlank()) return results;
if (response == null || response.isBlank()) {
log.warn("SearXNG returned empty body (status={}, url={})", status, requestUrl);
return results;
}
if (status >= 400) {
log.warn("SearXNG returned HTTP {} — preview: {}", status, preview(response));
return results;
}
if (contentType != null && !contentType.contains("json")) {
// Most common cause: settings.yml has no `json` under search.formats
// or the Limiter plugin rewrote the response to HTML.
log.warn("SearXNG did not return JSON (contentType={}). Check settings.yml has search.formats including 'json' and server.limiter: false. Preview: {}",
contentType, preview(response));
return results;
}
try {
JSONObject json = JSONUtil.parseObj(response);
JSONArray items = json.getJSONArray("results");
if (items == null) return results;
if (items == null || items.isEmpty()) return results;
limit = Math.min(items.size(), limit);
for (int i = 0; i < limit; i++) {
@ -114,11 +132,17 @@ public class SearXNGSearchProvider implements SearchProvider {
.build());
}
} catch (Exception e) {
log.warn("SearXNG 结果解析失败: {}", e.getMessage());
log.warn("SearXNG parse failed: {} — preview: {}", e.getMessage(), preview(response));
}
return results;
}
private static String preview(String body) {
if (body == null) return "";
String flat = body.replaceAll("\\s+", " ").trim();
return flat.length() > 200 ? flat.substring(0, 200) + "..." : flat;
}
private String extractDomain(String url) {
try {
return URI.create(url).getHost();