// Bot Persona A/B 测试 调度入口 // 1) 采集最近 N 天 assignment 的真实互动数据 // 2) 回填 bot_persona_metrics(按变体按天聚合) // 3) 更新变体的 sampleCount/replyCount/likeCount/humanReplyCount/engagementScore // 4) 对所有 bot 的实验做显著性分析,winner 自动提升权重 // // 用法: // node scripts/bot-persona-experiment-run.mjs # 默认跑 1+2+3+4 // node scripts/bot-persona-experiment-run.mjs --no-switch # 只跑 1+2+3 // node scripts/bot-persona-experiment-run.mjs --lookback=14 # 自定义回看天数 // node scripts/bot-persona-experiment-run.mjs --bot=laochen # 只为指定 bot 跑 import { PrismaClient } from "@prisma/client"; import { PrismaMariaDb } from "@prisma/adapter-mariadb"; import { readFileSync } from "fs"; import { resolve, dirname } from "path"; import { fileURLToPath } from "url"; import "dotenv/config"; import { recordAssignmentMetrics, analyzeAndSwitchAllBots, disconnectBotExperiment, } from "./lib/bot-persona-experiment.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 }); const BOT_DATA_PATH = resolve(__dirname, "..", "data", "bot-characters.json"); function ts() { return `[${new Date().toISOString()}]`; } function parseArgs() { const args = process.argv.slice(2); const out = { lookback: 7, noSwitch: false, bot: null, mode: "all" }; for (const a of args) { if (a.startsWith("--lookback=")) out.lookback = parseInt(a.split("=")[1], 10); else if (a === "--no-switch") out.noSwitch = true; else if (a === "--metrics-only") out.mode = "metrics"; else if (a === "--analyze-only") out.mode = "analyze"; else if (a.startsWith("--bot=")) out.bot = a.split("=")[1]; } return out; } async function main() { const args = parseArgs(); console.log(`${ts()} Bot Persona A/B run starting (lookback=${args.lookback}d, mode=${args.mode}, bot=${args.bot || "all"}, noSwitch=${args.noSwitch})`); if (!process.env.DATABASE_URL) { throw new Error("DATABASE_URL 未配置"); } const startTime = Date.now(); let metricsResult = null; let analysisResult = null; // 1) 采集指标 if (args.mode === "all" || args.mode === "metrics") { console.log(`${ts()} [Step 1] 采集 assignment 真实互动数据 ...`); metricsResult = await recordAssignmentMetrics({ lookbackDays: args.lookback }); console.log( `${ts()} ✓ updated=${metricsResult.updated} assignments=${metricsResult.assignments} dayBuckets=${metricsResult.dayBuckets}` ); } // 2) 分析 + 切换 if (!args.noSwitch && (args.mode === "all" || args.mode === "analyze")) { console.log(`${ts()} [Step 2] 显著性分析 + winner 权重切换 ...`); analysisResult = await analyzeAndSwitchAllBots(); console.log(`${ts()} ✓ botCount=${analysisResult.botCount}`); for (const r of analysisResult.results.slice(0, 10)) { console.log( `${ts()} bot#${r.botId} winner=${r.winner || "∅"} action=${r.action} (${r.reason || ""})` ); } } const duration = Date.now() - startTime; console.log(`${ts()} Done in ${duration}ms.`); await prisma.taskLog.create({ data: { taskKey: "bot-persona-experiment-run", taskName: "Bot Persona A/B 调度", status: "success", startedAt: new Date(startTime), finishedAt: new Date(), triggerBy: "cron", result: { lookbackDays: args.lookback, mode: args.mode, noSwitch: args.noSwitch, bot: args.bot, metricsResult, analysisResult, durationMs: duration, }, }, }); } main() .catch(async (e) => { console.error(`${ts()} Fatal:`, e); try { await prisma.taskLog.create({ data: { taskKey: "bot-persona-experiment-run", taskName: "Bot Persona A/B 调度", 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 disconnectBotExperiment(); });