码桶

发现社区成员的开源项目

奶狗 /

ng-webot

公开
main
ng-webot/routes/stats.js
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;