全项目扫描修复: Docker数据卷修复+安全requireAdmin+12页SEO+API白名单+脚本超时+常量提取+假数据删除
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// Bot 表现统计:拉近 N 天的每日统计 + 总览指标
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const days = Math.min(Math.max(parseInt(searchParams.get("days") || "14", 10), 1), 60);
|
||||
const botParam = searchParams.get("botUserId");
|
||||
|
||||
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
||||
const sinceDate = new Date(since.getFullYear(), since.getMonth(), since.getDate());
|
||||
|
||||
// 1. 全部 bot 基础信息
|
||||
const botUsers = await prisma.user.findMany({
|
||||
where: { isBot: true },
|
||||
select: { id: true, name: true, email: true, avatarUrl: true, points: true, level: true, createdAt: true },
|
||||
});
|
||||
if (botUsers.length === 0) {
|
||||
return NextResponse.json({ stats: [], summary: {}, trends: [] });
|
||||
}
|
||||
const botById = new Map(botUsers.map((u) => [u.id, u]));
|
||||
const botIds = botUsers.map((u) => u.id);
|
||||
|
||||
// 2. BotConfig 拿到 botKey 和 primaryForums
|
||||
const botConfigs = await prisma.botConfig.findMany({
|
||||
where: { userId: { in: botIds } },
|
||||
select: { id: true, userId: true, primaryForums: true, displayName: true },
|
||||
});
|
||||
const configByUserId = new Map(botConfigs.map((c) => [c.userId, c]));
|
||||
|
||||
// 3. 周期内 BotDailyStat 聚合
|
||||
const dailyStats = await prisma.botDailyStat.groupBy({
|
||||
by: ["botId"],
|
||||
where: { date: { gte: sinceDate } },
|
||||
_sum: {
|
||||
topicCreated: true,
|
||||
replySent: true,
|
||||
followUpSent: true,
|
||||
repliesReceived: true,
|
||||
likesReceived: true,
|
||||
humanInteractions: true,
|
||||
feedbackProcessed: true,
|
||||
},
|
||||
});
|
||||
const statMap = new Map(
|
||||
dailyStats.map((s) => [
|
||||
s.botId,
|
||||
{
|
||||
topicCreated: s._sum.topicCreated || 0,
|
||||
replySent: s._sum.replySent || 0,
|
||||
followUpSent: s._sum.followUpSent || 0,
|
||||
repliesReceived: s._sum.repliesReceived || 0,
|
||||
likesReceived: s._sum.likesReceived || 0,
|
||||
humanInteractions: s._sum.humanInteractions || 0,
|
||||
feedbackProcessed: s._sum.feedbackProcessed || 0,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
// 4. Memory 数量
|
||||
const memCounts = await prisma.botMemory.groupBy({
|
||||
by: ["botId"],
|
||||
_count: { _all: true },
|
||||
_avg: { importance: true },
|
||||
});
|
||||
const memMap = new Map(
|
||||
memCounts.map((m) => [
|
||||
m.botId,
|
||||
{ count: m._count._all, avgImportance: m._avg.importance || 0 },
|
||||
])
|
||||
);
|
||||
|
||||
// 5. Persona + 跨板块亲和度
|
||||
const personas = await prisma.botPersona.findMany({
|
||||
select: {
|
||||
botId: true,
|
||||
avgReplyLength: true,
|
||||
questionRatio: true,
|
||||
postSampleCount: true,
|
||||
stanceKeywords: true,
|
||||
},
|
||||
});
|
||||
const personaMap = new Map(personas.map((p) => [p.botId, p]));
|
||||
const affinities = await prisma.botCrossForumAffinity.findMany({
|
||||
select: { botId: true, forumSlug: true, affinity: true, sampleCount: true },
|
||||
});
|
||||
const affinityMap = new Map<number, typeof affinities>();
|
||||
for (const a of affinities) {
|
||||
if (!affinityMap.has(a.botId)) affinityMap.set(a.botId, []);
|
||||
affinityMap.get(a.botId)!.push(a);
|
||||
}
|
||||
|
||||
// 6. 计算每个 bot 的"健康分"
|
||||
const stats = botUsers.map((u) => {
|
||||
const config = configByUserId.get(u.id);
|
||||
const cfgId = config?.id ?? 0;
|
||||
const d = statMap.get(cfgId) || {
|
||||
topicCreated: 0,
|
||||
replySent: 0,
|
||||
followUpSent: 0,
|
||||
repliesReceived: 0,
|
||||
likesReceived: 0,
|
||||
humanInteractions: 0,
|
||||
feedbackProcessed: 0,
|
||||
};
|
||||
const mem = memMap.get(cfgId) || { count: 0, avgImportance: 0 };
|
||||
const persona = personaMap.get(cfgId);
|
||||
const affs = affinityMap.get(cfgId) || [];
|
||||
|
||||
// 综合健康分:发帖×3 + 回复×1 + 收赞×2 + 真人互动×5
|
||||
const healthScore =
|
||||
d.topicCreated * 3 +
|
||||
d.replySent * 1 +
|
||||
d.likesReceived * 2 +
|
||||
d.humanInteractions * 5;
|
||||
|
||||
// 平均亲和度
|
||||
const avgAffinity = affs.length > 0
|
||||
? affs.reduce((s, a) => s + a.affinity, 0) / affs.length
|
||||
: 0;
|
||||
|
||||
// 状态判定
|
||||
let status: "active" | "warming" | "fresh" | "idle" = "idle";
|
||||
if (healthScore >= 50) status = "active";
|
||||
else if (healthScore >= 10) status = "warming";
|
||||
else if (d.humanInteractions > 0 || d.likesReceived > 0) status = "fresh";
|
||||
else status = "idle";
|
||||
|
||||
return {
|
||||
userId: u.id,
|
||||
name: u.name,
|
||||
avatarUrl: u.avatarUrl,
|
||||
points: u.points,
|
||||
level: u.level,
|
||||
botConfigId: config?.id || null,
|
||||
primaryForums: (config?.primaryForums as string[]) || [],
|
||||
topicCreated: d.topicCreated,
|
||||
replySent: d.replySent,
|
||||
followUpSent: d.followUpSent,
|
||||
repliesReceived: d.repliesReceived,
|
||||
likesReceived: d.likesReceived,
|
||||
humanInteractions: d.humanInteractions,
|
||||
feedbackProcessed: d.feedbackProcessed,
|
||||
memoryCount: mem.count,
|
||||
avgImportance: mem.avgImportance,
|
||||
personaQuestionRatio: persona?.questionRatio || 0,
|
||||
personaSampleCount: persona?.postSampleCount || 0,
|
||||
affinityCount: affs.length,
|
||||
avgAffinity: Math.round(avgAffinity * 100) / 100,
|
||||
healthScore,
|
||||
status,
|
||||
};
|
||||
});
|
||||
|
||||
// 按健康分排序
|
||||
stats.sort((a, b) => b.healthScore - a.healthScore);
|
||||
|
||||
// 7. 时间趋势(按天聚合)
|
||||
const trendRaw = await prisma.botDailyStat.groupBy({
|
||||
by: ["date"],
|
||||
where: { date: { gte: sinceDate } },
|
||||
_sum: {
|
||||
topicCreated: true,
|
||||
replySent: true,
|
||||
likesReceived: true,
|
||||
humanInteractions: true,
|
||||
},
|
||||
orderBy: { date: "asc" },
|
||||
});
|
||||
const trends = trendRaw.map((t) => ({
|
||||
date: t.date.toISOString().split("T")[0],
|
||||
topicCreated: t._sum.topicCreated || 0,
|
||||
replySent: t._sum.replySent || 0,
|
||||
likesReceived: t._sum.likesReceived || 0,
|
||||
humanInteractions: t._sum.humanInteractions || 0,
|
||||
}));
|
||||
|
||||
// 7b. 如果指定了 botUserId,额外返回该 bot 的每日趋势
|
||||
let perBotTrend: typeof trends = [];
|
||||
if (botParam) {
|
||||
const botUserId = Number(botParam);
|
||||
if (!Number.isNaN(botUserId)) {
|
||||
const botConfig = configByUserId.get(botUserId);
|
||||
if (botConfig) {
|
||||
const perBot = await prisma.botDailyStat.findMany({
|
||||
where: { botId: botConfig.id, date: { gte: sinceDate } },
|
||||
orderBy: { date: "asc" },
|
||||
});
|
||||
perBotTrend = perBot.map((d) => ({
|
||||
date: d.date.toISOString().split("T")[0],
|
||||
topicCreated: d.topicCreated,
|
||||
replySent: d.replySent,
|
||||
likesReceived: d.likesReceived,
|
||||
humanInteractions: d.humanInteractions,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 全局汇总
|
||||
const summary = {
|
||||
totalBots: stats.length,
|
||||
activeBots: stats.filter((s) => s.status === "active").length,
|
||||
warmingBots: stats.filter((s) => s.status === "warming").length,
|
||||
idleBots: stats.filter((s) => s.status === "idle").length,
|
||||
totalTopics: stats.reduce((s, b) => s + b.topicCreated, 0),
|
||||
totalReplies: stats.reduce((s, b) => s + b.replySent, 0),
|
||||
totalLikes: stats.reduce((s, b) => s + b.likesReceived, 0),
|
||||
totalHumanInteractions: stats.reduce((s, b) => s + b.humanInteractions, 0),
|
||||
totalMemories: stats.reduce((s, b) => s + b.memoryCount, 0),
|
||||
avgHealthScore: stats.length > 0
|
||||
? Math.round(stats.reduce((s, b) => s + b.healthScore, 0) / stats.length)
|
||||
: 0,
|
||||
days,
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
stats: stats.slice(0, 50), // 最多 50 个 bot(已排序)
|
||||
summary,
|
||||
trends,
|
||||
perBotTrend,
|
||||
botById: Object.fromEntries(botUsers.map((u) => [u.id, u.name])),
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as { message?: string };
|
||||
return NextResponse.json({ error: err.message || "Bot 统计失败" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user