mirror of
https://github.com/joeseesun/wechat-radar.git
synced 2026-09-10 09:38:29 +09:00
Initial open source release
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
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<string, number>(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<string, number>();
|
||||
const sendersByGroup = new Map<string, Map<string, number>>();
|
||||
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<string, number>();
|
||||
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<string, number[]>();
|
||||
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<string, number[]>();
|
||||
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<WxSession[]> {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user