修复若干问题

This commit is contained in:
zqlit committed 2026-05-20 15:33:10 +08:00
1 parent 0ee5005b87
commit 347d52d4b8
5 files changed
+192 -117

No files matched your search

+143 -86
View File
@@ -1,109 +1,166 @@
const fs = require('fs');
const yaml = require('js-yaml');
const axios = require('axios');
const path = require('path');
const https = require('https');
const http = require('http');
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 linksPath = path.join(__dirname, '../data/links.yaml');
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;
// Ensure static dir exists
if (!fs.existsSync(path.dirname(outputPath))) {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
}
// Config
const CONCURRENCY = 10;
const TIMEOUT = 10000;
// Optimized Axios Instance with Keep-Alive
const axiosInstance = axios.create({
timeout: TIMEOUT,
maxRedirects: 5,
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
const client = axios.create({
timeout: BATCH_TIMEOUT,
validateStatus: (s) => s >= 200 && s < 500,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
},
validateStatus: (status) => status >= 200 && status < 400
'User-Agent': 'blog-link-health/1.0 (+github-actions)'
}
});
async function checkUrl(url) {
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 {
await axiosInstance.head(url);
return true;
} catch (error) {
try {
await axiosInstance.get(url);
return true;
} catch (err2) {
return false;
}
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 checkLinks() {
console.log('Starting link check...');
const start = Date.now();
async function fetchBatchHealth() {
try {
const fileContents = fs.readFileSync(linksPath, 'utf8');
const data = yaml.load(fileContents);
if (!data || !data.blogs) {
console.log('No blogs found in links.yaml');
return;
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;
}
const blogs = data.blogs.filter(b => b.url);
const results = {};
const failedLinks = [];
let completed = 0;
const total = blogs.length;
// Concurrency Pool
async function worker() {
while (blogs.length > 0) {
const blog = blogs.shift();
const status = await checkUrl(blog.url);
results[blog.url] = status;
completed++;
const icon = status ? '🟢' : '🔴';
console.log(`[${completed}/${total}] ${icon} ${blog.name}`);
if (!status) {
failedLinks.push(`- ${blog.name}: ${blog.url}`);
}
}
}
// Start workers
const workers = Array(Math.min(blogs.length, CONCURRENCY))
.fill(null)
.map(() => worker());
await Promise.all(workers);
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
const duration = ((Date.now() - start) / 1000).toFixed(2);
console.log(`✨ Check finished in ${duration}s. Status saved to ${outputPath}`);
// Write failed links to summary file for email notification
const summaryPath = path.join(__dirname, '../failed_links_summary.txt');
if (failedLinks.length > 0) {
const summaryContent = `\n🚫 监测到 ${failedLinks.length} 个异常友链:\n${failedLinks.join('\n')}\n`;
fs.writeFileSync(summaryPath, summaryContent);
console.log(`Failed links summary saved to ${summaryPath}`);
} else {
fs.writeFileSync(summaryPath, '\n✅ 所有友链状态正常\n');
}
return data;
} catch (e) {
console.error(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}`);
}
checkLinks();
main().catch((e) => {
console.error('[link-check] fatal:', e);
process.exit(1);
});