118 lines
3.5 KiB
JavaScript
118 lines
3.5 KiB
JavaScript
|
|
import express from 'express';
|
||
|
|
|
||
|
|
const router = express.Router();
|
||
|
|
|
||
|
|
// DuckDuckGo Instant Answer API - no key required
|
||
|
|
async function ddgInstantAnswer(query) {
|
||
|
|
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
|
||
|
|
const res = await fetch(url, {
|
||
|
|
headers: { 'User-Agent': 'IQAI-Dashboard/1.0' }
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error('DDG search failed');
|
||
|
|
return res.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
// DuckDuckGo HTML search scraper for more results
|
||
|
|
async function ddgWebSearch(query) {
|
||
|
|
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
||
|
|
const res = await fetch(url, {
|
||
|
|
headers: {
|
||
|
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||
|
|
'Accept': 'text/html'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
if (!res.ok) return [];
|
||
|
|
const html = await res.text();
|
||
|
|
|
||
|
|
// Parse result links and snippets from DDG HTML
|
||
|
|
const results = [];
|
||
|
|
const resultRegex = /<a[^>]+class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
||
|
|
const snippetRegex = /<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
||
|
|
|
||
|
|
const links = [...html.matchAll(resultRegex)].slice(0, 8);
|
||
|
|
const snippets = [...html.matchAll(snippetRegex)].slice(0, 8);
|
||
|
|
|
||
|
|
for (let i = 0; i < Math.min(links.length, 5); i++) {
|
||
|
|
const title = links[i][2].replace(/<[^>]+>/g, '').trim();
|
||
|
|
const snippet = snippets[i] ? snippets[i][1].replace(/<[^>]+>/g, '').trim() : '';
|
||
|
|
const href = links[i][1];
|
||
|
|
// DDG redirects through their own URLs, extract the real URL
|
||
|
|
const realUrl = href.startsWith('//duckduckgo.com/l/?uddg=')
|
||
|
|
? decodeURIComponent(href.split('uddg=')[1]?.split('&')[0] || href)
|
||
|
|
: href;
|
||
|
|
if (title && !title.includes('DuckDuckGo')) {
|
||
|
|
results.push({ title, snippet, url: realUrl });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return results;
|
||
|
|
}
|
||
|
|
|
||
|
|
router.get('/', async (req, res) => {
|
||
|
|
const { q } = req.query;
|
||
|
|
if (!q) return res.status(400).json({ error: 'Query parameter q is required' });
|
||
|
|
|
||
|
|
try {
|
||
|
|
const [instant, webResults] = await Promise.allSettled([
|
||
|
|
ddgInstantAnswer(q),
|
||
|
|
ddgWebSearch(q)
|
||
|
|
]);
|
||
|
|
|
||
|
|
const answer = instant.status === 'fulfilled' ? instant.value : null;
|
||
|
|
const results = webResults.status === 'fulfilled' ? webResults.value : [];
|
||
|
|
|
||
|
|
// Format for injection into AI prompt
|
||
|
|
const formatted = formatSearchResults(q, answer, results);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
query: q,
|
||
|
|
answer,
|
||
|
|
results,
|
||
|
|
formatted
|
||
|
|
});
|
||
|
|
} catch (err) {
|
||
|
|
res.status(500).json({ error: err.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
function formatSearchResults(query, instant, webResults) {
|
||
|
|
const lines = [`[WEB SEARCH RESULTS FOR: "${query}"]`, ''];
|
||
|
|
|
||
|
|
if (instant?.AbstractText) {
|
||
|
|
lines.push(`Summary: ${instant.AbstractText}`);
|
||
|
|
if (instant.AbstractSource) lines.push(`Source: ${instant.AbstractSource}`);
|
||
|
|
lines.push('');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (instant?.Answer) {
|
||
|
|
lines.push(`Direct Answer: ${instant.Answer}`);
|
||
|
|
lines.push('');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (webResults.length > 0) {
|
||
|
|
lines.push('Web Results:');
|
||
|
|
webResults.forEach((r, i) => {
|
||
|
|
lines.push(`${i + 1}. ${r.title}`);
|
||
|
|
if (r.snippet) lines.push(` ${r.snippet}`);
|
||
|
|
lines.push(` URL: ${r.url}`);
|
||
|
|
});
|
||
|
|
lines.push('');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (instant?.RelatedTopics?.length > 0) {
|
||
|
|
const topics = instant.RelatedTopics
|
||
|
|
.filter(t => t.Text)
|
||
|
|
.slice(0, 3)
|
||
|
|
.map(t => `- ${t.Text}`);
|
||
|
|
if (topics.length > 0) {
|
||
|
|
lines.push('Related Topics:');
|
||
|
|
lines.push(...topics);
|
||
|
|
lines.push('');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
lines.push('[END SEARCH RESULTS]');
|
||
|
|
return lines.join('\n');
|
||
|
|
}
|
||
|
|
|
||
|
|
export { router as searchRouter };
|