import { PrismaClient } from "@prisma/client"; import { PrismaMariaDb } from "@prisma/adapter-mariadb"; import "dotenv/config"; import { withRetry } from "./lib/retry.mjs"; const rawUrl = process.env.DATABASE_URL; if (!rawUrl) { console.error("DATABASE_URL not set"); process.exit(1); } const base = rawUrl.replace("mysql://", "mariadb://"); const sep = base.includes("?") ? "&" : "?"; const connectionString = `${base}${sep}connection_limit=20&pool_timeout=30`; const adapter = new PrismaMariaDb(connectionString); const prisma = new PrismaClient({ adapter }); async function checkWebsite(url) { try { const res = await withRetry(async () => { return await fetch(url, { method: "GET", signal: AbortSignal.timeout(8000), redirect: "follow", headers: { "User-Agent": "Mozilla/5.0 (compatible; CheckBot/1.0)" }, }); }, { maxRetries: 2, label: `Website ${url}` }); return res.status >= 200 && res.status < 400; } catch { return false; } } async function checkLogo(logoUrl) { if (!logoUrl) return false; if (logoUrl.startsWith("/logos/")) return true; if (logoUrl.startsWith("data:")) return true; try { const res = await withRetry(async () => { return await fetch(logoUrl, { method: "HEAD", signal: AbortSignal.timeout(5000) }); }, { maxRetries: 2, label: `Logo ${logoUrl}` }); return res.ok; } catch { return false; } } async function main() { const startTime = new Date(); const now = new Date(); const cnOffset = 8 * 60 * 60 * 1000; const cnTime = new Date(now.getTime() + cnOffset); const today = cnTime.toISOString().split("T")[0]; console.log(`🔍 [Task3] ${today} 巡检已入库工具...`); const tools = await prisma.tool.findMany({ select: { id: true, name: true, websiteUrl: true, logoUrl: true, status: true }, }); const pendingTools = await prisma.pendingTool.findMany({ where: { status: "pending" }, select: { id: true, name: true, websiteUrl: true, logoUrl: true, checkWebsiteOk: true, checkLogoOk: true, categoryId: true }, }); const firstCategory = await prisma.category.findFirst({ select: { id: true } }); const defaultCategoryId = firstCategory?.id || 1; console.log(` 已发布工具: ${tools.length}, 待审核: ${pendingTools.length}`); let checked = 0; let websiteFails = 0; let logoFails = 0; let autoPublished = 0; const errors = []; for (const tool of tools) { try { const websiteOk = await checkWebsite(tool.websiteUrl); const logoOk = await checkLogo(tool.logoUrl); await prisma.tool.update({ where: { id: tool.id }, data: { lastCheckedAt: new Date(), websiteOk, logoOk }, }); if (!websiteOk) websiteFails++; if (!logoOk) logoFails++; checked++; } catch (e) { errors.push(`Tool ${tool.name}: ${e.message}`); } if (checked % 10 === 0) { console.log(` 进度: ${checked}/${tools.length}`); } } console.log(`\n 已发布工具巡检完成: ${checked}个, 网站失效${websiteFails}个, Logo异常${logoFails}个`); checked = 0; for (const pt of pendingTools) { try { const websiteOk = pt.websiteUrl ? await checkWebsite(pt.websiteUrl) : false; const logoOk = await checkLogo(pt.logoUrl); await prisma.pendingTool.update({ where: { id: pt.id }, data: { checkWebsiteOk: websiteOk, checkLogoOk: logoOk, checkDescOk: true, checkCategoryOk: true }, }); if (websiteOk && logoOk) { const slug = pt.slug || pt.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + "-" + Date.now().toString(36); const existingSlug = await prisma.tool.findUnique({ where: { slug } }); if (!existingSlug) { await prisma.tool.create({ data: { name: pt.name, slug, categoryId: pt.categoryId || defaultCategoryId, description: pt.description || "", logoUrl: pt.logoUrl, websiteUrl: pt.websiteUrl || "", pricingModel: pt.pricingModel || "freemium", pricingDetail: pt.pricingDetail, features: pt.features, tags: pt.tags, status: "published", websiteOk: true, logoOk: true, }, }); await prisma.pendingTool.delete({ where: { id: pt.id } }); autoPublished++; console.log(` ✅ 自动发布: ${pt.name}`); } } checked++; } catch (e) { errors.push(`PendingTool ${pt.name}: ${e.message}`); } if (checked % 5 === 0) { console.log(` 待审核进度: ${checked}/${pendingTools.length}`); } } const endTime = new Date(); const duration = endTime.getTime() - startTime.getTime(); const result = { totalTools: tools.length, totalPending: pendingTools.length, websiteFails, logoFails, autoPublished, duration: `${(duration / 1000).toFixed(1)}s`, }; console.log(`\n🏁 [Task3] 完成: 巡检${tools.length + pendingTools.length}个, 自动发布${autoPublished}个, 网站失效${websiteFails}个`); await prisma.taskLog.create({ data: { taskKey: "task3-check-tools", taskName: "工具巡检", status: errors.length > 0 ? "completed" : "completed", startedAt: startTime, finishedAt: endTime, duration, result, error: errors.length > 0 ? errors.slice(0, 5).join("\n") : null, }, }); await prisma.$disconnect(); } main() .then(() => { console.log("Done"); process.exit(0); }) .catch(async (e) => { console.error("❌ 任务三失败:", e.message); if (prisma) { try { await prisma.taskLog.create({ data: { taskKey: "task3-check-tools", taskName: "工具巡检", status: "failed", startedAt: new Date(), finishedAt: new Date(), duration: 0, error: e.message, }, }); } catch {} } process.exit(1); }).finally(() => prisma.$disconnect());