# SearXNG + Creeper 集成 - 需求文档
> 生成时间:2025-12-05
> 基于项目:web-analysis-mcp
> 技术栈:TypeScript + Node.js + MCP SDK
---
## 项目概况
**技术栈**:TypeScript 5.6 + Node.js 18+ + @modelcontextprotocol/sdk + zod + axios
**架构模式**:分层架构(tools → services → external APIs)
**代码风格**:camelCase 函数 / PascalCase 类 / kebab-case 文件
---
## 改动点
### 要实现什么
1. **替换搜索服务**:删除 Tavily,使用 SearXNG 作为搜索引擎
2. **集成 Creeper 爬虫**:通过子进程调用 Python Creeper 爬取网页内容
3. **规则过滤器**:基于排名、域名、关键词过滤搜索结果(不用 LLM)
4. **优化总结策略**:单次总结 / Map-Reduce 总结(根据内容大小自动选择)
### 优化后的数据流
```
用户查询
↓
SearXNG 搜索(获取 10-20 个结果)
↓
规则过滤(取前 5-8 个高质量链接)
- 按搜索排名优先
- 排除黑名单域名
- 保留白名单域名优先
↓
Creeper 并行爬取(concurrency=5)
↓
内容大小判断
├─ < 50KB → 单次 LLM 总结
└─ ≥ 50KB → Map-Reduce 总结
↓
返回结果 + 来源链接
```
### 与现有功能的关系
| 现有模块 | 处理方式 |
|---------|---------|
| `services/tavily.ts` | **删除** |
| `services/fetcher.ts` | **保留**(batch_fetch 工具仍需使用) |
| `services/summarizer.ts` | **复用**(添加 Map-Reduce 方法) |
| `tools/web-search.ts` | **重写**(使用新流程) |
| `tools/fetch-summary.ts` | **保留** |
| `tools/batch-fetch.ts` | **保留** |
| `server.ts` | **修改**(更新服务初始化) |
### 新增依赖
无需新增 npm 依赖,使用现有的 axios 调用 SearXNG API,使用 child_process 调用 Creeper。
---
## 实现方案
### 需要删除的文件
```
src/services/tavily.ts # 删除 Tavily MCP 客户端
```
### 需要新增的文件
```
src/services/searxng.ts # SearXNG 搜索服务
src/services/creeper.ts # Creeper 爬虫服务(子进程调用)
src/services/filter.ts # 规则过滤器
src/utils/map-reduce.ts # Map-Reduce 总结工具
tests/inspector/searxng.test.json # SearXNG 工具测试
```
### 需要修改的文件
```
src/server.ts # 修改:删除 Tavily 初始化,添加 SearXNG/Creeper
src/tools/web-search.ts # 修改:使用新的搜索流程
src/utils/config.ts # 修改:添加 SearXNG/Creeper 配置
src/types/index.ts # 修改:添加新类型定义
```
---
## 详细设计
### 1. SearXNG 服务 (`services/searxng.ts`)
```typescript
interface SearXNGConfig {
baseUrl: string; // SearXNG 实例地址
timeout?: number; // 请求超时(默认 10s)
}
interface SearXNGResult {
title: string;
url: string;
content: string; // 摘要描述
engine: string; // 来源引擎
score?: number; // 相关性分数
}
class SearXNGService {
// 执行搜索,返回结果列表
async search(query: string, options?: {
categories?: string[]; // 搜索类别
language?: string; // 语言
timeRange?: string; // 时间范围
maxResults?: number; // 最大结果数(默认 20)
}): Promise<SearXNGResult[]>
}
```
**API 调用**:
```
GET {baseUrl}/search?q={query}&format=json&language={lang}
```
### 2. Creeper 服务 (`services/creeper.ts`)
```typescript
interface CreeperConfig {
pythonPath?: string; // Python 路径(默认 python)
scriptPath: string; // creeper.py 路径
concurrency?: number; // 并发数(默认 5)
timeout?: number; // 超时(默认 60s)
}
interface CreeperResult {
url: string;
title: string;
summary: string; // 页面描述
content: string; // 页面内容(Markdown)
success: boolean;
error?: string;
}
class CreeperService {
// 批量爬取 URL
async crawl(urls: string[]): Promise<CreeperResult[]>
}
```
**子进程调用**:
```bash
python creeper.py --urls "URL1,URL2,URL3" -c 5
```
**输出解析**:stdout 为 JSON 数组
### 3. 规则过滤器 (`services/filter.ts`)
```typescript
interface FilterConfig {
maxResults: number; // 最大保留数(默认 8)
domainBlacklist?: string[]; // 域名黑名单
domainWhitelist?: string[]; // 域名白名单(优先保留)
}
class ResultFilter {
// 过滤搜索结果
filter(results: SearXNGResult[], query: string): SearXNGResult[]
}
```
**过滤策略**(按优先级):
1. 排除黑名单域名
2. 白名单域名优先排序
3. 按原始搜索排名保留前 N 个
### 4. Map-Reduce 总结 (`utils/map-reduce.ts`)
```typescript
interface MapReduceOptions {
chunkSize: number; // 每个 chunk 的最大字符数(默认 30000)
mapPrompt?: string; // Map 阶段提示词
reducePrompt?: string; // Reduce 阶段提示词
}
// Map: 并行总结每个网页(提取关键点)
// Reduce: 合并所有关键点生成最终总结
async function mapReduceSummarize(
contents: Array<{url: string; content: string}>,
summarizer: SummarizerService,
options?: MapReduceOptions
): Promise<string>
```
### 5. 配置更新 (`utils/config.ts`)
**新增环境变量**:
| 变量 | 必需 | 默认值 | 说明 |
|------|------|--------|------|
| `SEARXNG_BASE_URL` | 是 | - | SearXNG 实例地址 |
| `CREEPER_PATH` | 是 | - | creeper.py 所在目录 |
| `CREEPER_PYTHON` | 否 | `python` | Python 解释器路径 |
| `CREEPER_CONCURRENCY` | 否 | `5` | Creeper 并发数 |
| `FILTER_MAX_RESULTS` | 否 | `8` | 过滤后保留的最大结果数 |
| `DOMAIN_BLACKLIST` | 否 | - | 域名黑名单(逗号分隔) |
| `DOMAIN_WHITELIST` | 否 | - | 域名白名单(逗号分隔) |
| `MAP_REDUCE_THRESHOLD` | 否 | `50000` | 触发 Map-Reduce 的内容阈值 |
**删除环境变量**:
- `TAVILY_MCP_PATH`
- `TAVILY_API_KEY`
### 6. web_search 工具重写 (`tools/web-search.ts`)
**输入参数**(保持兼容):
```typescript
const webSearchInputSchema = z.object({
query: z.string().describe('搜索查询'),
max_results: z.number().optional().default(10),
language: z.string().optional().default('zh'),
time_range: z.enum(['day', 'week', 'month', 'year']).optional(),
include_domains: z.array(z.string()).optional(),
exclude_domains: z.array(z.string()).optional(),
});
```
**执行流程**:
```typescript
async function executeWebSearch(input, services, config) {
// 1. SearXNG 搜索
const searchResults = await searxngService.search(input.query, {
language: input.language,
timeRange: input.time_range,
maxResults: input.max_results * 2, // 多取一些供过滤
});
// 2. 规则过滤
const filteredResults = filter.filter(searchResults, input.query, {
maxResults: input.max_results,
domainWhitelist: input.include_domains,
domainBlacklist: input.exclude_domains,
});
// 3. Creeper 爬取
const urls = filteredResults.map(r => r.url);
const crawledPages = await creeperService.crawl(urls);
// 4. 计算内容总大小
const totalLength = crawledPages
.filter(p => p.success)
.reduce((sum, p) => sum + p.content.length, 0);
// 5. 选择总结策略
let summary: string;
if (totalLength < config.mapReduceThreshold) {
// 单次总结
const combinedContent = formatForSummary(crawledPages);
summary = await summarizer.summarize(combinedContent, SEARCH_SUMMARY_PROMPT);
} else {
// Map-Reduce 总结
summary = await mapReduceSummarize(crawledPages, summarizer);
}
// 6. 格式化输出
return formatOutput(summary, crawledPages);
}
```
---
## 实施步骤
**步骤 1:环境准备**
- [ ] 确认 SearXNG 实例可访问
- [ ] 确认 Creeper 项目路径正确
- [ ] 更新 `.env.example` 添加新配置项
**步骤 2:删除 Tavily**
- [ ] 删除 `src/services/tavily.ts`
- [ ] 从 `server.ts` 移除 Tavily 相关代码
**步骤 3:实现新服务**
- [ ] 实现 `src/services/searxng.ts`(SearXNG 搜索服务)
- [ ] 实现 `src/services/creeper.ts`(Creeper 爬虫服务)
- [ ] 实现 `src/services/filter.ts`(规则过滤器)
- [ ] 实现 `src/utils/map-reduce.ts`(Map-Reduce 总结)
**步骤 4:更新配置**
- [ ] 更新 `src/utils/config.ts` 添加新配置项
- [ ] 更新 `src/types/index.ts` 添加新类型
**步骤 5:重写 web_search**
- [ ] 重写 `src/tools/web-search.ts` 使用新流程
- [ ] 更新 `src/server.ts` 初始化新服务
**步骤 6:测试**
- [ ] 创建 `tests/inspector/searxng.test.json`
- [ ] 运行 `npm run test:dev` 测试新功能
- [ ] 回归测试 `fetch_and_summarize` 和 `batch_fetch`
**步骤 7:文档更新**
- [ ] 更新 CHANGELOG.md
- [ ] 更新 README.md(环境变量、使用说明)
- [ ] 更新 CLAUDE.md(架构变更)
**步骤 8:提交代码**
```bash
git add .
git commit -m "feat: 替换 Tavily 为 SearXNG + Creeper 搜索方案
- 删除 Tavily MCP 客户端依赖
- 新增 SearXNG 搜索服务
- 新增 Creeper 爬虫服务(子进程调用)
- 新增规则过滤器(域名黑白名单)
- 新增 Map-Reduce 总结策略
- 优化搜索流程,减少 LLM 调用次数
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>"
```
**步骤 9:自我检查**
- [ ] 新功能是否按需求正常工作
- [ ] 现有功能是否正常(fetch_and_summarize, batch_fetch)
- [ ] CHANGELOG.md 是否已更新
- [ ] README.md 是否已更新
- [ ] 所有相关文件是否已提交
---
## 使用方式
### 环境变量配置
```bash
# SearXNG 配置(必需)
SEARXNG_BASE_URL=http://localhost:8080
# Creeper 配置(必需)
CREEPER_PATH=/home/lyf/workspaces/creeper
CREEPER_PYTHON=python
CREEPER_CONCURRENCY=5
# 过滤配置(可选)
FILTER_MAX_RESULTS=8
DOMAIN_BLACKLIST=pinterest.com,facebook.com
DOMAIN_WHITELIST=github.com,stackoverflow.com
# 总结配置(可选)
MAP_REDUCE_THRESHOLD=50000
# LLM 配置(保持不变)
SUMMARY_API_KEY=your-deepseek-api-key
SUMMARY_BASE_URL=https://api.deepseek.com
SUMMARY_MODEL=deepseek-chat
```
### MCP 工具调用
```json
{
"tool": "web_search",
"arguments": {
"query": "TypeScript 最佳实践 2024",
"max_results": 5,
"language": "zh",
"time_range": "year",
"include_domains": ["github.com", "dev.to"],
"exclude_domains": ["medium.com"]
}
}
```
---
## 注意事项
**技术风险**:
- SearXNG 实例需要稳定运行,建议自建而非使用公共实例
- Creeper 依赖 Python 环境和 Playwright,需确保环境完整
- 子进程调用有额外开销,但换来更强的爬取能力
**性能考虑**:
- 规则过滤替代 LLM 过滤,节省 1 次 LLM 调用
- Map-Reduce 仅在内容量大时触发,小内容单次总结更快
- Creeper 并发爬取,比串行快 3-5 倍
**兼容性**:
- `web_search` 工具参数保持向后兼容
- `fetch_and_summarize` 和 `batch_fetch` 不受影响
- 环境变量变更:删除 `TAVILY_*`,新增 `SEARXNG_*` 和 `CREEPER_*`