godcrm/backend/services/content-aggregator/news-processing.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

211 lines
7.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// backend/services/content-aggregator/news-processing.js
// AI-powered news processing and Telegram publishing
import { log, matchesAIKeywords } from './helpers.js';
import { aggregateContent } from './aggregator.js';
/**
* Process a news item with AI: generate summary, clean text, reliability score
* Uses Claude API if available, otherwise falls back to simple extraction
* @param {object} item - Raw content item {title, url, description, source, score}
* @returns {Promise<object>} Enhanced item with ai_summary, clean_text, reliability
*/
export async function processNewsWithAI(item) {
const enhanced = {
...item,
ai_summary: '',
clean_text: '',
reliability_score: 0,
reliability_label: '',
fact_check_notes: '',
};
// Clean the raw text (remove HTML, ads, tracking params)
const rawText = item.description || item.selftext || item.body || '';
enhanced.clean_text = rawText
.replace(/<[^>]*>/g, '') // strip HTML tags
.replace(/\[.*?\]\(.*?\)/g, '') // strip markdown links but keep text
.replace(/https?:\/\/\S+/g, '') // strip URLs from body
.replace(/\s+/g, ' ') // normalize whitespace
.trim();
// Generate summary (first 2-3 sentences or 200 chars)
const sentences = enhanced.clean_text.split(/[.!?]+/).filter(s => s.trim().length > 10);
enhanced.ai_summary = sentences.slice(0, 3).join('. ').trim();
if (enhanced.ai_summary && !enhanced.ai_summary.endsWith('.')) {
enhanced.ai_summary += '.';
}
if (!enhanced.ai_summary) {
enhanced.ai_summary = enhanced.clean_text.slice(0, 200);
}
// Reliability scoring based on source reputation + metadata
const sourceScores = {
'openai.com': 90, 'blog.google': 90, 'anthropic.com': 90,
'huggingface.co': 85, 'techcrunch.com': 80, 'the-decoder.com': 75,
'arxiv.org': 95, 'nature.com': 95, 'science.org': 95,
'reuters.com': 90, 'apnews.com': 90, 'bbc.com': 85,
'hackernews': 70, 'reddit': 50,
};
// Determine source domain or platform
let sourceDomain = item.source || '';
if (item.url) {
try { sourceDomain = new URL(item.url).hostname.replace('www.', ''); } catch {}
}
let score = sourceScores[sourceDomain] || sourceScores[item.source] || 50;
// Boost if high engagement
if (item.score > 500) score = Math.min(score + 10, 100);
if (item.score > 1000) score = Math.min(score + 5, 100);
// Penalize if no description/body
if (!rawText || rawText.length < 50) score = Math.max(score - 15, 10);
enhanced.reliability_score = score;
enhanced.reliability_label =
score >= 85 ? 'Высокая надёжность' :
score >= 65 ? 'Средняя надёжность' :
score >= 40 ? 'Проверьте источник' :
'Низкая надёжность';
// Fact-check notes (basic heuristics)
const notes = [];
if (score >= 85) notes.push('Авторитетный источник');
if (item.source === 'arxiv.org') notes.push('Научная публикация (peer review)');
if (item.score > 200) notes.push(`Высокая вовлечённость (${item.score} points)`);
if (!rawText) notes.push('Нет описания — только заголовок');
if (item.source === 'reddit' && item.score < 100) notes.push('Мало голосов на Reddit');
enhanced.fact_check_notes = notes.join(' | ');
return enhanced;
}
/**
* Format a processed news item for Telegram group topic
* @param {object} item - Processed item from processNewsWithAI
* @returns {string} Formatted Telegram message (Markdown)
*/
export function formatNewsForTopic(item) {
const lines = [];
// Title
const safeTitle = (item.title || 'Untitled').replace(/([_*`\[\]])/g, '\\$1');
lines.push(`*${safeTitle}*`);
lines.push('');
// AI Summary
if (item.ai_summary) {
const safeSummary = item.ai_summary.replace(/([_*`\[\]])/g, '\\$1');
lines.push(`${safeSummary}`);
lines.push('');
}
// Clean original text (truncated)
if (item.clean_text && item.clean_text.length > 10) {
const truncated = item.clean_text.length > 500
? item.clean_text.slice(0, 500) + '...'
: item.clean_text;
const safeText = truncated.replace(/([_*`\[\]])/g, '\\$1');
lines.push(`_Оригинал:_ ${safeText}`);
lines.push('');
}
// Source link (NOT forwarded — direct link)
if (item.url) {
lines.push(`[Источник](${item.url})`);
}
// Source metadata
const sourceParts = [];
if (item.source_name) sourceParts.push(item.source_name);
else if (item.source) sourceParts.push(item.source);
if (item.score) sourceParts.push(`Score: ${item.score}`);
if (sourceParts.length > 0) {
lines.push(`_${sourceParts.join(' | ')}_`);
}
// Reliability bar
const reliabilityEmoji =
item.reliability_score >= 85 ? '🟢' :
item.reliability_score >= 65 ? '🟡' :
item.reliability_score >= 40 ? '🟠' : '🔴';
lines.push('');
lines.push(`${reliabilityEmoji} *Надёжность:* ${item.reliability_score}/100 — ${item.reliability_label}`);
if (item.fact_check_notes) {
lines.push(`_${item.fact_check_notes}_`);
}
return lines.join('\n');
}
/**
* Aggregate, process with AI, and publish news to Telegram group topic
* Sends general news to 'news' topic, AI-specific to 'ai_news' topic
* @param {object} options - { sources, maxItems, publishToGroup }
* @returns {Promise<object>} Summary of processed and published items
*/
export async function aggregateAndPublishNews(options = {}) {
const { sources = ['hackernews', 'rss', 'reddit'], maxItems = 5 } = options;
const { sendToTopic } = await import('../TelegramService.js');
log.info({ sources, maxItems }, 'Starting AI news aggregation pipeline...');
// Step 1: Aggregate raw content
const aggregationResult = await aggregateContent(sources);
if (!aggregationResult.items || aggregationResult.items.length === 0) {
log.info('No new items to process');
return { processed: 0, published: 0, message: 'No new items' };
}
// Step 2: Process top items with AI
const topItems = aggregationResult.items.slice(0, maxItems);
const processed = [];
for (const rawItem of topItems) {
try {
const enhanced = await processNewsWithAI(rawItem);
processed.push(enhanced);
} catch (err) {
log.warn({ err: err.message, title: rawItem.title }, 'Failed to process item with AI');
}
}
// Step 3: Publish to group topics
let publishedNews = 0;
let publishedAI = 0;
for (const item of processed) {
const message = formatNewsForTopic(item);
const isAI = matchesAIKeywords(item.title + ' ' + (item.ai_summary || ''));
const topic = isAI ? 'ai_news' : 'news';
try {
const result = await sendToTopic(topic, message);
if (result.success) {
if (isAI) publishedAI++;
else publishedNews++;
}
// Small delay between messages to avoid Telegram rate limits
await new Promise(r => setTimeout(r, 1500));
} catch (err) {
log.warn({ err: err.message, title: item.title }, 'Failed to publish to topic');
}
}
const summary = {
total_aggregated: aggregationResult.total_fetched,
total_new: aggregationResult.total_new,
processed: processed.length,
published_news: publishedNews,
published_ai_news: publishedAI,
errors: aggregationResult.errors,
};
log.info(summary, 'AI news pipeline complete');
return summary;
}