export interface WebSearchResult { title: string; snippet: string; url: string; updatedAt?: string; } export interface WebSearchResponse { provider: 'wikipedia'; query: string; language: 'en' | 'ru'; results: WebSearchResult[]; } function plainText(value: unknown): string { return String(value ?? '') .replace(/<[^>]*>/gu, '') .replaceAll('"', '"') .replaceAll(''', "'") .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replace(/\s+/gu, ' ') .trim() .slice(0, 1200); } export async function searchWikipedia( query: string, requestedLanguage?: string, requestedLimit?: number, signal?: AbortSignal, fetchImpl: typeof fetch = fetch, ): Promise { const normalizedQuery = query.trim().slice(0, 500); if (!normalizedQuery) throw new Error('web_search.query must be a non-empty string'); const language: 'en' | 'ru' = requestedLanguage === 'ru' || (requestedLanguage !== 'en' && /\p{Script=Cyrillic}/u.test(normalizedQuery)) ? 'ru' : 'en'; const limit = Math.min(8, Math.max(1, Math.round(requestedLimit ?? 5))); const params = new URLSearchParams({ action: 'query', list: 'search', srsearch: normalizedQuery, srlimit: String(limit), srprop: 'snippet|timestamp', format: 'json', formatversion: '2', origin: '*', }); const controller = new AbortController(); const abort = () => controller.abort(signal?.reason); signal?.addEventListener('abort', abort, { once: true }); const timer = setTimeout(() => controller.abort(new DOMException('web_search timed out', 'TimeoutError')), 8000); try { const response = await fetchImpl(`https://${language}.wikipedia.org/w/api.php?${params}`, { method: 'GET', credentials: 'omit', referrerPolicy: 'no-referrer', signal: controller.signal, }); if (!response.ok) throw new Error(`web_search provider returned HTTP ${response.status}`); const payload = await response.json() as { query?: { search?: Array<{ title?: unknown; snippet?: unknown; timestamp?: unknown }> }; error?: { info?: unknown }; }; if (payload.error) throw new Error(`web_search provider error: ${String(payload.error.info ?? 'unknown error')}`); const results = (payload.query?.search ?? []).slice(0, limit).map((item) => { const title = plainText(item.title); return { title, snippet: plainText(item.snippet), url: `https://${language}.wikipedia.org/wiki/${encodeURIComponent(title.replaceAll(' ', '_'))}`, ...(typeof item.timestamp === 'string' ? { updatedAt: item.timestamp } : {}), }; }); return { provider: 'wikipedia', query: normalizedQuery, language, results }; } finally { clearTimeout(timer); signal?.removeEventListener('abort', abort); } }