// ============================================================ // Web Search Adapter — Tìm kiếm web và trả kết quả // Phụ thuộc: currentLang, _adapterPath // // Chiến lược (không cần API key, không cần CORS proxy): // 1. DuckDuckGo Instant Answer API (có CORS headers, trả tóm tắt Wikipedia) // 2. Fallback → link tìm kiếm clickable (Google + DuckDuckGo + Bing) // // Tùy chọn: Google Custom Search API (cần GOOGLE_API_KEY + GOOGLE_CX) // ============================================================ var GOOGLE_API_KEY = ''; var GOOGLE_CX = ''; var WEB_SEARCH_TIMEOUT = 5000; // ---- DuckDuckGo Instant Answer API ---- async function duckDuckGoSearch(query) { // DDG API hỗ trợ CORS — gọi trực tiếp từ browser var url = 'https://api.duckduckgo.com/?q=' + encodeURIComponent(query) + '&format=json&no_html=1&skip_disambig=1'; var controller = new AbortController(); var timeoutId = setTimeout(function () { controller.abort(); }, WEB_SEARCH_TIMEOUT); try { var response = await fetch(url, { signal: controller.signal }); if (!response.ok) return null; var data = await response.json(); var results = { abstract: data.Abstract || '', abstractSource: data.AbstractSource || '', abstractURL: data.AbstractURL || '', answer: data.Answer || '', definition: data.Definition || '', definitionSource: data.DefinitionSource || '', definitionURL: data.DefinitionURL || '', related: [] }; if (data.RelatedTopics) { for (var i = 0; i < Math.min(data.RelatedTopics.length, 5); i++) { var topic = data.RelatedTopics[i]; if (topic && topic.Text) { results.related.push({ text: topic.Text, url: topic.FirstURL || '' }); } } } if (!results.abstract && !results.answer && !results.definition && results.related.length === 0) { return null; } return results; } catch (err) { return null; } finally { clearTimeout(timeoutId); } } // ---- Google Custom Search API (cần key) ---- async function googleSearch(query, numResults) { if (!GOOGLE_API_KEY || !GOOGLE_CX) return null; numResults = numResults || 3; var url = 'https://www.googleapis.com/customsearch/v1' + '?key=' + encodeURIComponent(GOOGLE_API_KEY) + '&cx=' + encodeURIComponent(GOOGLE_CX) + '&q=' + encodeURIComponent(query) + '&num=' + numResults; var controller = new AbortController(); var timeoutId = setTimeout(function () { controller.abort(); }, WEB_SEARCH_TIMEOUT); try { var response = await fetch(url, { signal: controller.signal }); if (!response.ok) return null; var data = await response.json(); if (!data.items || data.items.length === 0) return null; return data.items.map(function (item) { return { title: item.title || '', link: item.link || '', snippet: item.snippet || '' }; }); } catch (err) { return null; } finally { clearTimeout(timeoutId); } } // ---- Format ---- function formatDDGResults(results, query, lang) { var lines = []; if (lang === 'en') lines.push('🔍 Search results for "' + query + '":'); else if (lang === 'ja') lines.push('🔍 「' + query + '」の検索結果:'); else lines.push('🔍 Kết quả tìm kiếm "' + query + '":'); if (results.answer) lines.push('💡 ' + results.answer); if (results.abstract) { lines.push('\n' + results.abstract); if (results.abstractSource && results.abstractURL) { lines.push('📖 Nguồn: ' + results.abstractSource + ' — ' + results.abstractURL); } } if (results.definition) { lines.push('📝 ' + results.definition); if (results.definitionSource) lines.push(' — ' + results.definitionSource); } if (results.related.length > 0) { lines.push(''); if (lang === 'en') lines.push('Related:'); else if (lang === 'ja') lines.push('関連:'); else lines.push('Liên quan:'); for (var i = 0; i < results.related.length; i++) { var r = results.related[i]; lines.push('• ' + r.text); if (r.url) lines.push(' 🔗 ' + r.url); } } // Luôn thêm link tìm kiếm đầy đủ lines.push(''); lines.push(_searchLinksOnly(query)); return lines.join('\n'); } function formatGoogleResults(results, query, lang) { var lines = []; if (lang === 'en') lines.push('🔍 Search results for "' + query + '":'); else if (lang === 'ja') lines.push('🔍 「' + query + '」の検索結果:'); else lines.push('🔍 Kết quả tìm kiếm "' + query + '":'); for (var i = 0; i < results.length; i++) { lines.push((i + 1) + '. ' + results[i].title); if (results[i].snippet) lines.push(' ' + results[i].snippet); if (results[i].link) lines.push(' 🔗 ' + results[i].link); } return lines.join('\n'); } function formatSearchLinks(query, lang) { var lines = []; if (lang === 'en') lines.push('🔍 Here are search links for "' + query + '":'); else if (lang === 'ja') lines.push('🔍 「' + query + '」の検索リンク:'); else lines.push('🔍 Link tìm kiếm "' + query + '":'); lines.push(_searchLinksOnly(query)); return lines.join('\n'); } function _searchLinksOnly(query) { var encoded = encodeURIComponent(query); return '🌐 Google: https://www.google.com/search?q=' + encoded + '\n🦆 DuckDuckGo: https://duckduckgo.com/?q=' + encoded + '\n🔵 Bing: https://www.bing.com/search?q=' + encoded; } // ---- Web Search Adapter ---- function webSearchAdapter(rs, args) { _adapterPath.push('web_search'); var query = (args || []).join(' ').trim(); var lang = currentLang || 'vi'; if (query.length === 0) { if (lang === 'en') return Promise.resolve('Please provide a search query.'); if (lang === 'ja') return Promise.resolve('検索キーワードを入力してください。'); return Promise.resolve('Vui lòng nhập từ khóa tìm kiếm.'); } // Ưu tiên Google nếu có key if (GOOGLE_API_KEY && GOOGLE_CX) { return googleSearch(query).then(function (results) { if (results) return formatGoogleResults(results, query, lang); return _ddgFallback(query, lang); }).catch(function () { return _ddgFallback(query, lang); }); } return _ddgFallback(query, lang); } function _ddgFallback(query, lang) { return duckDuckGoSearch(query).then(function (results) { if (results) return formatDDGResults(results, query, lang); // DDG không có instant answer → trả link tìm kiếm return formatSearchLinks(query, lang); }).catch(function () { return formatSearchLinks(query, lang); }); } // Node/test: export to globalThis if (typeof module !== 'undefined' && module.exports) { globalThis.GOOGLE_API_KEY = GOOGLE_API_KEY; globalThis.GOOGLE_CX = GOOGLE_CX; globalThis.duckDuckGoSearch = duckDuckGoSearch; globalThis.googleSearch = googleSearch; globalThis.webSearchAdapter = webSearchAdapter; globalThis.formatSearchLinks = formatSearchLinks; }