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