全项目扫描修复: Docker数据卷修复+安全requireAdmin+12页SEO+API白名单+脚本超时+常量提取+假数据删除
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// 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();
|
||||
});
|
||||
Reference in New Issue
Block a user