const fs = require('fs'); const path = require('path'); const axios = require('axios'); const API_BASE = 'https://rssapi.usj.cc'; const HEALTH_URL = `${API_BASE}/api/health`; const LINKS_URL = `${API_BASE}/api/links`; const outputPath = path.join(__dirname, '../static/link_status.json'); const summaryPath = path.join(__dirname, '../failed_links_summary.txt'); const BATCH_TIMEOUT = 120000; // /api/health probes all sites server-side const SINGLE_TIMEOUT = 20000; const PER_SITE_CONCURRENCY = 8; if (!fs.existsSync(path.dirname(outputPath))) { fs.mkdirSync(path.dirname(outputPath), { recursive: true }); } const client = axios.create({ timeout: BATCH_TIMEOUT, validateStatus: (s) => s >= 200 && s < 500, headers: { 'User-Agent': 'blog-link-health/1.0 (+github-actions)' } }); function ensureArray(value) { return Array.isArray(value) ? value : []; } function pickName(map, url) { if (!url) return ''; return map.get(url) || map.get(url.replace(/\/+$/, '')) || ''; } function buildNameMap(links) { const map = new Map(); links.forEach((l) => { if (!l || !l.url) return; map.set(l.url, l.name || l.url); map.set(l.url.replace(/\/+$/, ''), l.name || l.url); }); return map; } async function fetchLinks() { try { const resp = await client.get(LINKS_URL, { timeout: SINGLE_TIMEOUT }); return ensureArray(resp.data && resp.data.links); } catch (e) { console.warn('[link-check] /api/links failed:', e.message); return []; } } async function fetchBatchHealth() { try { const resp = await client.get(HEALTH_URL, { timeout: BATCH_TIMEOUT }); const data = resp.data; if (!data || !Array.isArray(data.results)) { console.warn('[link-check] /api/health returned unexpected payload, falling back to per-site probes'); return null; } return data; } catch (e) { console.warn('[link-check] /api/health batch failed:', e.message, '- falling back to per-site probes'); return null; } } async function fetchSingleHealth(url) { try { const resp = await client.get(HEALTH_URL, { params: { url }, timeout: SINGLE_TIMEOUT }); const data = resp.data || {}; return { url, status: typeof data.status === 'number' ? data.status : 0, ok: !!data.ok, latency: typeof data.latency === 'number' ? data.latency : null, error: typeof data.error === 'string' ? data.error : '' }; } catch (e) { return { url, status: 0, ok: false, latency: null, error: e.message || 'request_failed' }; } } async function fetchPerSiteHealth(links) { const queue = links.filter((l) => l && l.url).map((l) => l.url); const out = []; let cursor = 0; async function worker() { while (cursor < queue.length) { const idx = cursor++; const url = queue[idx]; const result = await fetchSingleHealth(url); out.push(result); } } const workers = Array(Math.min(queue.length, PER_SITE_CONCURRENCY)) .fill(null) .map(() => worker()); await Promise.all(workers); return out; } function writeOutputs(results, nameMap) { const map = {}; const failed = []; results.forEach((r) => { if (!r || !r.url) return; map[r.url] = !!r.ok; if (!r.ok) { const name = pickName(nameMap, r.url) || r.url; const reason = r.error ? r.error : (r.status ? `HTTP ${r.status}` : 'unknown'); failed.push(`- ${name}: ${r.url} (${reason})`); } }); fs.writeFileSync(outputPath, JSON.stringify(map, null, 2)); if (failed.length > 0) { const summary = `\n🚫 监测到 ${failed.length} 个异常友链:\n${failed.join('\n')}\n`; fs.writeFileSync(summaryPath, summary); } else { fs.writeFileSync(summaryPath, '\n✅ 所有友链状态正常\n'); } return failed.length; } async function main() { const start = Date.now(); console.log('[link-check] starting via', HEALTH_URL); const linksList = await fetchLinks(); const nameMap = buildNameMap(linksList); let results = null; const batch = await fetchBatchHealth(); if (batch) { results = batch.results; console.log(`[link-check] batch report: total=${batch.total} alive=${batch.alive} dead=${batch.dead}`); } else if (linksList.length > 0) { console.log(`[link-check] per-site probe of ${linksList.length} sites...`); results = await fetchPerSiteHealth(linksList); } else { console.error('[link-check] no links to probe and batch endpoint failed'); process.exit(1); } const failedCount = writeOutputs(results, nameMap); const duration = ((Date.now() - start) / 1000).toFixed(2); console.log(`[link-check] done in ${duration}s, failed=${failedCount}, status=${outputPath}, summary=${summaryPath}`); } main().catch((e) => { console.error('[link-check] fatal:', e); process.exit(1); });