import { NextRequest, NextResponse } from 'next/server'; import { wxSessions } from '@/lib/wx'; import type { WxSession } from '@/lib/wx-types'; import { listCachedStatsRange } from '@/lib/stats-aggregator'; import { listAllTags, listGroups, listFavorites } from '@/lib/groups'; import { effectiveGroupIds } from '@/lib/group-classifier'; import { rangeToWindow, dateList, normalizeDate, normalizeRangeKey } from '@/lib/range'; import { countMentionsBetween } from '@/lib/mentions'; import { buildDashboardIntelligence } from '@/lib/dashboard-intelligence'; import { cache, CK } from '@/lib/cache'; import { db } from '@/lib/db'; import { readConfig } from '@/lib/config'; export const dynamic = 'force-dynamic'; export async function GET(req: NextRequest) { try { const url = new URL(req.url); const range = normalizeRangeKey(url.searchParams.get('range'), 'week'); const anchorDate = normalizeDate(url.searchParams.get('date')); const w = rangeToWindow(range, anchorDate); const sessions = await loadSessionsSafe(500); const groups = sessions.filter((s) => s.is_group); const groupNames = new Map(groups.map((g) => [g.username, g.chat])); const allCount = groups.length; const cached = listCachedStatsRange(w.since, w.until); const totalMessages = cached.reduce((sum, r) => sum + r.total, 0); const dates = dateList(w.since, w.until); const trendByDate = new Map(dates.map((d) => [d, 0])); for (const r of cached) { if (trendByDate.has(r.date)) { trendByDate.set(r.date, (trendByDate.get(r.date) ?? 0) + r.total); } } const trend = dates.map((d) => ({ date: d, count: trendByDate.get(d) ?? 0 })); const peak = trend.reduce((max, t) => (t.count > max.count ? t : max), { date: '', count: 0 }); const sumTrend = trend.reduce((s, t) => s + t.count, 0); const avg = trend.length > 0 ? sumTrend / trend.length : 0; const totalsByGroup = new Map(); const sendersByGroup = new Map>(); for (const r of cached) { totalsByGroup.set(r.chatroom_id, (totalsByGroup.get(r.chatroom_id) ?? 0) + r.total); const senderMap = sendersByGroup.get(r.chatroom_id) ?? new Map(); for (const s of r.top_senders) { senderMap.set(s.sender, (senderMap.get(s.sender) ?? 0) + s.count); } sendersByGroup.set(r.chatroom_id, senderMap); } const active = groups.filter((g) => (totalsByGroup.get(g.username) ?? 0) > 0).length; const silent = allCount - active; const topActiveGroups = groups .map((g) => ({ chatroom_id: g.username, name: g.chat, summary: g.summary, total: totalsByGroup.get(g.username) ?? 0, top_senders: Array.from(sendersByGroup.get(g.username)?.entries() ?? []) .map(([sender, count]) => ({ sender, count })) .sort((a, b) => b.count - a.count) .slice(0, 3), })) .filter((g) => g.total > 0) .sort((a, b) => b.total - a.total); const tags = listAllTags(); const cats = listGroups(); const tagsByChatroom = new Map(); for (const t of tags) { const arr = tagsByChatroom.get(t.chatroom_id) ?? []; arr.push(t.group_id); tagsByChatroom.set(t.chatroom_id, arr); } const effectiveTagsByChatroom = new Map(); for (const g of groups) { effectiveTagsByChatroom.set( g.username, effectiveGroupIds(g.chat, g.summary, tagsByChatroom.get(g.username) ?? [], cats), ); } const taggedChatroomIds = new Set( Array.from(effectiveTagsByChatroom.entries()) .filter(([, ids]) => ids.length > 0) .map(([chatroomId]) => chatroomId), ); const unsortedCount = groups.filter((g) => (effectiveTagsByChatroom.get(g.username) ?? []).length === 0).length; const categoryStats = cats.map((c) => { const memberIds = Array.from(effectiveTagsByChatroom.entries()) .filter(([, ids]) => ids.includes(c.id)) .map(([chatroomId]) => chatroomId); const memberSet = new Set(memberIds); let groupMessageCount = 0; for (const r of cached) { if (memberSet.has(r.chatroom_id)) groupMessageCount += r.total; } return { id: c.id, name: c.name, color: c.color, emoji: c.emoji, group_count: memberIds.length, message_count: groupMessageCount, }; }); const unsortedMessageCount = cached .filter((r) => !taggedChatroomIds.has(r.chatroom_id)) .reduce((s, r) => s + r.total, 0); if (unsortedCount > 0) { categoryStats.push({ id: -1, name: '未分类', color: '#94a3b8', emoji: '❓', group_count: unsortedCount, message_count: unsortedMessageCount, }); } const favorites = listFavorites(); const mentionCount = countMentionsBetween(unixStartOfDay(w.since), unixEndOfDay(w.until)); return NextResponse.json({ ok: true, range, window: w, cards: { active_groups: active, total_groups: allCount, total_messages: totalMessages, mentions: mentionCount, silent_groups: silent, avg_per_group: allCount ? Math.round(totalMessages / allCount) : 0, }, trend: { data: trend, peak, avg, total: sumTrend, }, active_groups: topActiveGroups, categories: categoryStats, intelligence: buildDashboardIntelligence(w.until, groupNames), sidebar_counts: { all: allCount, favorites: favorites.length, unsorted: unsortedCount, }, }); } catch (e) { const message = e instanceof Error ? e.message : 'unknown error'; console.error('/api/stats failed', e); return NextResponse.json({ ok: false, error: message }, { status: 500 }); } } async function loadSessionsSafe(limit: number): Promise { if (readConfig().demoMode) return listLocalSessionsFallback(limit); const cached = cache.get(CK.sessions()) as WxSession[] | undefined; try { const sessions = await wxSessions(limit); cache.set(CK.sessions(), sessions, 60); return sessions; } catch (e) { if (cached?.length) return cached; console.warn('wx sessions failed, falling back to local radar.db', e); return listLocalSessionsFallback(limit); } } function listLocalSessionsFallback(limit: number): WxSession[] { const rows = db() .prepare( ` SELECT m.chatroom_id, m.sender, m.content, m.time, m.timestamp, m.type FROM messages m JOIN ( SELECT chatroom_id, MAX(timestamp) AS timestamp FROM messages GROUP BY chatroom_id ) latest ON latest.chatroom_id = m.chatroom_id AND latest.timestamp = m.timestamp GROUP BY m.chatroom_id ORDER BY m.timestamp DESC LIMIT ? `, ) .all(limit) as Array<{ chatroom_id: string; sender: string; content: string; time: string; timestamp: number; type: string; }>; return rows.map((r) => ({ chat: r.chatroom_id, chat_type: 'group', is_group: true, last_msg_type: r.type, last_sender: r.sender, summary: r.content, time: r.time, timestamp: r.timestamp, unread: 0, username: r.chatroom_id, })); } function unixStartOfDay(date: string) { const [year, month, day] = date.split('-').map(Number); return Math.floor(new Date(year, month - 1, day, 0, 0, 0, 0).getTime() / 1000); } function unixEndOfDay(date: string) { const [year, month, day] = date.split('-').map(Number); return Math.floor(new Date(year, month - 1, day, 23, 59, 59, 999).getTime() / 1000); }