This commit is contained in:
Vaica committed 2026-06-24 15:08:41 +08:00
1 parent a664180b47
commit eabdb1a507
3 files changed
+352

No files matched your search

+263
View File
@@ -0,0 +1,263 @@
import { sendMessage } from "./tg";
import { ALLOWED_IDS } from "./config";
import { getFeedsHealth } from "@/lib/rssapi";
import { ARTALK_SERVER, ARTALK_SITE, ARTALK_ADMIN_EMAIL, ARTALK_ADMIN_PASSWORD } from "@/lib/config";
import fs from "fs";
import path from "path";
// 博客创建时间(2024-07-11)
const BLOG_CREATED_DATE = new Date("2024-07-11");
// 里程碑配置
const MILESTONES = {
posts: [100, 150, 200, 250, 300, 500],
comments: [100, 500, 1000, 2000, 5000],
};
// 记录已通知的里程碑
const notifiedMilestones = new Set<string>();
// 获取 Artalk 管理员 token
async function getArtalkToken(): Promise<string | null> {
if (!ARTALK_ADMIN_EMAIL || !ARTALK_ADMIN_PASSWORD) {
return null;
}
try {
const res = await fetch(`${ARTALK_SERVER}/api/v2/auth/email/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: ARTALK_ADMIN_EMAIL, password: ARTALK_ADMIN_PASSWORD }),
});
if (!res.ok) return null;
const data = await res.json() as { token?: string };
return data.token || null;
} catch {
return null;
}
}
// 发送通知给所有允许的用户
async function notifyAll(message: string): Promise<void> {
const targets = ALLOWED_IDS.length > 0 ? ALLOWED_IDS : [];
for (const chatId of targets) {
try {
await sendMessage(chatId, message, { parse_mode: "HTML" });
} catch (e) {
console.error("[blog-monitor] send notification failed:", e instanceof Error ? e.message : String(e));
}
}
}
// ============================================================
// 功能 1: 友链状态监控
// ============================================================
export async function checkLinksStatus(): Promise<void> {
console.log("[blog-monitor] 🔗 checking links status...");
try {
// 直接调用 API 的健康检查端点
const healthResult = await getFeedsHealth();
if (!healthResult || !healthResult.results) {
console.log("[blog-monitor] ❌ failed to fetch health status");
return;
}
const { total, alive, dead, results } = healthResult;
// 分类链接状态
const invalidLinks = results.filter((r) => !r.ok);
const slowLinks = results.filter((r) => r.ok && r.latency && r.latency > 3000);
console.log(`[blog-monitor] ✅ links check completed: ${alive} valid, ${dead} invalid, ${slowLinks.length} slow`);
// 只在有失效或缓慢链接时发送通知
if (invalidLinks.length > 0 || slowLinks.length > 0) {
const msg = [
`🔗 <b>友链状态监控报告</b>`,
``,
`📊 <b>总体情况:</b>`,
`├─ 📁 总数:${total} 个`,
`├─ ✅ 有效:${alive} 个`,
`├─ ❌ 失效:${dead} 个`,
`└─ ⚠️ 缓慢:${slowLinks.length} 个(> 3 秒)`,
``,
];
if (invalidLinks.length > 0) {
msg.push(`🚨 <b>失效链接:</b>`);
invalidLinks.slice(0, 5).forEach((link, index) => {
const error = link.error || `HTTP ${link.status}`;
const prefix = index === invalidLinks.length - 1 || index === 4 ? "└─" : "├─";
msg.push(`${prefix} 🔴 ${link.url}`);
msg.push(` └─ ${error}`);
});
if (invalidLinks.length > 5) {
msg.push(` ⋯ 还有 ${invalidLinks.length - 5} 个`);
}
msg.push(``);
}
if (slowLinks.length > 0) {
msg.push(`🐢 <b>响应缓慢:</b>`);
slowLinks.slice(0, 3).forEach((link, index) => {
const latency = ((link.latency || 0) / 1000).toFixed(1);
const prefix = index === slowLinks.length - 1 || index === 2 ? "└─" : "├─";
msg.push(`${prefix} 🟡 ${link.url}`);
msg.push(` └─ ${latency}秒`);
});
msg.push(``);
}
msg.push(`💡 <b>建议:</b>尽快检查并修复失效链接,保持友链健康`);
await notifyAll(msg.join("\n"));
console.log("[blog-monitor] 📤 links notification sent");
} else {
console.log("[blog-monitor] 🎉 all links healthy");
}
} catch (e) {
console.error("[blog-monitor] ❌ check links failed:", e instanceof Error ? e.message : String(e));
}
}
// ============================================================
// 功能 2: 博客里程碑通知
// ============================================================
export async function checkBlogMilestones(): Promise<void> {
console.log("[blog-monitor] 🎯 checking blog milestones...");
try {
// 获取文章数量(通过读取 content/posts 目录)
const postsDir = path.join(process.cwd(), "..", "content", "posts");
let postsCount = 0;
if (fs.existsSync(postsDir)) {
const years = fs.readdirSync(postsDir).filter((f) => /^\d{4}$/.test(f));
for (const year of years) {
const yearDir = path.join(postsDir, year);
if (fs.statSync(yearDir).isDirectory()) {
const posts = fs.readdirSync(yearDir).filter((f) => {
const fullPath = path.join(yearDir, f);
return fs.statSync(fullPath).isDirectory();
});
postsCount += posts.length;
}
}
}
// 获取评论数量
let commentsCount = 0;
const token = await getArtalkToken();
if (token) {
try {
const res = await fetch(
`${ARTALK_SERVER}/api/v2/comments?site_name=${encodeURIComponent(ARTALK_SITE)}&limit=1`,
{ headers: { Authorization: `Bearer ${token}` } }
);
if (res.ok) {
const data = await res.json() as { count?: number };
commentsCount = data.count || 0;
}
} catch {
console.error("[blog-monitor] ❌ failed to fetch comments count");
}
}
console.log(`[blog-monitor] 📊 stats: ${postsCount} posts, ${commentsCount} comments`);
// 检查文章里程碑
for (const milestone of MILESTONES.posts) {
const key = `posts-${milestone}`;
if (postsCount >= milestone && !notifiedMilestones.has(key)) {
notifiedMilestones.add(key);
const msg = [
`🎉🎊🥳 <b>博客里程碑达成!</b> 🥳🎊🎉`,
``,
`📝 <b>文章里程碑:</b>`,
`├─ 📰 当前文章:${postsCount} 篇`,
`├─ 🎯 达成目标:${milestone} 篇`,
`└─ ⭐ 成就解锁!`,
``,
`🏆 <b>恭喜!这是一个了不起的成就!</b>`,
``,
`💡 <b>建议:</b>回顾一下最受欢迎的文章,继续保持创作热情!`,
``,
`📈 <b>继续加油,下一个目标等着你!</b>`,
];
await notifyAll(msg.join("\n"));
console.log(`[blog-monitor] 🏆 milestone notification sent: ${milestone} posts`);
}
}
// 检查评论里程碑
for (const milestone of MILESTONES.comments) {
const key = `comments-${milestone}`;
if (commentsCount >= milestone && !notifiedMilestones.has(key)) {
notifiedMilestones.add(key);
const msg = [
`💬🎉🎊 <b>评论里程碑达成!</b> 🎊🎉💬`,
``,
`💬 <b>评论里程碑:</b>`,
`├─ 💭 当前评论:${commentsCount} 条`,
`├─ 🎯 达成目标:${milestone} 条`,
`└─ ⭐ 成就解锁!`,
``,
`🏆 <b>恭喜!读者互动非常活跃!</b>`,
``,
`💡 <b>建议:</b>感谢一下活跃的评论者,继续维护社区氛围!`,
``,
`🤝 <b>感谢每一位参与互动的读者!</b>`,
];
await notifyAll(msg.join("\n"));
console.log(`[blog-monitor] 🏆 milestone notification sent: ${milestone} comments`);
}
}
} catch (e) {
console.error("[blog-monitor] ❌ check milestones failed:", e instanceof Error ? e.message : String(e));
}
}
// ============================================================
// 启动所有监控任务
// ============================================================
let monitorStarted = false;
export function startBlogMonitor(): void {
if (monitorStarted) return;
monitorStarted = true;
console.log("[blog-monitor] 🚀 starting blog monitor...");
// 启动时立即检查一次(延迟 30 秒,等待系统初始化)
setTimeout(async () => {
console.log("[blog-monitor] ⏰ running initial checks...");
// 并行执行所有检查
await Promise.allSettled([
checkLinksStatus(),
checkBlogMilestones(),
]);
console.log("[blog-monitor] ✅ initial checks completed");
}, 30_000);
// 定时任务:
// 友链检查:每 6 小时
setInterval(checkLinksStatus, 6 * 60 * 60 * 1000);
// 里程碑检查:每 12 小时
setInterval(checkBlogMilestones, 12 * 60 * 60 * 1000);
console.log("[blog-monitor] 📅 monitor started with schedules:");
console.log(" 🔗 Links: every 6 hours");
console.log(" 🎯 Milestones: every 12 hours");
}
+2
View File
@@ -1,5 +1,6 @@
import { startPolling } from "./poll";
import { startCommentNotifier } from "./notify";
import { startBlogMonitor } from "./blog-monitor";
import { TG_TOKEN } from "./config";
const g = globalThis as Record<string, unknown>;
@@ -16,6 +17,7 @@ export async function startBot(): Promise<void> {
console.log("[bot] initializing...");
await startPolling();
startCommentNotifier();
startBlogMonitor();
}
export { TG_TOKEN } from "./config";