mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Corrections et améliorations du système RSS et de l'assistant IA: RSS: - Correction de la route backend /api/rss/articles pour respecter le paramètre limit - Changement de LIMIT 5 fixe à LIMIT ? avec paramètre dynamique - Support de 14 articles avec pagination de 7 par page - Amélioration du cache pour supporter différentes limites Assistant IA: - Suppression de la vérification settings.openrouter_api_key côté frontend - Les modèles se chargent maintenant automatiquement via le backend - Retrait de la section "Modèle IA" dans l'administration OpenRouter - Amélioration de la gestion des erreurs lors du chargement des modèles Les utilisateurs peuvent maintenant voir tous les modèles OpenRouter disponibles dans le sélecteur du chatbox, sans configuration manuelle dans l'admin.
390 lines
12 KiB
JavaScript
390 lines
12 KiB
JavaScript
// Routes de gestion des flux RSS
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const Parser = require('rss-parser');
|
|
const axios = require('axios');
|
|
const { getAll, getOne, runQuery } = require('../config/database');
|
|
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
|
const logger = require('../config/logger');
|
|
|
|
const parser = new Parser();
|
|
|
|
// Routes admin (gestion des flux)
|
|
router.use(authenticateToken);
|
|
|
|
/**
|
|
* GET /api/rss/feeds
|
|
* Liste tous les flux RSS
|
|
*/
|
|
router.get('/feeds', requireAdmin, async (req, res) => {
|
|
try {
|
|
const feeds = await getAll('SELECT * FROM rss_feeds ORDER BY created_at DESC');
|
|
res.json(feeds || []);
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la récupération des flux RSS:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/rss/feeds
|
|
* Ajouter un nouveau flux RSS
|
|
*/
|
|
router.post('/feeds', requireAdmin, async (req, res) => {
|
|
try {
|
|
const { url } = req.body;
|
|
|
|
if (!url) {
|
|
return res.status(400).json({ error: 'URL requise' });
|
|
}
|
|
|
|
// Vérifier si le flux existe déjà
|
|
const existing = await getOne('SELECT id FROM rss_feeds WHERE url = ?', [url]);
|
|
if (existing) {
|
|
return res.status(409).json({ error: 'Ce flux existe déjà' });
|
|
}
|
|
|
|
// Tester le flux
|
|
try {
|
|
const feed = await parser.parseURL(url);
|
|
|
|
// Ajouter le flux
|
|
const result = await runQuery(
|
|
'INSERT INTO rss_feeds (url, title, description) VALUES (?, ?, ?)',
|
|
[url, feed.title || url, feed.description || '']
|
|
);
|
|
|
|
logger.info(`Flux RSS ajouté: ${feed.title} (${url})`);
|
|
|
|
res.json({
|
|
id: result.id,
|
|
url,
|
|
title: feed.title || url,
|
|
description: feed.description || '',
|
|
enabled: 1
|
|
});
|
|
} catch (parseError) {
|
|
logger.error('Erreur lors du parsing du flux RSS:', parseError);
|
|
return res.status(400).json({ error: 'Impossible de parser ce flux RSS. Vérifiez l\'URL.' });
|
|
}
|
|
} catch (error) {
|
|
logger.error('Erreur lors de l\'ajout du flux RSS:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/rss/feeds/:id
|
|
* Mettre à jour un flux RSS (activer/désactiver)
|
|
*/
|
|
router.put('/feeds/:id', requireAdmin, async (req, res) => {
|
|
try {
|
|
const { enabled } = req.body;
|
|
|
|
await runQuery(
|
|
'UPDATE rss_feeds SET enabled = ? WHERE id = ?',
|
|
[enabled ? 1 : 0, req.params.id]
|
|
);
|
|
|
|
logger.info(`Flux RSS ${enabled ? 'activé' : 'désactivé'}: ${req.params.id}`);
|
|
res.json({ message: 'Flux mis à jour avec succès' });
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la mise à jour du flux RSS:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/rss/feeds/:id
|
|
* Supprimer un flux RSS
|
|
*/
|
|
router.delete('/feeds/:id', requireAdmin, async (req, res) => {
|
|
try {
|
|
await runQuery('DELETE FROM rss_feeds WHERE id = ?', [req.params.id]);
|
|
|
|
logger.info(`Flux RSS supprimé: ${req.params.id}`);
|
|
res.json({ message: 'Flux supprimé avec succès' });
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la suppression du flux RSS:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/rss/fetch
|
|
* Récupérer manuellement les articles de tous les flux RSS activés
|
|
*/
|
|
router.post('/fetch', requireAdmin, async (req, res) => {
|
|
try {
|
|
const rssScheduler = require('../services/rss-scheduler');
|
|
await rssScheduler.manualFetch();
|
|
|
|
// Invalider le cache
|
|
articlesCache = null;
|
|
articlesCacheTime = 0;
|
|
|
|
res.json({ message: 'Mise à jour des flux RSS terminée avec succès' });
|
|
} catch (error) {
|
|
logger.error('Erreur lors du fetch manuel des flux RSS:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/rss/refresh
|
|
* Alias pour fetch (pour compatibilité)
|
|
*/
|
|
router.post('/refresh', requireAdmin, async (req, res) => {
|
|
try {
|
|
const rssScheduler = require('../services/rss-scheduler');
|
|
await rssScheduler.manualFetch();
|
|
|
|
// Invalider le cache
|
|
articlesCache = null;
|
|
articlesCacheTime = 0;
|
|
|
|
res.json({ message: 'Mise à jour des flux RSS terminée avec succès' });
|
|
} catch (error) {
|
|
logger.error('Erreur lors du refresh des flux RSS:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
// Cache pour les articles RSS (30 secondes)
|
|
let articlesCache = null;
|
|
let articlesCacheTime = 0;
|
|
const ARTICLES_CACHE_DURATION = 30000; // 30 secondes
|
|
|
|
/**
|
|
* GET /api/rss/articles
|
|
* Récupérer les articles RSS (avec cache et paramètre limit)
|
|
*/
|
|
router.get('/articles', async (req, res) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit) || 5;
|
|
|
|
// Utiliser le cache si disponible et récent
|
|
const now = Date.now();
|
|
if (articlesCache && (now - articlesCacheTime) < ARTICLES_CACHE_DURATION) {
|
|
logger.debug('Articles RSS servis depuis le cache');
|
|
return res.json(articlesCache.slice(0, limit));
|
|
}
|
|
|
|
const articles = await getAll(`
|
|
SELECT
|
|
a.id, a.title, a.link, a.description, a.pub_date, a.content,
|
|
COALESCE(f.title, f.url) as feed_title, f.url as feed_url
|
|
FROM rss_articles a
|
|
LEFT JOIN rss_feeds f ON a.feed_id = f.id
|
|
WHERE a.pub_date IS NOT NULL
|
|
ORDER BY a.pub_date DESC
|
|
LIMIT ?
|
|
`, [limit]);
|
|
|
|
// Mettre en cache
|
|
articlesCache = articles || [];
|
|
articlesCacheTime = now;
|
|
|
|
logger.info(`Articles RSS récupérés: ${articles?.length || 0}`);
|
|
res.json(articles || []);
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la récupération des articles RSS:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/rss/summarize
|
|
* Générer un résumé PAR ARTICLE (100 mots max) avec lien
|
|
*/
|
|
router.post('/summarize', async (req, res) => {
|
|
try {
|
|
// Récupérer les paramètres
|
|
const apiKey = await getOne('SELECT value FROM settings WHERE key = ?', ['openrouter_api_key']);
|
|
const model = await getOne('SELECT value FROM settings WHERE key = ?', ['openrouter_model']);
|
|
|
|
if (!apiKey || !apiKey.value) {
|
|
return res.status(400).json({ error: 'Clé API OpenRouter non configurée' });
|
|
}
|
|
|
|
// Récupérer les 5 derniers articles
|
|
const articles = await getAll(`
|
|
SELECT
|
|
a.id, a.title, a.description, a.link, a.pub_date, a.content,
|
|
COALESCE(f.title, f.url) as feed_title
|
|
FROM rss_articles a
|
|
LEFT JOIN rss_feeds f ON a.feed_id = f.id
|
|
WHERE a.pub_date IS NOT NULL
|
|
ORDER BY a.pub_date DESC
|
|
LIMIT 5
|
|
`);
|
|
|
|
if (articles.length === 0) {
|
|
return res.status(400).json({ error: 'Aucun article à résumer' });
|
|
}
|
|
|
|
// Générer un résumé pour CHAQUE article (en parallèle)
|
|
const selectedModel = model?.value || 'openai/gpt-3.5-turbo';
|
|
logger.info(`Génération de ${articles.length} résumés avec ${selectedModel}...`);
|
|
|
|
const summaryPromises = articles.map(async (article) => {
|
|
const prompt = `Résume cet article en maximum 100 mots. Sois concis et informatif.
|
|
|
|
Titre: ${article.title}
|
|
Source: ${article.feed_title}
|
|
Description: ${article.description || ''}
|
|
${article.content ? `Contenu: ${article.content.substring(0, 1000)}` : ''}
|
|
|
|
Résumé (100 mots max):`;
|
|
|
|
try {
|
|
const response = await axios.post(
|
|
'https://openrouter.ai/api/v1/chat/completions',
|
|
{
|
|
model: selectedModel,
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: prompt
|
|
}
|
|
],
|
|
max_tokens: 200
|
|
},
|
|
{
|
|
headers: {
|
|
'Authorization': `Bearer ${apiKey.value}`,
|
|
'HTTP-Referer': 'https://noteflow.app',
|
|
'X-Title': 'NoteFlow RSS Summarizer',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
timeout: 30000
|
|
}
|
|
);
|
|
|
|
const summary = response.data.choices[0].message.content;
|
|
|
|
// Sauvegarder le résumé individuel avec feed_title
|
|
await runQuery(
|
|
'INSERT INTO rss_summaries (summary, model, articles_count, feed_title, created_at) VALUES (?, ?, ?, ?, datetime("now"))',
|
|
[`**${article.title}**\n\n${summary}\n\n🔗 [Lire l'article](${article.link})`, selectedModel, 1, article.feed_title]
|
|
);
|
|
|
|
return {
|
|
article_id: article.id,
|
|
title: article.title,
|
|
link: article.link,
|
|
feed_title: article.feed_title,
|
|
summary: summary,
|
|
pub_date: article.pub_date
|
|
};
|
|
} catch (err) {
|
|
logger.error(`Erreur résumé article "${article.title}":`, err.message);
|
|
return {
|
|
article_id: article.id,
|
|
title: article.title,
|
|
link: article.link,
|
|
feed_title: article.feed_title,
|
|
summary: article.description || 'Résumé non disponible',
|
|
pub_date: article.pub_date,
|
|
error: true
|
|
};
|
|
}
|
|
});
|
|
|
|
const summaries = await Promise.all(summaryPromises);
|
|
|
|
logger.info(`${summaries.length} résumés générés avec succès`);
|
|
|
|
res.json({
|
|
summaries,
|
|
model: selectedModel,
|
|
articles_count: articles.length
|
|
});
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la génération des résumés:', error);
|
|
|
|
if (error.response) {
|
|
logger.error('Réponse OpenRouter:', error.response.data);
|
|
return res.status(error.response.status).json({
|
|
error: 'Erreur API OpenRouter',
|
|
details: error.response.data
|
|
});
|
|
}
|
|
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/rss/summaries
|
|
* Récupérer les 5 derniers résumés
|
|
*/
|
|
router.get('/summaries', async (req, res) => {
|
|
try {
|
|
const summaries = await getAll(`
|
|
SELECT id, summary, model, articles_count, feed_title, created_at
|
|
FROM rss_summaries
|
|
ORDER BY created_at DESC
|
|
LIMIT 5
|
|
`);
|
|
|
|
res.json(summaries || []);
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la récupération des résumés:', error);
|
|
res.status(500).json({ error: 'Erreur serveur' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/rss/models
|
|
* Récupérer la liste des modèles OpenRouter depuis l'API
|
|
*/
|
|
router.get('/models', requireAdmin, async (req, res) => {
|
|
try {
|
|
// Récupérer les modèles depuis l'API OpenRouter
|
|
const response = await axios.get('https://openrouter.ai/api/v1/models', {
|
|
headers: {
|
|
'HTTP-Referer': 'https://noteflow.app',
|
|
'X-Title': 'NoteFlow'
|
|
},
|
|
timeout: 10000
|
|
});
|
|
|
|
// Transformer les données en format simplifié
|
|
const models = response.data.data.map(model => ({
|
|
id: model.id,
|
|
name: model.name || model.id,
|
|
provider: model.id.split('/')[0] || 'Unknown',
|
|
context_length: model.context_length,
|
|
pricing: model.pricing
|
|
}));
|
|
|
|
// Trier par provider puis par nom
|
|
models.sort((a, b) => {
|
|
if (a.provider !== b.provider) {
|
|
return a.provider.localeCompare(b.provider);
|
|
}
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
res.json(models);
|
|
} catch (error) {
|
|
logger.error('Erreur lors de la récupération des modèles:', error);
|
|
|
|
// Fallback sur une liste de modèles populaires si l'API échoue
|
|
const fallbackModels = [
|
|
{ id: 'openai/gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'openai' },
|
|
{ id: 'openai/gpt-3.5-turbo', name: 'GPT-3.5 Turbo', provider: 'openai' },
|
|
{ id: 'anthropic/claude-3-opus', name: 'Claude 3 Opus', provider: 'anthropic' },
|
|
{ id: 'anthropic/claude-3-sonnet', name: 'Claude 3 Sonnet', provider: 'anthropic' },
|
|
{ id: 'google/gemini-pro', name: 'Gemini Pro', provider: 'google' },
|
|
{ id: 'meta-llama/llama-3-70b-instruct', name: 'Llama 3 70B', provider: 'meta-llama' },
|
|
{ id: 'mistralai/mistral-large', name: 'Mistral Large', provider: 'mistralai' }
|
|
];
|
|
|
|
res.json(fallbackModels);
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|