| const fs = require('fs'); |
| const path = require('path'); |
|
|
| const { collectSitesRecursively, normalizeUrlKey } = require('../utils/sites'); |
| const { createLogger } = require('../utils/logger'); |
|
|
| const log = createLogger('cache:articles'); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function tryLoadArticlesFeedCache(pageId, config) { |
| if (!pageId) return null; |
|
|
| const cacheDirFromEnv = process.env.RSS_CACHE_DIR ? String(process.env.RSS_CACHE_DIR) : ''; |
| const cacheDirFromConfig = |
| config && config.site && config.site.rss && config.site.rss.cacheDir |
| ? String(config.site.rss.cacheDir) |
| : ''; |
| const cacheDir = cacheDirFromEnv || cacheDirFromConfig || 'dev'; |
|
|
| const cacheBaseDir = path.isAbsolute(cacheDir) ? cacheDir : path.join(process.cwd(), cacheDir); |
| const cachePath = path.join(cacheBaseDir, `${pageId}.feed-cache.json`); |
| if (!fs.existsSync(cachePath)) return null; |
|
|
| try { |
| const raw = fs.readFileSync(cachePath, 'utf8'); |
| const parsed = JSON.parse(raw); |
| if (!parsed || typeof parsed !== 'object') return null; |
|
|
| const articles = Array.isArray(parsed.articles) ? parsed.articles : []; |
| const items = articles |
| .map((a) => { |
| const title = a && a.title ? String(a.title) : ''; |
| const url = a && a.url ? String(a.url) : ''; |
| if (!title || !url) return null; |
|
|
| return { |
| |
| name: title, |
| url, |
| icon: a && a.icon ? String(a.icon) : 'fas fa-pen', |
| description: a && a.summary ? String(a.summary) : '', |
|
|
| |
| publishedAt: a && a.publishedAt ? String(a.publishedAt) : '', |
| source: a && a.source ? String(a.source) : '', |
| |
| sourceUrl: a && a.sourceUrl ? String(a.sourceUrl) : '', |
|
|
| |
| external: true, |
| }; |
| }) |
| .filter(Boolean); |
|
|
| return { |
| items, |
| meta: { |
| pageId: parsed.pageId || pageId, |
| generatedAt: parsed.generatedAt || '', |
| total: |
| parsed.stats && Number.isFinite(parsed.stats.totalArticles) |
| ? parsed.stats.totalArticles |
| : items.length, |
| }, |
| }; |
| } catch (e) { |
| log.warn('articles 缓存读取失败,将回退 Phase 1', { path: cachePath }); |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function buildArticlesCategoriesByPageCategories(categories, articlesItems) { |
| const safeItems = Array.isArray(articlesItems) ? articlesItems : []; |
| const safeCategories = Array.isArray(categories) ? categories : []; |
|
|
| |
| if (safeCategories.length === 0) { |
| return [ |
| { |
| name: '最新文章', |
| icon: 'fas fa-rss', |
| items: safeItems, |
| }, |
| ]; |
| } |
|
|
| const categoryIndex = safeCategories.map((category) => { |
| const sites = []; |
| collectSitesRecursively(category, sites); |
|
|
| const siteUrlKeys = new Set(); |
| const siteNameKeys = new Set(); |
| sites.forEach((site) => { |
| const urlKey = normalizeUrlKey(site && site.url ? String(site.url) : ''); |
| if (urlKey) siteUrlKeys.add(urlKey); |
| const nameKey = site && site.name ? String(site.name).trim().toLowerCase() : ''; |
| if (nameKey) siteNameKeys.add(nameKey); |
| }); |
|
|
| return { category, siteUrlKeys, siteNameKeys }; |
| }); |
|
|
| const buckets = categoryIndex.map(() => []); |
| const uncategorized = []; |
|
|
| safeItems.forEach((item) => { |
| const sourceUrlKey = normalizeUrlKey(item && item.sourceUrl ? String(item.sourceUrl) : ''); |
| const sourceNameKey = item && item.source ? String(item.source).trim().toLowerCase() : ''; |
|
|
| let matchedIndex = -1; |
| if (sourceUrlKey) { |
| matchedIndex = categoryIndex.findIndex((idx) => idx.siteUrlKeys.has(sourceUrlKey)); |
| } |
| if (matchedIndex < 0 && sourceNameKey) { |
| matchedIndex = categoryIndex.findIndex((idx) => idx.siteNameKeys.has(sourceNameKey)); |
| } |
|
|
| if (matchedIndex < 0) { |
| uncategorized.push(item); |
| return; |
| } |
|
|
| buckets[matchedIndex].push(item); |
| }); |
|
|
| const displayCategories = categoryIndex.map((idx, i) => ({ |
| name: idx.category && idx.category.name ? String(idx.category.name) : '未命名分类', |
| icon: idx.category && idx.category.icon ? String(idx.category.icon) : 'fas fa-rss', |
| items: buckets[i], |
| })); |
|
|
| if (uncategorized.length > 0) { |
| displayCategories.push({ |
| name: '其他', |
| icon: 'fas fa-ellipsis-h', |
| items: uncategorized, |
| }); |
| } |
|
|
| return displayCategories; |
| } |
|
|
| module.exports = { |
| tryLoadArticlesFeedCache, |
| buildArticlesCategoriesByPageCategories, |
| }; |
|
|