/** * web_search — 网络搜索工具 * * 双模式搜索:SearXNG 元搜索 / 内置四引擎并行搜索 * 内置引擎:Bing + 百度 + 搜狗 + 360 搜索(Promise.allSettled 容错并发) * 智能排序:引擎权重(50%) + 可达性(30%) + 摘要质量(20%) * 自动抓取:对前 N 条结果调用 web_fetch 获取完整正文 * * @see docs/Agent网络工具通用设计-v2.md — 第 2 章 web_search 搜索设计 */ import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool'; import type { MetonaToolDef } from '../../../harness/types'; import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types'; import type { ConfigService } from '../../../services/config.service'; import { searchCache, normalizeUrl, fetchWithTimeout, buildSearXNGAuthHeaders, logTool, } from './network-utils'; import type { WebFetchTool } from './web-fetch'; // ===== 类型定义 ===== interface SearchResult { title: string; url: string; snippet: string; engine: string; weight: number; reachable?: boolean; _score?: number; _enhanced?: boolean; } interface SearXNGConfig { enabled: boolean; url: string; engines: string; language: string; safesearch: number; time_range: string; max_results: number; auth_key: string; auth_type: string; format: string; fetch_count: number; fetch_mode: string; } // ===== 引擎定义 ===== interface EngineDef { name: string; weight: number; searchUrl: (query: string, timeRange?: string) => string; parse: (html: string) => SearchResult[]; } const ENGINES: EngineDef[] = [ { name: 'bing', weight: 90, searchUrl: (q, tr) => { const freshness = tr ? `&filters=ex1:"ez${tr === 'day' ? '1' : tr === 'week' ? '2' : tr === 'month' ? '3' : '4'}"` : ''; return `https://www.bing.com/search?q=${encodeURIComponent(q)}${freshness}&count=20`; }, parse: parseBing, }, { name: '百度', weight: 80, searchUrl: (q) => `https://www.baidu.com/s?wd=${encodeURIComponent(q)}&rn=20`, parse: parseBaidu, }, { name: '搜狗', weight: 75, searchUrl: (q) => `https://www.sogou.com/web?query=${encodeURIComponent(q)}&num=20`, parse: parseSogou, }, { name: '360搜索', weight: 75, searchUrl: (q) => `https://www.so.com/s?q=${encodeURIComponent(q)}&pn=20`, parse: parse360, }, ]; // ===== HTML 解析器(正则实现,后续可迁移至 cheerio) ===== function parseBing(html: string): SearchResult[] { const results: SearchResult[] = []; const blocks = html.split(/
]*>([\s\S]*?)<\/p>/i) || block.match(/class="b_caption"[^>]*>([\s\S]*?)<\/div>/i); const snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, '').trim() : ''; if (title && url && !url.includes('bing.com')) { results.push({ title, url, snippet, engine: 'bing', weight: 90 }); } } return results; } function parseBaidu(html: string): SearchResult[] { const results: SearchResult[] = []; const blocks = html.split(/