From 993ac5581d25b46ed4ff914bbfc927663bd8d089 Mon Sep 17 00:00:00 2001 From: zqlit Date: Wed, 20 May 2026 15:33:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=8B=A5=E5=B9=B2=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.json | 3 +- .claude/settings.local.json | 11 +- failed_links_summary.txt | 9 +- scripts/check_links.js | 229 ++++++++++++++++++++++-------------- static/link_status.json | 57 ++++----- 5 files changed, 192 insertions(+), 117 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 84e38953..51dea878 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -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" diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e2a95bab..72cb09ee 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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)" ] } } diff --git a/failed_links_summary.txt b/failed_links_summary.txt index 8151db5e..4596bac1 100644 --- a/failed_links_summary.txt +++ b/failed_links_summary.txt @@ -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) diff --git a/scripts/check_links.js b/scripts/check_links.js index 91d5f0b1..bd2da204 100644 --- a/scripts/check_links.js +++ b/scripts/check_links.js @@ -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); +}); diff --git a/static/link_status.json b/static/link_status.json index 6b5f1403..47453e9c 100644 --- a/static/link_status.json +++ b/static/link_status.json @@ -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 } \ No newline at end of file