// 数字人 vs 真人"对抗学习" 调度入口 // 每周给所有专家 bot 各选 1 篇真实高赞真人帖,提取"为什么火"的洞察 // 用法: // node scripts/bot-adversarial-learning-run.mjs # 默认本周 // node scripts/bot-adversarial-learning-run.mjs --week=2026-W23 # 指定周 // node scripts/bot-adversarial-learning-run.mjs --force # 强制重学(覆盖已有) // node scripts/bot-adversarial-learning-run.mjs --bot=laochen # 单 bot import { PrismaClient } from "@prisma/client"; import { PrismaMariaDb } from "@prisma/adapter-mariadb"; import { fileURLToPath } from "url"; import { dirname } from "path"; import "dotenv/config"; import { runWeeklyAdversarialLearning, getISOWeekKey, disconnectAdversarialLearning, } from "./lib/bot-adversarial-learning.mjs"; const __dirname = dirname(fileURLToPath(import.meta.url)); const base = process.env.DATABASE_URL.replace("mysql://", "mariadb://"); const sep = base.includes("?") ? "&" : "?"; const connectionString = `${base}${sep}connection_limit=3&pool_timeout=10`; const adapter = new PrismaMariaDb(connectionString); const prisma = new PrismaClient({ adapter }); function ts() { return `[${new Date().toISOString()}]`; } function parseArgs() { const args = process.argv.slice(2); const out = { week: null, force: false, bot: null, lookback: 7 }; for (const a of args) { if (a.startsWith("--week=")) out.week = a.split("=")[1]; else if (a === "--force") out.force = true; else if (a.startsWith("--bot=")) out.bot = a.split("=")[1]; else if (a.startsWith("--lookback=")) out.lookback = parseInt(a.split("=")[1], 10); } return out; } async function main() { const args = parseArgs(); const weekKey = args.week || getISOWeekKey(); console.log( `${ts()} Adversarial Learning run starting (week=${weekKey}, force=${args.force}, bot=${args.bot || "all"}, lookback=${args.lookback}d)` ); if (!process.env.DATABASE_URL) { throw new Error("DATABASE_URL 未配置"); } const startTime = Date.now(); // --force 时:先把本周已有 learning 删掉(针对限定 bot) if (args.force) { const where = args.bot ? { bot: { user: { email: `bot_${args.bot}@zhuiguang.ai` } }, weekKey } : { weekKey }; const del = await prisma.botAdversarialLearning.deleteMany({ where }); console.log(`${ts()} --force: 删除了 ${del.count} 条已有 learning`); } // 单 bot 模式 let botConfigIds = null; if (args.bot) { const u = await prisma.user.findFirst({ where: { email: `bot_${args.bot}@zhuiguang.ai` } }); if (!u) { console.error(`${ts()} Bot "${args.bot}" 未找到`); process.exit(1); } const c = await prisma.botConfig.findUnique({ where: { userId: u.id } }); if (!c) { console.error(`${ts()} Bot "${args.bot}" 没有 BotConfig`); process.exit(1); } botConfigIds = [c.id]; } const summary = await runWeeklyAdversarialLearning({ weekKey, lookbackDays: args.lookback, botConfigIds, }); const duration = Date.now() - startTime; console.log( `${ts()} Done: bots=${summary.botCount} learned=${summary.learned} skipped=${summary.skipped} errors=${summary.errors} (${duration}ms)` ); for (const r of summary.results.slice(0, 10)) { if (r.error) { console.log(`${ts()} ✗ ${r.botName} ${r.error}`); } else if (r.skipped) { console.log(`${ts()} ⤼ ${r.botName} skip=${r.reason}`); } else { const rec = r.record; console.log( `${ts()} ✓ ${r.botName} ← topic#${rec.sourceRefId} "${rec.sourceTitle?.slice(0, 30)}" rel=${rec.relevanceScore} cat=${JSON.stringify(rec.learnCategories)}` ); } } await prisma.taskLog.create({ data: { taskKey: "bot-adversarial-learning-run", taskName: "Bot 对抗学习(真人高赞帖)", status: summary.errors > 0 ? "partial" : "success", startedAt: new Date(startTime), finishedAt: new Date(), triggerBy: "cron", result: { weekKey, botCount: summary.botCount, learned: summary.learned, skipped: summary.skipped, errors: summary.errors, force: args.force, lookbackDays: args.lookback, durationMs: duration, }, }, }); } main() .catch(async (e) => { console.error(`${ts()} Fatal:`, e); try { await prisma.taskLog.create({ data: { taskKey: "bot-adversarial-learning-run", taskName: "Bot 对抗学习(真人高赞帖)", status: "failed", startedAt: new Date(), finishedAt: new Date(), triggerBy: "cron", error: (e && e.message ? e.message : String(e)).slice(0, 1000), }, }); } catch {} process.exit(1); }) .finally(async () => { await prisma.$disconnect(); await disconnectAdversarialLearning(); });