// 健康检查脚本:扫描所有定时任务最近一次执行情况 import { PrismaClient } from "@prisma/client"; import { PrismaMariaDb } from "@prisma/adapter-mariadb"; import "dotenv/config"; const base = process.env.DATABASE_URL.replace("mysql://", "mariadb://"); const sep = base.includes("?") ? "&" : "?"; const connectionString = `${base}${sep}connection_limit=2&pool_timeout=10`; const adapter = new PrismaMariaDb(connectionString); const prisma = new PrismaClient({ adapter }); async function main() { console.log("========== 任务调度健康检查 ==========\n"); // 1. 任务配置 const configs = await prisma.taskConfig.findMany({ orderBy: { taskKey: "asc" } }); console.log("--- 任务配置 ---"); console.log(`共 ${configs.length} 条配置`); for (const c of configs) { console.log(` [${c.enabled ? "●" : "○"}] ${c.taskKey.padEnd(30)} cron=${c.cronExpr || "—"} ${c.taskName}`); } // 2. 最近 7 天的执行记录 const since = new Date(Date.now() - 7 * 24 * 3600 * 1000); const logs = await prisma.taskLog.findMany({ where: { startedAt: { gte: since } }, orderBy: { startedAt: "desc" }, }); // 兼容历史 status 语义:success == completed,error == failed const isOk = (s) => s === "completed" || s === "success"; const isBad = (s) => s === "failed" || s === "error"; console.log(`\n--- 最近 7 天日志(${logs.length} 条) ---`); const byKey = new Map(); for (const l of logs) { if (!byKey.has(l.taskKey)) byKey.set(l.taskKey, []); byKey.get(l.taskKey).push(l); } for (const [key, list] of byKey.entries()) { const success = list.filter((l) => isOk(l.status)).length; const failed = list.filter((l) => isBad(l.status)).length; const last = list[0]; const lastDate = new Date(last.startedAt).toLocaleString("zh-CN"); console.log(` ${key.padEnd(30)} 共${list.length}次 成功${success} 失败${failed} 最近=${lastDate} status=${last.status}`); } // 3. 任何"从未执行"的任务 console.log("\n--- 检查哪些 taskKey 已配置但从未执行 ---"); const executedKeys = new Set(logs.map((l) => l.taskKey)); const allExpectedKeys = new Set([ ...configs.map((c) => c.taskKey), // 工具/技能发现 "task1-discover-tools", "task3-check-tools", "task4-discover-skills", "task5-update-stars", "task6-review-hot", "task7-news-to-community", "daily-news", // Bot 核心引擎 "bot-activity", "bot-skill-crystallize", "bot-affinity-update", "bot-weekly-review", "bot-feedback-loop", "bot-adversarial-learning-run", "bot-persona-experiment-run", "bot-roundtable", // 运维 "cleanup-logs", "cleanup-task-logs", "reconcile-like-counts", "health-check", // 内容运营 "weekly-recommend-email", "grant-freeze-cards", "notification-digest", "newsletter-generate", // 竞品/趋势 "competitor-monitor", "trend-alert", // 数据维护 "enrich-tool-data", // 行业智库 "industry-thinktank", // 行业情报日更 "industry-daily", ]); const knownButNeverRun = [...allExpectedKeys].filter((k) => !executedKeys.has(k)); console.log("从未执行:", knownButNeverRun.length ? knownButNeverRun.join(", ") : "无"); // 4. bot 体系健康 console.log("\n--- Bot 体系健康 ---"); const totalBots = await prisma.user.count({ where: { isBot: true } }); const totalTopicsByBots = await prisma.forumTopic.count({ where: { user: { isBot: true } } }); const totalPostsByBots = await prisma.forumPost.count({ where: { user: { isBot: true } } }); const totalMemories = await prisma.botMemory.count(); const totalBotConfigs = await prisma.botConfig.count(); const lastBotTopic = await prisma.forumTopic.findFirst({ where: { user: { isBot: true } }, orderBy: { createdAt: "desc" }, select: { createdAt: true, title: true }, }); // 新形态(每日情报):Bot 主要产出是"楼层/帖子",主题帖只创建一次, // 因此活跃度判定必须用最新的 Bot 楼层(post)而不是主题帖创建时间 const lastBotPost = await prisma.forumPost.findFirst({ where: { user: { isBot: true } }, orderBy: { createdAt: "desc" }, select: { createdAt: true, content: true }, }); console.log(` Bot 用户总数: ${totalBots}`); console.log(` BotConfig 数: ${totalBotConfigs}`); console.log(` BotMemory 数: ${totalMemories}`); console.log(` Bot 发帖数: ${totalTopicsByBots}`); console.log(` Bot 回复数: ${totalPostsByBots}`); if (lastBotTopic) { const hoursAgo = (Date.now() - new Date(lastBotTopic.createdAt).getTime()) / 3600000; console.log(` 最近 Bot 帖: ${lastBotTopic.title.substring(0, 40)}... (${hoursAgo.toFixed(1)} 小时前)`); } else { console.log(" 最近 Bot 帖: 无"); } // 5. 最近失败的详情 const recentFailed = logs.filter((l) => isBad(l.status)).slice(0, 5); console.log(`\n--- 最近失败任务 (${recentFailed.length}) ---`); for (const f of recentFailed) { console.log(` ${f.taskKey} @ ${new Date(f.startedAt).toLocaleString("zh-CN")}`); if (f.error) console.log(` ${f.error.substring(0, 200)}`); } // 6. 告警汇总 const criticalFailures = logs.filter(l => isBad(l.status) && (Date.now() - new Date(l.startedAt).getTime()) < 24 * 3600 * 1000); console.log("\n--- ⚠️ 24小时告警汇总 ---"); if (criticalFailures.length === 0) { console.log(" ✅ 无告警,所有任务正常"); } else { console.log(` 🚨 ${criticalFailures.length} 个任务失败!`); for (const f of criticalFailures) { console.log(` - ${f.taskKey}: ${f.error?.substring(0, 100) || '未知错误'}`); } } // Bot 活跃度(新形态):以"行业情报日更引擎最近一次执行"为准(发布/跳过都算在跑), // 引擎每天 08:30(北京)一次,故阈值 26h;无执行记录时回退到最新 Bot 楼层时间 const lastDailyRun = await prisma.taskLog.findFirst({ where: { taskKey: "industry-daily" }, orderBy: { startedAt: "desc" }, select: { startedAt: true, status: true, result: true }, }); const lastActiveAt = lastDailyRun ? new Date(lastDailyRun.startedAt) : lastBotPost ? new Date(lastBotPost.createdAt) : null; if (lastActiveAt) { const hoursAgo = (Date.now() - lastActiveAt.getTime()) / 3600000; const which = lastDailyRun ? `日更引擎(${lastDailyRun.status})` : "Bot 楼层"; if (hoursAgo > 26) { console.log(` 🚨 Bot 体系活跃异常:最近 ${which} 在 ${hoursAgo.toFixed(1)}h 前(>26h),请检查 industry-daily 调度!`); } else { console.log(` ✅ Bot 活动正常(最近 ${which} ${hoursAgo.toFixed(1)}h 前)`); } } else { console.log(" ⚠️ 无 Bot 活跃记录(industry-daily 尚未执行过)"); } await prisma.$disconnect(); } main().catch((e) => { console.error(e); process.exit(1); });