mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 增加计数功能
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user