修复若干问题
This commit is contained in:
1 parent
0ee5005b87
commit
347d52d4b8
5 files changed
+178
-103
No files matched your search
@@ -24,7 +24,8 @@
|
||||
"Bash(curl -s \"http://127.0.0.1:1316/js/bundle.min.ca22314448750e3cd87573167443c4f61447dd8c77fdb2a8ecf962e6e770a7b7.js\" -o \".tmp-bundle.js\")",
|
||||
"Bash([ ! -x \"$CHROME\" ])",
|
||||
"Bash(powershell -c \"\\(Get-Command chrome.exe\\).Source\")",
|
||||
"Bash(node scripts/generate_notification_report.js)"
|
||||
"Bash(node scripts/generate_notification_report.js)",
|
||||
"Bash(curl -s -m 30 \"https://rssapi.usj.cc/api/health\")"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"E:\\GitHub\\Another-Blog"
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(awk '{print $5}')"
|
||||
"Bash(awk '{print $5}')",
|
||||
"Bash(curl -s -m 30 \"https://rssapi.usj.cc/api/health?url=blog.keepke.com\" -o .tmp-health-one.json)",
|
||||
"Bash(curl -s -m 30 \"https://rssapi.usj.cc/api/health?url=https://blog.keepke.com\" -o .tmp-h2.json)",
|
||||
"Bash(curl -s -m 180 \"https://rssapi.usj.cc/api/health\" -o .tmp-health.json)",
|
||||
"Bash(curl -s -m 30 \"https://rssapi.usj.cc/api/health?url=https://blog.keepke.com&proxy=1\" -o .tmp-h3.json)",
|
||||
"Bash(curl -s -m 30 \"https://rssapi.usj.cc/api/proxy?url=https://blog.keepke.com\" -o .tmp-proxy.html)",
|
||||
"Bash(curl -s -m 30 -X POST -H \"Content-Type: application/json\" -d '{\"url\":\"https://blog.keepke.com\",\"method\":\"HEAD\"}' \"https://rssapi.usj.cc/api/proxy\" -o .tmp-proxy.json -w \"HTTP %{http_code}\\\\n\")",
|
||||
"Bash(curl -sv -m 240 \"https://rssapi.usj.cc/api/health\" -o .tmp-health.json)",
|
||||
"Bash(echo \"exit=$?\")",
|
||||
"Bash(curl -s -m 30 \"https://rssapi.usj.cc/api/links\" -o .tmp-links.json)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
|
||||
🚫 监测到 1 个异常友链:
|
||||
- Kris Yan: https://blog.krisyan.dev
|
||||
🚫 监测到 6 个异常友链:
|
||||
- 团团生活志: https://t-t.live (HTTP 404)
|
||||
- Kris Yan: https://blog.krisyan.dev (HTTP 429)
|
||||
- 子舒: https://www.zishu.me (net_exception_timeout)
|
||||
- 北枫: https://beifeng.me (net_exception_connect_timeout)
|
||||
- 永恒末匕: https://likaijian.com (HTTP 404)
|
||||
- Vian: https://vian.top (timeout of 20000ms exceeded)
|
||||
+129
-72
@@ -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) {
|
||||
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 {
|
||||
await axiosInstance.get(url);
|
||||
return true;
|
||||
} catch (err2) {
|
||||
return false;
|
||||
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 checkLinks() {
|
||||
console.log('Starting link check...');
|
||||
const start = Date.now();
|
||||
|
||||
async function fetchSingleHealth(url) {
|
||||
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, {
|
||||
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' };
|
||||
}
|
||||
}
|
||||
|
||||
const blogs = data.blogs.filter(b => b.url);
|
||||
const results = {};
|
||||
const failedLinks = [];
|
||||
let completed = 0;
|
||||
const total = blogs.length;
|
||||
async function fetchPerSiteHealth(links) {
|
||||
const queue = links.filter((l) => l && l.url).map((l) => l.url);
|
||||
const out = [];
|
||||
let cursor = 0;
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
while (cursor < queue.length) {
|
||||
const idx = cursor++;
|
||||
const url = queue[idx];
|
||||
const result = await fetchSingleHealth(url);
|
||||
out.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Start workers
|
||||
const workers = Array(Math.min(blogs.length, CONCURRENCY))
|
||||
const workers = Array(Math.min(queue.length, PER_SITE_CONCURRENCY))
|
||||
.fill(null)
|
||||
.map(() => worker());
|
||||
|
||||
await Promise.all(workers);
|
||||
return out;
|
||||
}
|
||||
|
||||
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}`);
|
||||
function writeOutputs(results, nameMap) {
|
||||
const map = {};
|
||||
const failed = [];
|
||||
|
||||
// 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}`);
|
||||
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');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
return failed.length;
|
||||
}
|
||||
|
||||
checkLinks();
|
||||
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);
|
||||
});
|
||||
+30
-27
@@ -1,42 +1,45 @@
|
||||
{
|
||||
"https://blog.keepke.com": true,
|
||||
"https://loseu.cc": true,
|
||||
"https://blog.qydzz.cn": true,
|
||||
"https://heitaosan.com": true,
|
||||
"https://vian.top": true,
|
||||
"https://blog.200011.net": true,
|
||||
"https://blog.qydzz.cn": true,
|
||||
"https://loseu.cc": true,
|
||||
"https://blog.allnull.cn": true,
|
||||
"https://blog.fivk.cn": true,
|
||||
"https://beifeng.me": true,
|
||||
"https://www.rz.sb": true,
|
||||
"https://mojinxi.cn": true,
|
||||
"https://www.sirit.com.cn": true,
|
||||
"https://sweetjing.cc/": true,
|
||||
"https://www.hedelei.com": true,
|
||||
"https://vimin.cc": true,
|
||||
"https://duanxiansen.com": true,
|
||||
"https://xyzbz.cn": true,
|
||||
"https://t-t.live": true,
|
||||
"https://baipin.pw": true,
|
||||
"https://felixway.cn": true,
|
||||
"https://www.liveout.cn/": true,
|
||||
"https://blog.fivk.cn": true,
|
||||
"https://t-t.live": false,
|
||||
"https://www.hedelei.com": true,
|
||||
"https://robes.xin": true,
|
||||
"https://mojinxi.cn": true,
|
||||
"https://sweetjing.cc/": true,
|
||||
"https://vimin.cc": true,
|
||||
"https://www.sirit.com.cn": true,
|
||||
"https://sumiler.com": true,
|
||||
"https://ihello.cc": true,
|
||||
"https://whooc.com": true,
|
||||
"https://www.liuchengrui.cn": true,
|
||||
"https://xyzbz.cn": true,
|
||||
"https://felixway.cn": true,
|
||||
"https://blog.krisyan.dev": false,
|
||||
"https://ihello.cc": true,
|
||||
"https://www.liuchengrui.cn": true,
|
||||
"https://whooc.com": true,
|
||||
"https://vjo.cc": true,
|
||||
"https://www.ershiliu.com/": true,
|
||||
"https://baipin.pw": true,
|
||||
"https://www.zoujiang.com": true,
|
||||
"https://dai.ge": true,
|
||||
"https://skylan.cc": true,
|
||||
"https://www.zishu.me": false,
|
||||
"https://www.dusays.com": true,
|
||||
"https://aloner.ink": true,
|
||||
"https://www.jdeal.cn/": true,
|
||||
"https://immmmm.com": true,
|
||||
"https://digu.plus": true,
|
||||
"https://www.dusays.com": true,
|
||||
"https://lms.im": true,
|
||||
"https://likaijian.com": true,
|
||||
"https://beifeng.me": false,
|
||||
"https://jb18.cm": true,
|
||||
"https://www.zishu.me": true
|
||||
"https://digu.plus": true,
|
||||
"https://www.suiyan.cc/": true,
|
||||
"https://lms.im": true,
|
||||
"https://dai.ge": true,
|
||||
"https://wuziya.com": true,
|
||||
"https://xiangming.site/": true,
|
||||
"https://immmmm.com": true,
|
||||
"https://www.rz.sb": true,
|
||||
"https://www.liveout.cn/": true,
|
||||
"https://likaijian.com": false,
|
||||
"https://vian.top": false
|
||||
}
|
||||
Reference in new issue
Block a user