码桶
发现社区成员的开源项目
stats.js7 KB
// 系统统计接口:平台实例、消息、模型调用的统一视图。
const express = require('express');
const db = require('../lib/db');
const sysStats = require('../lib/sys-stats');
const router = express.Router();
function startOfToday() {
const d = new Date();
d.setHours(0, 0, 0, 0);
return Math.floor(d.getTime() / 1000);
}
// 按时间桶聚合(本地时区):range=1 -> 24 小时桶;否则按天桶。
function buildSeries(rows, range, now, getTs) {
const series = [];
if (range === 1) {
const hourNow = Math.floor(now / 3600);
for (let i = 0; i < 24; i++) {
const h = hourNow - (23 - i);
const dt = new Date(h * 3600 * 1000);
series.push({ label: String(dt.getHours()).padStart(2, '0') + ':00', rows: [] });
}
for (const r of rows) {
const ago = Math.floor((now - getTs(r)) / 3600);
if (ago >= 0 && ago < 24) series[23 - ago].rows.push(r);
}
} else {
const dayNow = Math.floor(now / 86400);
for (let i = 0; i < range; i++) {
const d = dayNow - (range - 1 - i);
const dt = new Date(d * 86400 * 1000);
series.push({ label: String(dt.getMonth() + 1).padStart(2, '0') + '/' + String(dt.getDate()).padStart(2, '0'), rows: [] });
}
for (const r of rows) {
const ago = Math.floor((now - getTs(r)) / 86400);
if (ago >= 0 && ago < range) series[range - 1 - ago].rows.push(r);
}
}
return series;
}
function aggMessages(series) {
return series.map((b) => {
let inC = 0;
let outC = 0;
for (const r of b.rows) {
if (r.direction === 1) inC++;
else if (r.direction === 2) outC++;
}
return { label: b.label, in: inC, out: outC, total: b.rows.length };
});
}
function aggModelCalls(series) {
return series.map((b) => {
let calls = 0;
let tokens = 0;
for (const r of b.rows) {
calls++;
tokens += r.total_tokens || 0;
}
return { label: b.label, calls, tokens };
});
}
router.post('/', async (req, res) => {
try {
const range = [1, 3, 7].includes(req.body && req.body.range) ? req.body.range : 1;
const now = Math.floor(Date.now() / 1000);
const todayStart = startOfToday();
const weekStart = now - 7 * 86400;
const bots = (await db.rows('SELECT id, name, login_status FROM bots ORDER BY id')) || [];
const instances = bots.length;
const running = bots.filter((b) => b.login_status === 'confirmed' || b.login_status === 'online').length;
const totalRow = await db.row('SELECT COUNT(*) AS c FROM messages');
const totalMessages = totalRow ? totalRow.c : 0;
const msgs = (await db.rows('SELECT direction, bot_id, created_at FROM messages WHERE created_at >= ?', [weekStart])) || [];
const calls = (await db.rows(
'SELECT model, mode, peer_id, total_tokens, prompt_tokens, completion_tokens, latency_ms, ttft_ms, success, created_at FROM model_calls WHERE created_at >= ?',
[weekStart]
)) || [];
const todayCalls = calls.filter((c) => c.created_at >= todayStart);
const todayModelCalls = todayCalls.length;
const todayTokens = todayCalls.reduce((s, c) => s + (c.total_tokens || 0), 0);
// 消息概览三档
const rangesMsg = {};
for (const d of [1, 3, 7]) {
const start = now - d * 86400;
let inC = 0;
let outC = 0;
for (const m of msgs) {
if (m.created_at >= start) {
if (m.direction === 1) inC++;
else if (m.direction === 2) outC++;
}
}
rangesMsg[d] = { in: inC, out: outC, total: inC + outC };
}
const msgTrend = aggMessages(buildSeries(msgs, range, now, (r) => r.created_at));
const rangeStart = now - range * 86400;
const botMap = {};
for (const m of msgs) {
if (m.created_at >= rangeStart) {
const k = m.bot_id;
if (!botMap[k]) botMap[k] = { bot_id: k, in: 0, out: 0, total: 0 };
botMap[k].total++;
if (m.direction === 1) botMap[k].in++;
else if (m.direction === 2) botMap[k].out++;
}
}
const byBot = Object.values(botMap)
.map((b) => {
const bot = bots.find((x) => x.id === b.bot_id);
return { bot_id: b.bot_id, name: bot ? bot.name : '平台#' + b.bot_id, ...b };
})
.sort((a, b) => b.total - a.total);
const callTrend = aggModelCalls(buildSeries(calls, range, now, (r) => r.created_at));
const succ = todayCalls.filter((c) => c.success).length;
const latArr = todayCalls.filter((c) => c.latency_ms).map((c) => c.latency_ms);
const ttftArr = todayCalls.filter((c) => c.ttft_ms).map((c) => c.ttft_ms);
const sumComp = todayCalls.reduce((s, c) => s + (c.completion_tokens || 0), 0);
const sumLatSec = latArr.reduce((s, v) => s + v, 0) / 1000;
const latencyAvg = latArr.length ? Math.round(latArr.reduce((s, v) => s + v, 0) / latArr.length) : 0;
const ttftAvg = ttftArr.length ? Math.round(ttftArr.reduce((s, v) => s + v, 0) / ttftArr.length) : 0;
const tpmAvg = sumLatSec > 0 ? Math.round(sumComp / (sumLatSec / 60)) : 0;
const successRate = todayCalls.length ? Math.round((succ / todayCalls.length) * 100) : 0;
const modelCallMap = {};
for (const c of todayCalls) {
const k = c.model || 'unknown';
if (!modelCallMap[k]) modelCallMap[k] = { model: k, calls: 0, tokens: 0 };
modelCallMap[k].calls++;
modelCallMap[k].tokens += c.total_tokens || 0;
}
const byModelCalls = Object.values(modelCallMap).sort((a, b) => b.calls - a.calls).slice(0, 10);
const byModelTokens = Object.values(modelCallMap).sort((a, b) => b.tokens - a.tokens).slice(0, 10);
const sessionMap = {};
for (const c of todayCalls) {
const k = c.peer_id || 'unknown';
if (!sessionMap[k]) sessionMap[k] = { peer_id: k, tokens: 0, calls: 0 };
sessionMap[k].tokens += c.total_tokens || 0;
sessionMap[k].calls++;
}
const topSessions = Object.values(sessionMap).sort((a, b) => b.tokens - a.tokens).slice(0, 10);
const sys = sysStats.getSysStats();
res.json({
ok: true,
system: {
instances,
running,
instanceList: bots,
totalMessages,
todayModelCalls,
todayTokens,
cpu: sys.cpu,
systemCpu: sys.systemCpu,
processMemoryMB: sys.processMemoryMB,
systemMemoryTotalGB: sys.systemMemoryTotalGB,
systemMemoryFreeGB: sys.systemMemoryFreeGB,
uptimeSec: sys.uptimeSec,
startedAt: sys.startedAt,
},
messages: { ranges: rangesMsg, trend: msgTrend, byBot },
models: {
todayCalls: todayModelCalls,
todayTokens,
totalCalls: calls.length,
ttftAvg,
latencyAvg,
tpmAvg,
successRate,
callTrend,
byModelCalls,
byModelTokens,
topSessions,
},
});
} catch (e) {
console.error('[stats] 统计失败:', e);
res.status(500).json({ ok: false, msg: '统计失败: ' + e.message });
}
});
module.exports = router;