230 lines
6.9 KiB
JavaScript
230 lines
6.9 KiB
JavaScript
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 { updatePersona as sharedUpdatePersona, computePersona } from "./lib/bot-persona.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=5&pool_timeout=30`;
|
|
const adapter = new PrismaMariaDb(connectionString);
|
|
const prisma = new PrismaClient({ adapter });
|
|
|
|
const BOT_DATA_PATH = resolve(__dirname, "..", "data", "bot-characters.json");
|
|
|
|
const LOOKBACK_DAYS = 14;
|
|
const MIN_AFFINITY_SAMPLES = 1;
|
|
|
|
function ts() {
|
|
return `[${new Date().toISOString()}]`;
|
|
}
|
|
|
|
async function loadBotCharacters() {
|
|
const raw = readFileSync(BOT_DATA_PATH, "utf-8");
|
|
const { characters } = JSON.parse(raw);
|
|
const map = {};
|
|
for (const c of characters) map[c.key] = c;
|
|
return { characters, map };
|
|
}
|
|
|
|
async function computeAffinity(botUser, botConfigId) {
|
|
const since = new Date(Date.now() - LOOKBACK_DAYS * 24 * 60 * 60 * 1000);
|
|
const myTopics = await prisma.forumTopic.findMany({
|
|
where: { userId: botUser.id, createdAt: { gt: since } },
|
|
select: { id: true, category: { select: { slug: true } } },
|
|
});
|
|
|
|
if (myTopics.length === 0) return { homeForums: new Map(), crossCount: 0 };
|
|
|
|
const topicIds = myTopics.map((t) => t.id);
|
|
const myForumSlugs = new Set(myTopics.map((t) => t.category.slug));
|
|
|
|
const humanRepliers = await prisma.forumPost.findMany({
|
|
where: {
|
|
topicId: { in: topicIds },
|
|
user: { isBot: false },
|
|
},
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
isBot: true,
|
|
forumTopics: {
|
|
where: { createdAt: { gt: since } },
|
|
select: { category: { select: { slug: true } } },
|
|
take: 10,
|
|
},
|
|
},
|
|
},
|
|
topic: { select: { category: { select: { slug: true, name: true } } } },
|
|
},
|
|
});
|
|
|
|
const homeForumCount = new Map();
|
|
for (const p of humanRepliers) {
|
|
const homeForums = p.user.forumTopics.map((t) => t.category.slug);
|
|
if (homeForums.length === 0) continue;
|
|
const counts = new Map();
|
|
for (const f of homeForums) counts.set(f, (counts.get(f) || 0) + 1);
|
|
const sorted = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]);
|
|
const homeForum = sorted[0][0];
|
|
if (myForumSlugs.has(homeForum)) continue;
|
|
homeForumCount.set(homeForum, (homeForumCount.get(homeForum) || 0) + 1);
|
|
}
|
|
|
|
return { homeForums: homeForumCount, crossCount: humanRepliers.length };
|
|
}
|
|
|
|
async function updatePersona(botChar) {
|
|
const botUser = await prisma.user.findFirst({
|
|
where: { email: `bot_${botChar.key}@zhuiguang.ai` },
|
|
});
|
|
if (!botUser) return false;
|
|
|
|
const botConfig = await prisma.botConfig.findUnique({
|
|
where: { userId: botUser.id },
|
|
});
|
|
if (!botConfig) return false;
|
|
|
|
// 调用 lib/bot-persona.mjs 的共享实现(同步)
|
|
const result = await sharedUpdatePersona(botUser, botConfig.id);
|
|
if (!result.success) return false;
|
|
|
|
// 再读一份用于日志输出
|
|
const persona = await computePersona(botUser, botConfig.id);
|
|
console.log(
|
|
`${ts()} 📐 ${botChar.displayName} 画像: 话题${persona.postSampleCount}条 / 平均话题${persona.avgTopicLength}字 / 平均回复${persona.avgReplyLength}字 / 问号率${persona.questionRatio}`
|
|
);
|
|
return true;
|
|
}
|
|
|
|
async function updateAffinity(botChar) {
|
|
const botUser = await prisma.user.findFirst({
|
|
where: { email: `bot_${botChar.key}@zhuiguang.ai` },
|
|
});
|
|
if (!botUser) return;
|
|
|
|
const botConfig = await prisma.botConfig.findUnique({
|
|
where: { userId: botUser.id },
|
|
});
|
|
if (!botConfig) return;
|
|
|
|
const { homeForums, crossCount } = await computeAffinity(botUser, botConfig.id);
|
|
|
|
const primaryForumSlugs = new Set(
|
|
(botChar.personality?.primaryForums || []).map((s) => s)
|
|
);
|
|
|
|
await prisma.botCrossForumAffinity.deleteMany({
|
|
where: {
|
|
botId: botConfig.id,
|
|
forumSlug: { in: Array.from(primaryForumSlugs) },
|
|
},
|
|
});
|
|
|
|
if (homeForums.size === 0) {
|
|
return;
|
|
}
|
|
|
|
const totalCrossSamples = Array.from(homeForums.values()).reduce((s, n) => s + n, 0);
|
|
|
|
for (const [slug, count] of homeForums.entries()) {
|
|
if (count < MIN_AFFINITY_SAMPLES) continue;
|
|
const affinity = Math.min(1, count / Math.max(crossCount / 4, 5));
|
|
const reason = `${count}/${totalCrossSamples} 跨板块互动`;
|
|
await prisma.botCrossForumAffinity.upsert({
|
|
where: { botId_forumSlug: { botId: botConfig.id, forumSlug: slug } },
|
|
create: {
|
|
botId: botConfig.id,
|
|
forumSlug: slug,
|
|
affinity,
|
|
sampleCount: count,
|
|
reason,
|
|
},
|
|
update: {
|
|
affinity,
|
|
sampleCount: count,
|
|
reason,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (homeForums.size > 0) {
|
|
const top = Array.from(homeForums.entries())
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 3)
|
|
.map(([s, c]) => `${s}(${c})`)
|
|
.join(", ");
|
|
console.log(`${ts()} 🌍 ${botChar.displayName} 跨板块亲和度: ${top}`);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log(`${ts()} Bot Affinity & Persona Update starting...`);
|
|
|
|
if (!process.env.DATABASE_URL) {
|
|
throw new Error("DATABASE_URL 未配置");
|
|
}
|
|
|
|
const { characters } = await loadBotCharacters();
|
|
const botChars = characters.filter((c) => !c.role || c.role !== "passerby");
|
|
console.log(`${ts()} Loaded ${botChars.length} expert bots.`);
|
|
|
|
let personaCount = 0;
|
|
let affinityCount = 0;
|
|
|
|
for (const botChar of botChars) {
|
|
try {
|
|
if (await updatePersona(botChar)) personaCount++;
|
|
await updateAffinity(botChar);
|
|
affinityCount++;
|
|
} catch (err) {
|
|
console.error(`${ts()} Error processing ${botChar.key}:`, err.message);
|
|
}
|
|
}
|
|
|
|
await prisma.taskLog.create({
|
|
data: {
|
|
taskKey: "bot-affinity-update",
|
|
taskName: "Bot亲和度与画像",
|
|
status: "success",
|
|
startedAt: new Date(),
|
|
finishedAt: new Date(),
|
|
triggerBy: "cron",
|
|
result: { personaCount, affinityCount, totalBots: botChars.length },
|
|
},
|
|
});
|
|
|
|
console.log(
|
|
`${ts()} Bot Affinity & Persona Update done. persona=${personaCount} affinity=${affinityCount}`
|
|
);
|
|
}
|
|
|
|
main()
|
|
.catch(async (e) => {
|
|
console.error(`${ts()} Fatal error:`, e);
|
|
try {
|
|
await prisma.taskLog.create({
|
|
data: {
|
|
taskKey: "bot-affinity-update",
|
|
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();
|
|
const { disconnectBotPersona } = await import("./lib/bot-persona.mjs");
|
|
await disconnectBotPersona();
|
|
});
|