feat: 增加计数功能

This commit is contained in:
foxhui
2026-01-11 19:25:34 +08:00
Unverified
parent 3183f7bdc5
commit aa730e7f22
29 changed files with 446 additions and 30 deletions
+38 -2
View File
@@ -35,6 +35,7 @@ import {
} from '../../../config/validator.js';
import { registry } from '../../../backend/registry.js';
import { sendRestartSignal, sendStopSignal, isUnderSupervisor, getVncInfo } from '../../../utils/ipc.js';
import { getTodayStats, getStatsRange, clearStatsRange } from '../../../utils/stats.js';
/**
* 读取请求体
@@ -395,18 +396,53 @@ export function createAdminRouter(context) {
// ==================== 统计与监控 ====================
// GET /admin/stats - 基本统计
// GET /admin/stats - 基本统计(包含今日成功/失败)
if (method === 'GET' && pathname === '/stats') {
const instances = config.backend?.pool?.instances || [];
const workers = config.backend?.pool?.workers || [];
const todayStats = getTodayStats();
sendJson(res, 200, {
instances: instances.length,
workers: workers.length
workers: workers.length,
success: todayStats.success,
failed: todayStats.failed
});
return;
}
// GET /admin/stats/range - 查询日期范围统计
if (method === 'GET' && pathname === '/stats/range') {
const url = new URL(req.url, `http://${req.headers.host}`);
const start = url.searchParams.get('start');
const end = url.searchParams.get('end');
if (!start || !end) {
sendApiError(res, { code: ERROR_CODES.INVALID_REQUEST_BODY, message: '缺少 start 或 end 参数' });
return;
}
const result = await getStatsRange(start, end);
sendJson(res, 200, result);
return;
}
// DELETE /admin/stats/range - 删除日期范围统计
if (method === 'DELETE' && pathname === '/stats/range') {
const url = new URL(req.url, `http://${req.headers.host}`);
const start = url.searchParams.get('start');
const end = url.searchParams.get('end');
if (!start || !end) {
sendApiError(res, { code: ERROR_CODES.INVALID_REQUEST_BODY, message: '缺少 start 或 end 参数' });
return;
}
const result = await clearStatsRange(start, end);
sendJson(res, 200, { success: true, deleted: result.deleted });
return;
}
// GET /admin/queue - 任务队列状态
if (method === 'GET' && pathname === '/queue') {
const queueStatus = queueManager.getStatus();
+6 -1
View File
@@ -14,6 +14,7 @@ import {
buildChatCompletionChunk
} from './respond.js';
import { ERROR_CODES } from './errors.js';
import { incrementSuccess, incrementFailed } from '../utils/stats.js';
/**
* @typedef {object} TaskContext
@@ -118,7 +119,8 @@ export function createQueueManager(queueConfig, callbacks) {
// 处理结果
if (result.error) {
// 生成失败:使用标准错误格式返回
// 生成失败:记录统计并返回错误
await incrementFailed();
sendApiError(res, {
code: ERROR_CODES.GENERATION_FAILED,
message: result.error,
@@ -141,6 +143,7 @@ export function createQueueManager(queueConfig, callbacks) {
finalContent = result.text || '生成失败';
}
logger.info('服务器', '结果已准备就绪', { id });
await incrementSuccess();
// 发送成功响应
logger.info('服务器', '准备发送响应...', { id, isStreaming, contentLength: finalContent.length });
@@ -159,6 +162,8 @@ export function createQueueManager(queueConfig, callbacks) {
// 清除心跳
if (heartbeatInterval) clearInterval(heartbeatInterval);
// 记录失败统计
await incrementFailed();
logger.error('服务器', '任务处理失败', { id, error: err.message });
sendApiError(res, {
code: ERROR_CODES.INTERNAL_ERROR,
+4
View File
@@ -25,6 +25,7 @@ const { getBackend } = await import('../backend/index.js');
const { logger } = await import('../utils/logger.js');
const { createQueueManager, createGlobalRouter } = await import('./index.js');
const { isUnderSupervisor } = await import('../utils/ipc.js');
const { loadTodayStats } = await import('../utils/stats.js');
// ==================== 初始化配置 ====================
@@ -131,6 +132,9 @@ const handleRequest = createGlobalRouter({
* @returns {Promise<void>}
*/
async function startServer() {
// 加载今日统计
await loadTodayStats();
// 登录模式提示
if (isLoginMode) {
logger.info('服务器', '登录模式已就绪,请在浏览器中完成登录操作');
+2 -2
View File
@@ -4,7 +4,7 @@
*
* - 环境变量:LOG_LEVEL=debug|info|warn|error
* - 输出格式:YYYY-MM-DD HH:mm:ss.SSS [LEVEL] [模块] 消息 | k=v ...
* - 日志文件:data/temp/system.log(超过 5MB 自动轮转)
* - 日志文件:data/logs/system.log(超过 5MB 自动轮转)
*/
import process from 'process';
@@ -23,7 +23,7 @@ const COLORS = {
};
// 日志文件配置
const LOG_DIR = path.join(process.cwd(), 'data', 'temp');
const LOG_DIR = path.join(process.cwd(), 'data', 'logs');
const LOG_FILE = path.join(LOG_DIR, 'system.log');
const LOG_FILE_OLD = path.join(LOG_DIR, 'system.log.old');
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
+183
View File
@@ -0,0 +1,183 @@
/**
* @fileoverview 请求统计管理模块
* @description 按日期存储成功/失败请求计数,支持日期范围查询和删除
*/
import { promises as fs } from 'fs';
import path from 'path';
// 日志目录
const LOG_DIR = path.join(process.cwd(), 'data', 'logs');
/**
* 获取指定日期的统计文件路径
* @param {string} date - YYYY-MM-DD 格式的日期
* @returns {string}
*/
function getStatsFilePath(date) {
return path.join(LOG_DIR, `stats_${date}.json`);
}
/**
* 获取今日日期字符串
* @returns {string} YYYY-MM-DD 格式
*/
function getTodayDateStr() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 内存缓存:今日统计
let todayStats = { success: 0, failed: 0 };
let todayDate = getTodayDateStr();
/**
* 确保日志目录存在
*/
async function ensureLogDir() {
try {
await fs.mkdir(LOG_DIR, { recursive: true });
} catch { /* 忽略已存在错误 */ }
}
/**
* 检查并切换日期(跨天时自动重置缓存)
*/
async function checkDateRollover() {
const currentDate = getTodayDateStr();
if (currentDate !== todayDate) {
// 保存昨日数据
await saveStats(todayDate, todayStats);
// 重置为新的一天
todayDate = currentDate;
todayStats = { success: 0, failed: 0 };
// 尝试加载今日已有数据
await loadTodayStats();
}
}
/**
* 保存统计到文件
* @param {string} date - 日期
* @param {object} stats - 统计数据
*/
async function saveStats(date, stats) {
await ensureLogDir();
const filePath = getStatsFilePath(date);
await fs.writeFile(filePath, JSON.stringify(stats, null, 2));
}
/**
* 加载今日统计(服务启动时调用)
*/
export async function loadTodayStats() {
await ensureLogDir();
todayDate = getTodayDateStr();
const filePath = getStatsFilePath(todayDate);
try {
const data = await fs.readFile(filePath, 'utf-8');
todayStats = JSON.parse(data);
} catch {
todayStats = { success: 0, failed: 0 };
}
return todayStats;
}
/**
* 增加成功计数
*/
export async function incrementSuccess() {
await checkDateRollover();
todayStats.success++;
await saveStats(todayDate, todayStats);
}
/**
* 增加失败计数
*/
export async function incrementFailed() {
await checkDateRollover();
todayStats.failed++;
await saveStats(todayDate, todayStats);
}
/**
* 获取今日统计
* @returns {{success: number, failed: number}}
*/
export function getTodayStats() {
// 检查是否跨天(同步版本,仅检查不保存)
const currentDate = getTodayDateStr();
if (currentDate !== todayDate) {
// 返回空数据,等待下次写入时触发跨天处理
return { success: 0, failed: 0 };
}
return { ...todayStats };
}
/**
* 获取日期范围内的汇总统计
* @param {string} startDate - 开始日期 YYYY-MM-DD
* @param {string} endDate - 结束日期 YYYY-MM-DD
* @returns {Promise<{success: number, failed: number, days: number}>}
*/
export async function getStatsRange(startDate, endDate) {
const result = { success: 0, failed: 0, days: 0 };
const start = new Date(startDate);
const end = new Date(endDate);
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
const filePath = getStatsFilePath(dateStr);
try {
const data = await fs.readFile(filePath, 'utf-8');
const stats = JSON.parse(data);
result.success += stats.success || 0;
result.failed += stats.failed || 0;
result.days++;
} catch {
// 文件不存在,跳过
}
}
return result;
}
/**
* 删除日期范围内的统计文件
* @param {string} startDate - 开始日期 YYYY-MM-DD
* @param {string} endDate - 结束日期 YYYY-MM-DD
* @returns {Promise<{deleted: number}>}
*/
export async function clearStatsRange(startDate, endDate) {
let deleted = 0;
const start = new Date(startDate);
const end = new Date(endDate);
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
const filePath = getStatsFilePath(dateStr);
try {
await fs.unlink(filePath);
deleted++;
// 如果删除的是今日文件,重置内存缓存
if (dateStr === todayDate) {
todayStats = { success: 0, failed: 0 };
}
} catch {
// 文件不存在,跳过
}
}
return { deleted };
}