Spaces:
Build error
Build error
File size: 5,936 Bytes
ef73937 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | import { scrapeQueueRepository } from '@core/storage/repositories/scrapeQueue.js';
import { channelRepository } from '@core/storage/repositories/channels.js';
import { userRepository } from '@core/storage/repositories/users.js';
import { logRepository } from '@core/storage/repositories/logs.js';
import { config } from '@config/index.js';
import { channelScraper } from '@modules/channels/scraper.js';
import { groqModerator } from '@modules/channels/ai.js';
import { sleep } from '@core/utils/helpers.js';
async function processScrapeQueue() {
console.log('🔄 Starting scrape worker...');
// Initialize repositories
await Promise.all([
scrapeQueueRepository.initialize(),
channelRepository.initialize(),
userRepository.initialize(),
logRepository.initialize(),
]);
let running = true;
let consecutiveEmpty = 0;
// Handle graceful shutdown
process.on('SIGINT', () => { running = false; });
process.on('SIGTERM', () => { running = false; });
while (running) {
try {
const item = await scrapeQueueRepository.getNextPending();
if (!item) {
consecutiveEmpty++;
if (consecutiveEmpty > 10) {
// Wait longer when queue is empty
await sleep(config.SCRAPER_INTERVAL_MS * 5);
} else {
await sleep(config.SCRAPER_INTERVAL_MS);
}
continue;
}
consecutiveEmpty = 0;
console.log(`📦 Processing queue item: ${item.id} (${item.channelUrl})`);
await processQueueItem(item);
// Rate limiting: wait between requests
await sleep(config.SCRAPER_INTERVAL_MS);
} catch (error) {
console.error('Scrape worker error:', error);
await logRepository.log({
level: 'error',
message: 'Scrape worker error',
error: { name: 'Error', message: String(error), stack: String(error) },
});
await sleep(5000);
}
}
console.log('🛑 Scrape worker stopped');
}
async function processQueueItem(item: any) {
const { id, channelUrl, userId, channelId, type } = item;
try {
// Update status to processing
await scrapeQueueRepository.updateStatus(id, 'processing');
// Step 1: Scrape channel data
await scrapeQueueRepository.updateStatus(id, 'scraping');
const scrapedData = await channelScraper.scrape(channelUrl);
if (!scrapedData) {
await handleFailure(id, 'Failed to scrape channel data', userId, channelUrl);
return;
}
// Update queue with scraped data
await scrapeQueueRepository.setScrapedData(id, scrapedData);
// If update type, update existing channel
if (type === 'update' && channelId) {
await channelRepository.updateFromScrapedData(channelId, scrapedData);
await channelRepository.updateScrapedAt(channelId);
await scrapeQueueRepository.updateStatus(id, 'completed');
console.log(`✅ Updated channel: ${channelId}`);
return;
}
// Step 2: Generate verification code (for new channels)
await scrapeQueueRepository.updateStatus(id, 'validating');
const verificationCode = item.validationCode || generateVerificationCode();
// Note: In real implementation, the verification code should already be set
// when the channel was added to queue. Here we just verify it exists.
if (!item.validationCode) {
await scrapeQueueRepository.setValidationCode(id, verificationCode);
}
// Step 3: AI Moderation
await scrapeQueueRepository.updateStatus(id, 'ai_check');
const aiResult = await groqModerator.moderate(scrapedData.description);
if (!aiResult.safe) {
await handleFailure(id, 'Content rejected by AI moderation', userId, channelUrl, aiResult.modelUsed);
return;
}
// Step 4: For new channels, verification is manual (owner must add code to description)
// The worker waits for manual verification - in practice this would be triggered
// by a separate verification check or webhook
if (type === 'new') {
await scrapeQueueRepository.updateStatus(id, 'validating');
console.log(`⏳ Channel ${id} awaiting verification code: ${verificationCode}`);
// In production, you'd have a separate verification checker
// For now, mark as completed to allow manual verification flow
await scrapeQueueRepository.updateStatus(id, 'completed');
} else {
await scrapeQueueRepository.updateStatus(id, 'completed');
}
console.log(`✅ Scraping completed for: ${scrapedData.name}`);
} catch (error) {
console.error(`Error processing queue item ${id}:`, error);
await handleFailure(id, String(error), userId, channelUrl);
}
}
async function handleFailure(queueId: string, error: string, userId: string, channelUrl: string, modelUsed?: string) {
const item = await scrapeQueueRepository.findById(queueId);
if (!item) return;
if (item.attempts + 1 >= item.maxAttempts) {
await scrapeQueueRepository.updateStatus(queueId, 'failed', { error });
// If it was a new channel, decrement user's channel count
if (item.type === 'new') {
await userRepository.decrementChannelCount(userId);
}
await logRepository.log({
level: 'error',
message: 'Scrape queue item failed permanently',
context: { queueId, channelUrl, error, attempts: item.attempts + 1, modelUsed },
userId,
});
} else {
await scrapeQueueRepository.incrementAttempts(queueId, error);
await logRepository.log({
level: 'warn',
message: 'Scrape queue item failed, will retry',
context: { queueId, channelUrl, error, attempt: item.attempts + 1 },
userId,
});
}
}
function generateVerificationCode(): string {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let code = '';
for (let i = 0; i < 6; i++) {
code += chars[Math.floor(Math.random() * chars.length)];
}
return code;
}
// Start worker
processScrapeQueue().catch(console.error); |