This commit is contained in:
Vaica committed 2026-06-24 15:28:02 +08:00
1 parent eabdb1a507
commit 0d698d40d8
1 file changed
+61 -28
+61 -28
View File
@@ -1,7 +1,7 @@
import { sendMessage } from "./tg"; import { sendMessage } from "./tg";
import { ALLOWED_IDS } from "./config"; import { ALLOWED_IDS } from "./config";
import { getFeedsHealth } from "@/lib/rssapi"; import { getLinks, checkLinkViaProxy } from "@/lib/rssapi";
import { ARTALK_SERVER, ARTALK_SITE, ARTALK_ADMIN_EMAIL, ARTALK_ADMIN_PASSWORD } from "@/lib/config"; import { ARTALK_SERVER, ARTALK_SITE, ARTALK_ADMIN_EMAIL, ARTALK_ADMIN_PASSWORD, CONTENT_DIR } from "@/lib/config";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
@@ -51,28 +51,51 @@ async function notifyAll(message: string): Promise<void> {
} }
// ============================================================ // ============================================================
// 功能 1: 友链状态监控 // 功能 1: 友链状态监控(使用 SCF 代理)
// ============================================================ // ============================================================
export async function checkLinksStatus(): Promise<void> { export async function checkLinksStatus(): Promise<void> {
console.log("[blog-monitor] 🔗 checking links status..."); console.log("[blog-monitor] 🔗 checking links status...");
try { try {
// 直接调用 API 的健康检查端点 // 获取可见友链(不传 all 参数,API 自动过滤隐藏友链)
const healthResult = await getFeedsHealth(); const links = await getLinks();
if (!healthResult || !healthResult.results) { if (links.length === 0) {
console.log("[blog-monitor] ❌ failed to fetch health status"); console.log("[blog-monitor] ℹ️ no visible links found");
return; return;
} }
const { total, alive, dead, results } = healthResult; console.log(`[blog-monitor] 📊 checking ${links.length} visible links...`);
// 使用 SCF 代理并行检查所有链接(限制并发数为 10)
const batchSize = 10;
const results: Array<{ name: string; url: string; ok: boolean; status?: number; latency?: number; error?: string }> = [];
for (let i = 0; i < links.length; i += batchSize) {
const batch = links.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(async (link) => {
const health = await checkLinkViaProxy(link.url);
return {
name: link.name,
url: link.url,
ok: health.ok,
status: health.status,
latency: health.latency,
error: health.error,
};
})
);
results.push(...batchResults);
}
// 分类链接状态 // 分类链接状态
const invalidLinks = results.filter((r) => !r.ok); const invalidLinks = results.filter((r) => !r.ok);
const slowLinks = results.filter((r) => r.ok && r.latency && r.latency > 3000); const slowLinks = results.filter((r) => r.ok && r.latency && r.latency > 3000);
const validLinks = results.filter((r) => r.ok);
console.log(`[blog-monitor] ✅ links check completed: ${alive} valid, ${dead} invalid, ${slowLinks.length} slow`); console.log(`[blog-monitor] ✅ links check completed: ${validLinks.length} valid, ${invalidLinks.length} invalid, ${slowLinks.length} slow`);
// 只在有失效或缓慢链接时发送通知 // 只在有失效或缓慢链接时发送通知
if (invalidLinks.length > 0 || slowLinks.length > 0) { if (invalidLinks.length > 0 || slowLinks.length > 0) {
@@ -80,39 +103,39 @@ export async function checkLinksStatus(): Promise<void> {
`🔗 <b>友链状态监控报告</b>`, `🔗 <b>友链状态监控报告</b>`,
``, ``,
`📊 <b>总体情况:</b>`, `📊 <b>总体情况:</b>`,
`├─ 📁 总数:${total} 个`, `├─ 📁 总数:${links.length} 个`,
`├─ ✅ 有效:${alive} 个`, `├─ ✅ 有效:${validLinks.length} 个`,
`├─ ❌ 失效:${dead} 个`, `├─ ❌ 失效:${invalidLinks.length} 个`,
`└─ ⚠️ 缓慢:${slowLinks.length} 个(> 3 秒)`, `└─ ⚠️ 缓慢:${slowLinks.length} 个(> 3 秒)`,
``, ``,
]; ];
if (invalidLinks.length > 0) { if (invalidLinks.length > 0) {
msg.push(`🚨 <b>失效链接:</b>`); msg.push(`🚨 <b>失效链接:</b>`);
invalidLinks.slice(0, 5).forEach((link, index) => { invalidLinks.forEach((link, index) => {
const error = link.error || `HTTP ${link.status}`; const error = link.error || `HTTP ${link.status}`;
const prefix = index === invalidLinks.length - 1 || index === 4 ? "└─" : "├─"; const prefix = index === invalidLinks.length - 1 ? "└─" : "├─";
msg.push(`${prefix} 🔴 ${link.url}`); msg.push(`${prefix} 🔴 ${link.name}`);
msg.push(` └─ ${link.url}`);
msg.push(` └─ ${error}`); msg.push(` └─ ${error}`);
}); });
if (invalidLinks.length > 5) {
msg.push(` ⋯ 还有 ${invalidLinks.length - 5} 个`);
}
msg.push(``); msg.push(``);
} }
if (slowLinks.length > 0) { if (slowLinks.length > 0) {
msg.push(`🐢 <b>响应缓慢:</b>`); msg.push(`🐢 <b>响应缓慢:</b>`);
slowLinks.slice(0, 3).forEach((link, index) => { slowLinks.forEach((link, index) => {
const latency = ((link.latency || 0) / 1000).toFixed(1); const latency = ((link.latency || 0) / 1000).toFixed(1);
const prefix = index === slowLinks.length - 1 || index === 2 ? "└─" : "├─"; const prefix = index === slowLinks.length - 1 ? "└─" : "├─";
msg.push(`${prefix} 🟡 ${link.url}`); msg.push(`${prefix} 🟡 ${link.name}`);
msg.push(` └─ ${link.url}`);
msg.push(` └─ ${latency}秒`); msg.push(` └─ ${latency}秒`);
}); });
msg.push(``); msg.push(``);
} }
msg.push(`💡 <b>建议:</b>尽快检查并修复失效链接,保持友链健康`); msg.push(`💡 <b>建议:</b>尽快检查并修复失效链接,保持友链健康`);
msg.push(`📡 <b>检测方式:</b>SCF 代理`);
await notifyAll(msg.join("\n")); await notifyAll(msg.join("\n"));
console.log("[blog-monitor] 📤 links notification sent"); console.log("[blog-monitor] 📤 links notification sent");
@@ -132,14 +155,16 @@ export async function checkBlogMilestones(): Promise<void> {
console.log("[blog-monitor] 🎯 checking blog milestones..."); console.log("[blog-monitor] 🎯 checking blog milestones...");
try { try {
// 获取文章数量(通过读取 content/posts 目录) // 使用配置中的 CONTENT_DIR
const postsDir = path.join(process.cwd(), "..", "content", "posts"); console.log(`[blog-monitor] 📂 posts directory: ${CONTENT_DIR}`);
let postsCount = 0; let postsCount = 0;
if (fs.existsSync(postsDir)) { if (fs.existsSync(CONTENT_DIR)) {
const years = fs.readdirSync(postsDir).filter((f) => /^\d{4}$/.test(f)); const years = fs.readdirSync(CONTENT_DIR).filter((f) => /^\d{4}$/.test(f));
console.log(`[blog-monitor] 📅 found years: ${years.join(", ")}`);
for (const year of years) { for (const year of years) {
const yearDir = path.join(postsDir, year); const yearDir = path.join(CONTENT_DIR, year);
if (fs.statSync(yearDir).isDirectory()) { if (fs.statSync(yearDir).isDirectory()) {
const posts = fs.readdirSync(yearDir).filter((f) => { const posts = fs.readdirSync(yearDir).filter((f) => {
const fullPath = path.join(yearDir, f); const fullPath = path.join(yearDir, f);
@@ -148,11 +173,14 @@ export async function checkBlogMilestones(): Promise<void> {
postsCount += posts.length; postsCount += posts.length;
} }
} }
} else {
console.error(`[blog-monitor] ❌ posts directory not found: ${CONTENT_DIR}`);
} }
// 获取评论数量 // 获取评论数量
let commentsCount = 0; let commentsCount = 0;
const token = await getArtalkToken(); const token = await getArtalkToken();
console.log(`[blog-monitor] 🔑 artalk token: ${token ? "obtained" : "failed"}`);
if (token) { if (token) {
try { try {
@@ -164,10 +192,15 @@ export async function checkBlogMilestones(): Promise<void> {
if (res.ok) { if (res.ok) {
const data = await res.json() as { count?: number }; const data = await res.json() as { count?: number };
commentsCount = data.count || 0; commentsCount = data.count || 0;
console.log(`[blog-monitor] 💬 comments API response: ${JSON.stringify(data)}`);
} else {
console.error(`[blog-monitor] ❌ comments API failed: ${res.status}`);
} }
} catch { } catch (e) {
console.error("[blog-monitor] ❌ failed to fetch comments count"); console.error("[blog-monitor] ❌ failed to fetch comments:", e instanceof Error ? e.message : String(e));
} }
} else {
console.error("[blog-monitor] ❌ no artalk token, cannot fetch comments");
} }
console.log(`[blog-monitor] 📊 stats: ${postsCount} posts, ${commentsCount} comments`); console.log(`[blog-monitor] 📊 stats: ${postsCount} posts, ${commentsCount} comments`);