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(); 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 }); } }