1508
This commit is contained in:
1 parent
b8d721c7ff
commit
c506aadb46
3 files changed
+352
No files matched your search
@@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
# 诊断评论通知问题
|
||||
|
||||
echo ""
|
||||
echo "==================================="
|
||||
echo " 评论通知诊断"
|
||||
echo "==================================="
|
||||
echo ""
|
||||
|
||||
# 读取配置
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "错误: .env 文件不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARTALK_SERVER=$(grep "^ARTALK_SERVER=" .env | cut -d'=' -f2)
|
||||
ARTALK_SITE=$(grep "^ARTALK_SITE=" .env | cut -d'=' -f2)
|
||||
ARTALK_ADMIN_EMAIL=$(grep "^ARTALK_ADMIN_EMAIL=" .env | cut -d'=' -f2)
|
||||
ARTALK_ADMIN_PASSWORD=$(grep "^ARTALK_ADMIN_PASSWORD=" .env | cut -d'=' -f2)
|
||||
|
||||
echo "1. 配置信息"
|
||||
echo " 服务器: $ARTALK_SERVER"
|
||||
echo " 站点: $ARTALK_SITE"
|
||||
echo ""
|
||||
|
||||
# 登录
|
||||
echo "2. 登录 Artalk..."
|
||||
TOKEN=$(curl -s -X POST "$ARTALK_SERVER/api/v2/auth/email/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$ARTALK_ADMIN_EMAIL\",\"password\":\"$ARTALK_ADMIN_PASSWORD\"}" | \
|
||||
grep -o '"token":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo " ✗ 登录失败"
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ 登录成功"
|
||||
echo ""
|
||||
|
||||
# 获取最新评论
|
||||
echo "3. 获取最新 10 条评论..."
|
||||
RESPONSE=$(curl -s "$ARTALK_SERVER/api/v2/comments?site_name=$ARTALK_SITE&page_key=/&limit=10&sort_by=date&sort_order=desc" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
COUNT=$(echo "$RESPONSE" | grep -o '"count":[0-9]*' | cut -d':' -f2)
|
||||
echo " 首页评论数: $COUNT"
|
||||
|
||||
if [ "$COUNT" -gt 0 ]; then
|
||||
echo " 最新评论:"
|
||||
echo "$RESPONSE" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
for c in data.get('comments', [])[:3]:
|
||||
print(f' ID {c[\"id\"]}: {c[\"nick\"]} - {c[\"content\"][:40]}...')
|
||||
" 2>/dev/null || echo " (需要 python3 来解析 JSON)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 获取最新评论(所有页面)
|
||||
echo "4. 获取所有页面最新评论(前 5 条)..."
|
||||
ALL_COMMENTS="[]"
|
||||
|
||||
# 获取前 5 个页面的评论
|
||||
for page_key in "/" "/20260623235948.html" "/202606231801.html" "/20211101.html" "/20210903.html"; do
|
||||
RESPONSE=$(curl -s "$ARTALK_SERVER/api/v2/comments?site_name=$ARTALK_SITE&page_key=$page_key&limit=2&sort_by=date&sort_order=desc" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
COUNT=$(echo "$RESPONSE" | grep -o '"count":[0-9]*' | cut -d':' -f2)
|
||||
if [ "$COUNT" -gt 0 ]; then
|
||||
echo " ✓ $page_key: $COUNT 条评论"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# 检查通知日志
|
||||
echo "5. 最近的通知日志..."
|
||||
docker compose logs --tail 20 2>&1 | grep -E "\[comment-notify\]|fetched.*comments" | tail -5
|
||||
echo ""
|
||||
|
||||
echo "==================================="
|
||||
echo " 诊断完成"
|
||||
echo "==================================="
|
||||
echo ""
|
||||
echo "如果评论存在但没有通知,可能原因:"
|
||||
echo "1. 评论的文章不在最新 20 个页面中"
|
||||
echo "2. 启动时已记录最新 ID,新评论 ID 必须更大"
|
||||
echo "3. Artalk API 返回的评论顺序问题"
|
||||
echo ""
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in new issue
Block a user