/**
* SearXNG 搜索服务
* 使用 HTTP API 调用 SearXNG 实例进行搜索
*/
import axios from 'axios';
import { logger } from '../utils/logger.js';
export interface SearXNGConfig {
baseUrl: string; // SearXNG 实例地址
timeout?: number; // 请求超时(默认 10s)
}
export interface SearXNGResult {
title: string;
url: string;
content: string; // 摘要描述
engine: string; // 来源引擎
score?: number; // 相关性分数
}
export interface SearXNGSearchOptions {
categories?: string[]; // 搜索类别
language?: string; // 语言
timeRange?: string; // 时间范围
maxResults?: number; // 最大结果数(默认 20)
}
export class SearXNGService {
private config: SearXNGConfig;
constructor(config: SearXNGConfig) {
this.config = {
timeout: 10000,
...config
};
// 移除尾部斜杠
if (this.config.baseUrl.endsWith('/')) {
this.config.baseUrl = this.config.baseUrl.slice(0, -1);
}
logger.info('SearXNG service initialized', {
baseUrl: this.config.baseUrl,
timeout: this.config.timeout
});
}
/**
* 执行搜索
*/
async search(query: string, options?: SearXNGSearchOptions): Promise<SearXNGResult[]> {
const startTime = Date.now();
try {
logger.info('Executing SearXNG search', { query, options });
// 构建请求参数
const params: Record<string, string> = {
q: query,
format: 'json',
language: options?.language || 'zh',
pageno: '1'
};
// 添加可选参数
if (options?.categories) {
params.categories = options.categories.join(',');
}
if (options?.timeRange) {
params.time_range = options.timeRange;
}
// 发送请求
const response = await axios.get(`${this.config.baseUrl}/search`, {
params,
timeout: this.config.timeout,
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; WebAnalysis-MCP/1.0)'
}
});
// 解析响应
logger.debug('SearXNG raw response', {
status: response.status,
statusText: response.statusText,
hasData: !!response.data,
dataType: typeof response.data,
isArray: Array.isArray(response.data),
resultsCount: response.data?.results?.length || 0
});
const results = this.parseResponse(response.data);
logger.info('SearXNG search results', {
parsedResultsCount: results.length,
results: results.slice(0, 3).map(r => ({ title: r.title, engine: r.engine }))
});
// 限制结果数量
const maxResults = options?.maxResults || 20;
const limitedResults = results.slice(0, maxResults);
const duration = Date.now() - startTime;
logger.info('SearXNG search completed', {
query,
resultCount: limitedResults.length,
duration: `${duration}ms`
});
return limitedResults;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('SearXNG search failed', {
query,
error: errorMessage,
url: `${this.config.baseUrl}/search`
});
throw new Error(`SearXNG 搜索失败: ${errorMessage}`);
}
}
/**
* 解析 SearXNG 响应数据
*/
private parseResponse(data: any): SearXNGResult[] {
// SearXNG JSON 响应格式
if (Array.isArray(data.results)) {
return data.results.map((item: any) => ({
title: item.title || '',
url: item.url || '',
content: item.content || '',
engine: item.engine || 'unknown',
score: item.score ? Number(item.score) : undefined
}));
}
// 备用解析(非标准格式)
if (Array.isArray(data)) {
return data.map((item: any) => ({
title: item.title || '',
url: item.url || '',
content: item.content || item.snippet || '',
engine: item.engine || 'unknown',
score: item.score ? Number(item.score) : undefined
}));
}
// 空结果
return [];
}
/**
* 测试 SearXNG 实例连接
*/
async testConnection(): Promise<boolean> {
try {
await this.search('test', { maxResults: 1 });
logger.info('SearXNG connection test passed');
return true;
} catch (error) {
logger.error('SearXNG connection test failed', {
error: error instanceof Error ? error.message : 'Unknown error'
});
return false;
}
}
}