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 { startPolling } from "./poll";
|
||||||
import { startCommentNotifier } from "./notify";
|
import { startCommentNotifier } from "./notify";
|
||||||
|
import { startBlogMonitor } from "./blog-monitor";
|
||||||
import { TG_TOKEN } from "./config";
|
import { TG_TOKEN } from "./config";
|
||||||
|
|
||||||
const g = globalThis as Record<string, unknown>;
|
const g = globalThis as Record<string, unknown>;
|
||||||
@@ -16,6 +17,7 @@ export async function startBot(): Promise<void> {
|
|||||||
console.log("[bot] initializing...");
|
console.log("[bot] initializing...");
|
||||||
await startPolling();
|
await startPolling();
|
||||||
startCommentNotifier();
|
startCommentNotifier();
|
||||||
|
startBlogMonitor();
|
||||||
}
|
}
|
||||||
|
|
||||||
export { TG_TOKEN } from "./config";
|
export { TG_TOKEN } from "./config";
|
||||||
|
|||||||
Reference in new issue
Block a user