Files

169 lines
6.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 健康检查脚本:扫描所有定时任务最近一次执行情况
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);
});