// Auto-draft generator for رادار آینده / در یک نگاه. // Scrapes recent Iranian steel news per section → Gemini (via Liara) writes // Persian analytical items → pushes them to the panel as DRAFTS for review. // // node scripts/generate-drafts.mjs // // Config + tone live in ./gen.config.mjs (gitignored). Drafts stay hidden from // the public site until you press «انتشار» in the CMS. import CFG from './gen.config.mjs' // allow pointing at a local panel for testing without editing the config const PANEL = { baseUrl: process.env.PANEL_BASE || CFG.panel.baseUrl, username: process.env.PANEL_USER || CFG.panel.username, password: process.env.PANEL_PASS || CFG.panel.password, } const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36' const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) // section → { backend category code, scrape query, page label } const CATEGORIES = [ { code: 'market', label: 'بازار و زنجیره فولاد', query: 'قیمت بازار فولاد' }, { code: 'energy', label: 'پایداری و فولاد سبز', query: 'فولاد سبز محیط زیست انرژی' }, { code: 'geo', label: 'ژئوپلیتیک و اقتصاد جهانی', query: 'تعرفه صادرات فولاد اقتصاد جهانی' }, { code: 'tech', label: 'فناوری و نوآوری', query: 'فناوری نوآوری فولاد' }, ] const NEWS_SITES = ['https://www.chilanonline.com', 'https://felezatonline.ir', 'https://madanmedia.ir', 'https://www.madannews.ir'] // ---------- helpers ---------- const decode = (s) => s.replace(//g, '$1') .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(+n)) .replace(/ | | /g, ' ') .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'") .trim() const stripTags = (s) => decode(s.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ')) const tag = (b, n) => { const m = b.match(new RegExp(`<${n}[^>]*>([\\s\\S]*?)<\\/${n}>`, 'i')); return m ? m[1] : '' } async function get(url, timeout = 18000) { const ac = new AbortController() const t = setTimeout(() => ac.abort(), timeout) try { const r = await fetch(url, { headers: { 'User-Agent': UA }, redirect: 'follow', signal: ac.signal }); return r.ok ? await r.text() : '' } catch { return '' } finally { clearTimeout(t) } } function parseRss(xml) { return [...xml.matchAll(/([\s\S]*?)<\/item>/g)].map((m) => { const b = m[1] return { title: decode(tag(b, 'title')), url: decode(tag(b, 'link')).trim(), date: tag(b, 'pubDate').trim(), excerpt: stripTags(tag(b, 'description')).slice(0, 260) } }) } async function scrape(query) { const all = [] for (const base of NEWS_SITES) { const xml = await get(`${base}/?s=${encodeURIComponent(query)}&feed=rss2`) if (xml) all.push(...parseRss(xml)) await sleep(300) } const seen = new Set() return all .filter((it) => it.title && it.url && !seen.has(it.title) && seen.add(it.title)) .sort((a, b) => (Date.parse(b.date) || 0) - (Date.parse(a.date) || 0)) .slice(0, 8) } async function panelLogin() { const res = await fetch(`${PANEL.baseUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: PANEL.username, password: PANEL.password }), }) if (!res.ok) throw new Error(`panel login failed (${res.status})`) const cookies = res.headers.getSetCookie?.() || [] const session = cookies.map((c) => c.split(';')[0]).find((c) => c.startsWith('session=')) if (!session) throw new Error('no session cookie returned') return session } function parseItems(text) { let t = text.replace(/```json|```/g, '').trim() const s = t.indexOf('{'), e = t.lastIndexOf('}') if (s === -1 || e === -1) return [] try { return JSON.parse(t.slice(s, e + 1)).items || [] } catch { return [] } } async function generate(cat, news) { const newsText = news.map((n, i) => `${i + 1}. ${n.title}\n منبع: ${n.url}\n ${n.excerpt}`).join('\n\n') const user = `دسته: «${cat.label}» از میان خبرهای زیر، ${CFG.perCategory} آیتم تحلیلیِ مستقل و متفاوت برای این دسته بنویس. هر آیتم باید یک رویداد/موضوع واقعی از خبرها را پوشش دهد. خروجی را «فقط» به‌صورت JSON و دقیقاً با این ساختار بده (بدون هیچ توضیح اضافه): {"items":[{"titleFa":"تیتر کوتاه","excerptFa":"خلاصهٔ یک تا دو جمله‌ای","bodyFa":"متن اصلی ۳ تا ۵ پاراگراف","analysisFa":"تحلیل راهبردی یک پاراگراف","tags":["برچسب","برچسب"],"sourceUrl":"نشانی منبع مرتبط"}]} خبرها: ${newsText}` const res = await fetch(`${CFG.ai.baseUrl}/chat/completions`, { method: 'POST', headers: { Authorization: `Bearer ${CFG.ai.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: CFG.ai.model, messages: [{ role: 'system', content: CFG.systemPrompt }, { role: 'user', content: user }], max_tokens: 8000, temperature: 0.7 }), }) if (!res.ok) throw new Error(`gemini ${res.status}: ${(await res.text()).slice(0, 200)}`) const j = await res.json() return parseItems(j.choices?.[0]?.message?.content || '') } async function createDraft(session, cat, item, dateFa) { const body = { category: cat.code, titleFa: String(item.titleFa || '').trim(), excerptFa: item.excerptFa || null, bodyFa: item.bodyFa || null, analysisFa: item.analysisFa || null, tags: Array.isArray(item.tags) ? item.tags : [], source: item.sourceUrl || null, dateFa, status: 'draft', } if (!body.titleFa) return null const res = await fetch(`${PANEL.baseUrl}/api/radar`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: session }, body: JSON.stringify(body), }) if (!res.ok) { console.log(` ✗ POST failed ${res.status}: ${(await res.text()).slice(0, 120)}`); return null } return (await res.json()).id } async function main() { const dateFa = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { year: 'numeric', month: 'long', day: 'numeric' }).format(new Date()) console.log(`Login to panel…`) const session = await panelLogin() let total = 0 for (const cat of CATEGORIES) { console.log(`\n■ ${cat.label} (${cat.code})`) const news = await scrape(cat.query) console.log(` scraped ${news.length} articles`) if (!news.length) { console.log(' (no news, skipping)'); continue } const items = await generate(cat, news) console.log(` gemini returned ${items.length} items`) for (const it of items) { const id = await createDraft(session, cat, it, dateFa) if (id) { total++; console.log(` ✓ draft ${id} — ${String(it.titleFa).slice(0, 50)}`) } } } console.log(`\nDone. ${total} drafts created. Review & publish them in the panel → «در یک نگاه».`) } main().catch((e) => { console.error('FATAL:', e.message); process.exit(1) })