mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(search): bundled SearXNG sidecar actually works out of the box
This commit is contained in:
parent
178b9306d9
commit
52a9a785c1
@ -29,6 +29,10 @@ JWT_SECRET=
|
||||
# 若留空,服务器会允许所有 origin 并在启动日志里 WARN。生产部署务必设置。
|
||||
MATECLAW_CORS_ALLOWED_ORIGINS=
|
||||
|
||||
# SearXNG 会话密钥(容器内部用,留空会用开发默认值)。生产部署请设成 32+ 位随机串。
|
||||
# openssl rand -hex 32
|
||||
SEARXNG_SECRET=
|
||||
|
||||
# ==================== 浏览器工具(可选) ====================
|
||||
#
|
||||
# Docker 镜像已经把 Chromium 打进去了,默认零配置可用。
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user