Spaces:
Sleeping
Sleeping
| import express from 'express'; | |
| import * as cheerio from 'cheerio'; | |
| import { chromium } from 'playwright'; | |
| const app = express(); | |
| const PORT = 7860; | |
| const CACHE_TTL = 3600; | |
| const cache = new Map(); | |
| // ===== تابع تبدیل URL برتینا به لینک واقعی ===== | |
| function decodeBertinaUrl(bertinaUrl) { | |
| if (!bertinaUrl || !bertinaUrl.startsWith('/api/click')) { | |
| return bertinaUrl; | |
| } | |
| try { | |
| const url = new URL(bertinaUrl, 'https://search.bertina.ir'); | |
| const dest = url.searchParams.get('dest'); | |
| if (dest) { | |
| return Buffer.from(dest, 'base64').toString('utf-8'); | |
| } | |
| return bertinaUrl; | |
| } catch { | |
| return bertinaUrl; | |
| } | |
| } | |
| // ===== استخراج نتایج ارگانیک ===== | |
| async function searchBertinaOrganic(query) { | |
| const url = `https://search.bertina.ir/search?q=${encodeURIComponent(query)}`; | |
| console.log(`🔍 Fetching organic results from Bertina...`); | |
| try { | |
| const response = await fetch(url, { | |
| headers: { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' | |
| }, | |
| signal: AbortSignal.timeout(15000) | |
| }); | |
| if (!response.ok) throw new Error(`HTTP ${response.status}`); | |
| const html = await response.text(); | |
| const $ = cheerio.load(html); | |
| const results = []; | |
| $('article.group.animate-fade-in').each((i, el) => { | |
| const isAd = $(el).attr('data-ad-id') !== undefined; | |
| if (isAd) return; | |
| const link = $(el).find('h3 a'); | |
| const title = link.text().trim(); | |
| const href = link.attr('href') || ''; | |
| const domain = $(el).find('cite.not-italic').text().trim(); | |
| const snippet = $(el).find('p.mt-1.text-sm').text().trim(); | |
| if (title && href) { | |
| // تبدیل URL به لینک واقعی | |
| const realUrl = decodeBertinaUrl(href); | |
| results.push({ | |
| title: title.slice(0, 200), | |
| url: realUrl, | |
| snippet: snippet.slice(0, 300), | |
| site: domain || new URL(realUrl).hostname.replace('www.', '') | |
| }); | |
| } | |
| }); | |
| console.log(`✅ Organic results: ${results.length}`); | |
| return results; | |
| } catch (error) { | |
| console.error(`❌ Organic fetch error:`, error.message); | |
| return []; | |
| } | |
| } | |
| // ===== استخراج پاسخ هوش مصنوعی ===== | |
| async function searchBertinaAI(query) { | |
| console.log(`🤖 Fetching AI answer with Playwright...`); | |
| let browser; | |
| try { | |
| browser = await chromium.launch({ | |
| headless: true, | |
| args: ['--no-sandbox', '--disable-dev-shm-usage'] | |
| }); | |
| const page = await browser.newPage(); | |
| await page.goto(`https://search.bertina.ir/search?q=${encodeURIComponent(query)}`, { | |
| waitUntil: 'networkidle', | |
| timeout: 30000 | |
| }); | |
| // منتظر میمانیم تا بخش پاسخ هوش مصنوعی بارگذاری شود | |
| try { | |
| await page.waitForSelector('aside[aria-label="پاسخ هوش مصنوعی"]', { | |
| timeout: 10000 | |
| }); | |
| } catch (e) { | |
| console.log('⏭️ No AI answer section found'); | |
| return null; | |
| } | |
| // استخراج متن پاسخ با سلکتور دقیقتر | |
| // متن پاسخ داخل یک div با کلاس text-sm leading-relaxed قرار دارد | |
| const aiAnswer = await page.$eval( | |
| 'aside[aria-label="پاسخ هوش مصنوعی"] .text-sm.leading-relaxed', | |
| el => el.textContent.trim() | |
| ).catch(() => null); | |
| // اگر با سلکتور بالا پیدا نشد، از سلکتور جایگزین استفاده کن | |
| let aiAnswer2 = null; | |
| if (!aiAnswer || aiAnswer.length < 10) { | |
| aiAnswer2 = await page.$eval( | |
| 'aside[aria-label="پاسخ هوش مصنوعی"] .space-y-2', | |
| el => el.textContent.trim() | |
| ).catch(() => null); | |
| } | |
| const finalAnswer = aiAnswer || aiAnswer2; | |
| if (finalAnswer && finalAnswer.length > 10 && !finalAnswer.includes('در حال تفکر')) { | |
| console.log(`✅ AI answer extracted: ${finalAnswer.slice(0, 50)}...`); | |
| return finalAnswer; | |
| } | |
| console.log('⏭️ AI answer content is empty or still loading'); | |
| return null; | |
| } catch (error) { | |
| console.error(`❌ Playwright error:`, error.message); | |
| return null; | |
| } finally { | |
| if (browser) await browser.close(); | |
| } | |
| } | |
| // ===== جستجوی اصلی ===== | |
| async function search(query) { | |
| const cached = cache.get(query); | |
| if (cached && Date.now() - cached.timestamp < CACHE_TTL * 1000) { | |
| console.log(`✅ Cache hit for "${query}"`); | |
| return cached.results; | |
| } | |
| console.log(`🔍 Searching for: "${query}"`); | |
| // ۱. نتایج ارگانیک | |
| let results = await searchBertinaOrganic(query); | |
| // ۲. پاسخ هوش مصنوعی | |
| let aiAnswer = null; | |
| if (results.length > 0) { | |
| aiAnswer = await searchBertinaAI(query); | |
| } | |
| // ۳. اگر نتیجهای نبود، DuckDuckGo | |
| if (results.length === 0) { | |
| console.log(`🔄 Falling back to DuckDuckGo...`); | |
| try { | |
| const ddg = await fetch( | |
| `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1` | |
| ); | |
| const data = await ddg.json(); | |
| results = (data.RelatedTopics || []) | |
| .filter(t => t.FirstURL && t.Text) | |
| .slice(0, 10) | |
| .map(t => ({ | |
| title: t.Text.split('-')[0]?.trim() || t.Text, | |
| url: t.FirstURL, | |
| snippet: t.Text, | |
| site: new URL(t.FirstURL).hostname.replace('www.', '') | |
| })); | |
| } catch (e) {} | |
| } | |
| const output = { | |
| query, | |
| results, | |
| ai_answer: aiAnswer, | |
| source: results.length > 0 ? 'bertina' : 'duckduckgo' | |
| }; | |
| cache.set(query, { results: output, timestamp: Date.now() }); | |
| return output; | |
| } | |
| // ===== مسیر جستجو ===== | |
| app.get('/search', async (req, res) => { | |
| const query = req.query.q; | |
| if (!query) { | |
| return res.status(400).json({ error: 'Missing query parameter' }); | |
| } | |
| try { | |
| const result = await search(query); | |
| res.json(result); | |
| } catch (error) { | |
| console.error(`❌ Error:`, error.message); | |
| res.status(500).json({ | |
| error: error.message, | |
| query, | |
| results: [], | |
| ai_answer: null | |
| }); | |
| } | |
| }); | |
| app.get('/', (req, res) => { | |
| res.json({ | |
| status: 'running', | |
| usage: '/search?q=YOUR_QUERY', | |
| note: 'Bertina search + AI answer + real URLs' | |
| }); | |
| }); | |
| app.listen(PORT, '0.0.0.0', () => { | |
| console.log(`🚀 Server running on port ${PORT}`); | |
| console.log(`📍 Test: http://localhost:${PORT}/search?q=هوش مصنوعی`); | |
| }); |