mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 增加计数功能
This commit is contained in:
@@ -41,11 +41,11 @@
|
||||
| [**Gemini Enterprise Business**](https://business.gemini.google/) | ✅ | ✅ | ✅ |
|
||||
| [**Nano Banana Free**](https://nanobananafree.ai/) | 🚫 | ✅ | 🚫 |
|
||||
| [**zAI**](https://zai.is/) | ✅ | ✅ | 🚫 |
|
||||
| [**Google Gemini**](https://gemini.google.com/) | ✅ | ✅ | ✅ |
|
||||
| [**Google Gemini**](https://gemini.google.com/) | ✅ | ✅💧 | ✅💧 |
|
||||
| [**ZenMux**](https://zenmux.ai/) | ✅ | ❌ | 🚫 |
|
||||
| [**ChatGPT**](https://chatgpt.com/) | ✅ | ✅ | 🚫 |
|
||||
| [**DeepSeek**](https://chat.deepseek.com/) | ✅ | 🚫 | 🚫 |
|
||||
| [**Sora**](https://sora.chatgpt.com/) | 🚫 | 🚫 | ✅ |
|
||||
| [**Sora**](https://sora.chatgpt.com/) | 🚫 | 🚫 | ✅💧 |
|
||||
| [**Google Flow**](https://labs.google/fx/zh/tools/flow) | 🚫 | ✅ | ❌ |
|
||||
| [**豆包**](https://www.doubao.com/) | ✅ | ✅ | ❌ |
|
||||
| 待续... | - | - | - |
|
||||
@@ -53,7 +53,7 @@
|
||||
> [!NOTE]
|
||||
> **获取完整模型列表**: 通过 `GET /v1/models` 接口查看当前配置下所有可用模型及其详细信息。
|
||||
>
|
||||
> ✅目前支持;❌目前不支持,但未来可能会支持;🚫网站不支持, 未来是否在支持看网站具体情况;
|
||||
> ✅目前支持;❌目前不支持,但未来可能会支持;🚫网站不支持, 未来是否在支持看网站具体情况;💧结果带水印且无法去除;
|
||||
|
||||
---
|
||||
|
||||
|
||||
+3
-3
@@ -44,11 +44,11 @@
|
||||
| [**Gemini Enterprise Business**](https://business.gemini.google/) | ✅ | ✅ | ✅ |
|
||||
| [**Nano Banana Free**](https://nanobananafree.ai/) | 🚫 | ✅ | 🚫 |
|
||||
| [**zAI**](https://zai.is/) | ✅ | ✅ | 🚫 |
|
||||
| [**Google Gemini**](https://gemini.google.com/) | ✅ | ✅ | ✅ |
|
||||
| [**Google Gemini**](https://gemini.google.com/) | ✅ | ✅💧 | ✅💧 |
|
||||
| [**ZenMux**](https://zenmux.ai/) | ✅ | ❌ | 🚫 |
|
||||
| [**ChatGPT**](https://chatgpt.com/) | ✅ | ✅ | 🚫 |
|
||||
| [**DeepSeek**](https://chat.deepseek.com/) | ✅ | 🚫 | 🚫 |
|
||||
| [**Sora**](https://sora.chatgpt.com/) | 🚫 | 🚫 | ✅ |
|
||||
| [**Sora**](https://sora.chatgpt.com/) | 🚫 | 🚫 | ✅💧 |
|
||||
| [**Google Flow**](https://labs.google/fx/zh/tools/flow) | 🚫 | ✅ | ❌ |
|
||||
| [**Doubao**](https://www.doubao.com/) | ✅ | ✅ | ❌ |
|
||||
| To be continued... | - | - | - |
|
||||
@@ -56,7 +56,7 @@
|
||||
> [!NOTE]
|
||||
> **Get full model list**: Use the `GET /v1/models` endpoint to view all available models and their details under the current configuration.
|
||||
>
|
||||
> ✅ Supported; ❌ Not currently supported, but may be in the future; 🚫 Website does not support, future support depends on the website's status;
|
||||
> ✅ Supported; ❌ Not currently supported, but may be in the future; 🚫 Website does not support, future support depends on the website's status; 💧 Results contain watermarks that cannot be removed.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{c as i,I as u}from"./index-CeQVA4cs.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 140H96c-17.7 0-32 14.3-32 32v496c0 17.7 14.3 32 32 32h380v112H304c-8.8 0-16 7.2-16 16v48c0 4.4 3.6 8 8 8h432c4.4 0 8-3.6 8-8v-48c0-8.8-7.2-16-16-16H548V700h380c17.7 0 32-14.3 32-32V172c0-17.7-14.3-32-32-32zm-40 488H136V212h752v416z"}}]},name:"desktop",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){s(r,a,e[a])})}return r}function s(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=c({},t,e.attrs);return i(u,c({},n,{icon:l}),null)};o.displayName="DesktopOutlined";o.inheritAttrs=!1;export{o as D};
|
||||
import{c as i,I as u}from"./index-BQYktfbJ.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 140H96c-17.7 0-32 14.3-32 32v496c0 17.7 14.3 32 32 32h380v112H304c-8.8 0-16 7.2-16 16v48c0 4.4 3.6 8 8 8h432c4.4 0 8-3.6 8-8v-48c0-8.8-7.2-16-16-16H548V700h380c17.7 0 32-14.3 32-32V172c0-17.7-14.3-32-32-32zm-40 488H136V212h752v416z"}}]},name:"desktop",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){s(r,a,e[a])})}return r}function s(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=c({},t,e.attrs);return i(u,c({},n,{icon:l}),null)};o.displayName="DesktopOutlined";o.inheritAttrs=!1;export{o as D};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
-1
@@ -1 +0,0 @@
|
||||
.log-container[data-v-6c3b8e99]{max-height:600px;overflow-y:auto;font-family:Consolas,Monaco,monospace;font-size:12px;background:#fafafa;border-radius:4px;padding:12px}.log-line[data-v-6c3b8e99]{padding:4px 0;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.log-line[data-v-6c3b8e99]:hover{background:#e6f7ff;white-space:normal;word-break:break-all}.log-time[data-v-6c3b8e99]{color:#8c8c8c}.log-module[data-v-6c3b8e99]{color:#1890ff;margin-right:8px}.log-message[data-v-6c3b8e99]{color:#333}.level-erro .log-message[data-v-6c3b8e99]{color:#ff4d4f}.level-warn .log-message[data-v-6c3b8e99]{color:#faad14}.level-dbug .log-message[data-v-6c3b8e99]{color:#722ed1}.toolbar[data-v-6c3b8e99]{margin-bottom:16px}.toolbar-row[data-v-6c3b8e99]{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px}.toolbar-row[data-v-6c3b8e99]:last-child{margin-bottom:0}@media(min-width:768px){.toolbar[data-v-6c3b8e99]{display:flex;justify-content:space-between;align-items:center;gap:12px}.toolbar-row[data-v-6c3b8e99]{margin-bottom:0}.toolbar-row[data-v-6c3b8e99]:last-child{flex:1;max-width:300px}}
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
import{c as l,I as P,_ as q,j as J,r as p,l as Q,o as X,a as Y,b as N,d as b,w as o,g as v,f,h as m,u as C,R as k,y as Z,D as K,i as L,e as S,t as _,m as ee,F as te,s as R,M as ae,z as le,A as ne,B as oe}from"./index-CeQVA4cs.js";var se={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M304 280h56c4.4 0 8-3.6 8-8 0-28.3 5.9-53.2 17.1-73.5 10.6-19.4 26-34.8 45.4-45.4C450.9 142 475.7 136 504 136h16c28.3 0 53.2 5.9 73.5 17.1 19.4 10.6 34.8 26 45.4 45.4C650 218.9 656 243.7 656 272c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-40-8.8-76.7-25.9-108.1a184.31 184.31 0 00-74-74C596.7 72.8 560 64 520 64h-16c-40 0-76.7 8.8-108.1 25.9a184.31 184.31 0 00-74 74C304.8 195.3 296 232 296 272c0 4.4 3.6 8 8 8z"}},{tag:"path",attrs:{d:"M940 512H792V412c76.8 0 139-62.2 139-139 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8a63 63 0 01-63 63H232a63 63 0 01-63-63c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 76.8 62.2 139 139 139v100H84c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h148v96c0 6.5.2 13 .7 19.3C164.1 728.6 116 796.7 116 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-44.2 23.9-82.9 59.6-103.7a273 273 0 0022.7 49c24.3 41.5 59 76.2 100.5 100.5S460.5 960 512 960s99.8-13.9 141.3-38.2a281.38 281.38 0 00123.2-149.5A120 120 0 01836 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-79.3-48.1-147.4-116.7-176.7.4-6.4.7-12.8.7-19.3v-96h148c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM716 680c0 36.8-9.7 72-27.8 102.9-17.7 30.3-43 55.6-73.3 73.3C584 874.3 548.8 884 512 884s-72-9.7-102.9-27.8c-30.3-17.7-55.6-43-73.3-73.3A202.75 202.75 0 01308 680V412h408v268z"}}]},name:"bug",theme:"outlined"};function D(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},s=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(s=s.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),s.forEach(function(c){re(n,c,a[c])})}return n}function re(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var j=function(t,a){var s=D({},t,a.attrs);return l(P,D({},s,{icon:se}),null)};j.displayName="BugOutlined";j.inheritAttrs=!1;var ce={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 720a48 48 0 1096 0 48 48 0 10-96 0zm16-304v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8zm475.7 440l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zm-783.5-27.9L512 239.9l339.8 588.2H172.2z"}}]},name:"warning",theme:"outlined"};function E(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},s=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(s=s.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),s.forEach(function(c){ie(n,c,a[c])})}return n}function ie(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var I=function(t,a){var s=E({},t,a.attrs);return l(P,E({},s,{icon:ce}),null)};I.displayName="WarningOutlined";I.inheritAttrs=!1;const ue={class:"toolbar"},de={class:"toolbar-row"},fe={class:"toolbar-row"},ve={style:{"margin-bottom":"12px",color:"#8c8c8c","font-size":"12px"}},me={key:0,style:{color:"#1890ff","margin-left":"8px"}},pe={class:"log-container"},ge={class:"log-time"},_e={class:"log-module"},he={class:"log-message"},Oe={__name:"logs",setup(n){const t=J(),a=p([]),s=p(!1),c=p(0),h=p(!1),g=p(null),y=p(""),w=p("all"),U={INFO:{color:"#1890ff",icon:oe},WARN:{color:"#faad14",icon:I},ERRO:{color:"#ff4d4f",icon:ne},DBUG:{color:"#722ed1",icon:j}},z=async()=>{s.value=!0;try{const r=await fetch("/admin/logs?lines=500",{headers:t.getHeaders()});if(r.ok){const e=await r.json();a.value=A(e.logs||[]),c.value=e.total||0}}catch{R.error("获取日志失败")}finally{s.value=!1}},A=r=>r.map((e,i)=>{const d=e.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(\w+)\] \[([^\]]+)\] (.*)$/);return d?{id:i,time:d[1],level:d[2],module:d[3],message:d[4],raw:e}:{id:i,raw:e,level:"INFO",time:"",module:"",message:e}}),x=Q(()=>a.value.filter(e=>{if(w.value!=="all"&&e.level!==w.value)return!1;if(y.value){const i=y.value.toLowerCase();return e.raw.toLowerCase().includes(i)}return!0}).reverse()),F=()=>{ae.confirm({title:"确认清除日志",content:"此操作将删除所有系统日志文件,是否继续?",okText:"确认清除",okType:"danger",cancelText:"取消",async onOk(){try{(await fetch("/admin/logs",{method:"DELETE",headers:t.getHeaders()})).ok?(R.success("日志已清除"),a.value=[],c.value=0):R.error("清除失败")}catch{R.error("请求失败")}}})},M=()=>{const r=a.value.map(O=>O.raw).join(`
|
||||
`),e=new Blob([r],{type:"text/plain"}),i=URL.createObjectURL(e),d=document.createElement("a");d.href=i,d.download=`system-${new Date().toISOString().split("T")[0]}.log`,d.click(),URL.revokeObjectURL(i)},T=r=>{h.value=r,r?(z(),g.value=setInterval(z,5e3)):g.value&&(clearInterval(g.value),g.value=null)};return X(()=>{z()}),Y(()=>{g.value&&clearInterval(g.value)}),(r,e)=>{const i=f("a-select-option"),d=f("a-select"),O=f("a-button"),B=f("a-tooltip"),V=f("a-space"),W=f("a-input-search"),$=f("a-tag"),H=f("a-empty"),G=f("a-card");return b(),N(G,{title:"系统日志",bordered:!1},{default:o(()=>[v("div",ue,[v("div",de,[l(d,{value:w.value,"onUpdate:value":e[0]||(e[0]=u=>w.value=u),style:{width:"90px"},size:"small"},{default:o(()=>[l(i,{value:"all"},{default:o(()=>[...e[3]||(e[3]=[m("全部",-1)])]),_:1}),l(i,{value:"INFO"},{default:o(()=>[...e[4]||(e[4]=[m("INFO",-1)])]),_:1}),l(i,{value:"WARN"},{default:o(()=>[...e[5]||(e[5]=[m("WARN",-1)])]),_:1}),l(i,{value:"ERRO"},{default:o(()=>[...e[6]||(e[6]=[m("ERROR",-1)])]),_:1}),l(i,{value:"DBUG"},{default:o(()=>[...e[7]||(e[7]=[m("DEBUG",-1)])]),_:1})]),_:1},8,["value"]),l(V,{size:4},{default:o(()=>[l(B,{title:h.value?"关闭自动刷新":"开启自动刷新"},{default:o(()=>[l(O,{size:"small",type:h.value?"primary":"default",onClick:e[1]||(e[1]=u=>T(!h.value))},{icon:o(()=>[l(C(k))]),_:1},8,["type"])]),_:1},8,["title"]),l(B,{title:"导出日志"},{default:o(()=>[l(O,{size:"small",onClick:M},{icon:o(()=>[l(C(Z))]),_:1})]),_:1}),l(B,{title:"清除日志"},{default:o(()=>[l(O,{size:"small",danger:"",onClick:F},{icon:o(()=>[l(C(K))]),_:1})]),_:1})]),_:1})]),v("div",fe,[l(W,{value:y.value,"onUpdate:value":e[2]||(e[2]=u=>y.value=u),placeholder:"搜索日志",size:"small","enter-button":"","allow-clear":"",style:{width:"100%"}},null,8,["value"])])]),v("div",ve,[m(" 共 "+_(c.value)+" 条日志,当前显示 "+_(x.value.length)+" 条 ",1),h.value?(b(),L("span",me,[l(C(k),{spin:!0}),e[8]||(e[8]=m(" 自动刷新中 ",-1))])):S("",!0)]),v("div",pe,[(b(!0),L(te,null,ee(x.value,u=>(b(),L("div",{key:u.id,class:le(["log-line","level-"+u.level.toLowerCase()])},[v("span",ge,_(u.time),1),l($,{color:U[u.level]?.color||"#8c8c8c",size:"small",style:{margin:"0 8px"}},{default:o(()=>[m(_(u.level),1)]),_:2},1032,["color"]),v("span",_e,"["+_(u.module)+"]",1),v("span",he,_(u.message),1)],2))),128)),x.value.length===0?(b(),N(H,{key:0,description:"暂无日志"})):S("",!0)])]),_:1})}}},ye=q(Oe,[["__scopeId","data-v-6c3b8e99"]]);export{ye as default};
|
||||
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
||||
.log-container[data-v-f89c259b]{max-height:600px;overflow-y:auto;font-family:Consolas,Monaco,monospace;font-size:12px;background:#fafafa;border-radius:4px;padding:12px}.log-line[data-v-f89c259b]{padding:4px 0;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.log-line[data-v-f89c259b]:hover{background:#e6f7ff;white-space:normal;word-break:break-all}.log-time[data-v-f89c259b]{color:#8c8c8c}.log-module[data-v-f89c259b]{color:#1890ff;margin-right:8px}.log-message[data-v-f89c259b]{color:#333}.level-erro .log-message[data-v-f89c259b]{color:#ff4d4f}.level-warn .log-message[data-v-f89c259b]{color:#faad14}.level-dbug .log-message[data-v-f89c259b]{color:#722ed1}.toolbar[data-v-f89c259b]{margin-bottom:16px}.toolbar-row[data-v-f89c259b]{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px}.toolbar-row[data-v-f89c259b]:last-child{margin-bottom:0}@media(min-width:768px){.toolbar[data-v-f89c259b]{display:flex;justify-content:space-between;align-items:center;gap:12px}.toolbar-row[data-v-f89c259b]{margin-bottom:0}.toolbar-row[data-v-f89c259b]:last-child{flex:1;max-width:300px}}.stats-content[data-v-f89c259b]{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.stats-numbers[data-v-f89c259b]{display:flex;align-items:center;gap:20px}.stat-item[data-v-f89c259b]{display:flex;align-items:center;gap:6px;padding:4px 12px;background:#fafafa;border-radius:6px;transition:all .2s}.stat-item[data-v-f89c259b]:hover{background:#f0f0f0}.stat-item.success[data-v-f89c259b]{color:#52c41a}.stat-item.error[data-v-f89c259b]{color:#ff4d4f}.stat-item.neutral[data-v-f89c259b]{color:#8c8c8c}.stat-value[data-v-f89c259b]{font-size:18px;font-weight:600;font-family:SF Mono,Monaco,monospace}.stat-label[data-v-f89c259b]{font-size:12px;color:#8c8c8c}@media(max-width:576px){.stats-content[data-v-f89c259b]{flex-direction:column;align-items:flex-start}.stats-content .ant-divider[data-v-f89c259b]{display:none}.stats-numbers[data-v-f89c259b]{margin-top:8px}}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
import{_ as b,k as w,l as k,o as c,b as C,d as S,w as n,c as o,g as e,f as a,h as i}from"./index-BQYktfbJ.js";const B={style:{"margin-bottom":"8px"}},T={style:{"margin-bottom":"8px"}},z={style:{"margin-bottom":"8px"}},U={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},M={style:{"margin-bottom":"8px"}},j={style:{"margin-bottom":"8px"}},q={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},L={__name:"server",setup(N){const d=w(),s=k({port:5173,authToken:"",keepaliveMode:"comment",queueBuffer:2,imageLimit:5});c(async()=>{await d.fetchServerConfig(),Object.assign(s,d.serverConfig)});const m=async()=>{await d.saveServerConfig(s)};return(V,t)=>{const p=a("a-input-number"),r=a("a-col"),y=a("a-input-password"),u=a("a-select-option"),g=a("a-select"),f=a("a-row"),v=a("a-button"),x=a("a-card"),_=a("a-layout");return S(),C(_,{style:{background:"transparent"}},{default:n(()=>[o(x,{title:"服务器设置",bordered:!1,style:{width:"100%"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",B,[t[5]||(t[5]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"监听端口",-1)),t[6]||(t[6]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 设置服务器监听的端口号,默认为 5173 ",-1)),o(p,{value:s.port,"onUpdate:value":t[0]||(t[0]=l=>s.port=l),min:1,max:65535,placeholder:"请输入端口号",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",T,[t[7]||(t[7]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"鉴权 Token",-1)),t[8]||(t[8]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 用于 API 请求鉴权的密钥,留空则不启用鉴权 ",-1)),o(y,{value:s.authToken,"onUpdate:value":t[1]||(t[1]=l=>s.authToken=l),placeholder:"请输入 Token",type:"password"},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",z,[t[11]||(t[11]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"心跳包类型",-1)),t[12]||(t[12]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 选择 SSE 流式响应的心跳包格式 ",-1)),o(g,{value:s.keepaliveMode,"onUpdate:value":t[2]||(t[2]=l=>s.keepaliveMode=l),style:{width:"100%"},placeholder:"请选择心跳包类型"},{default:n(()=>[o(u,{value:"comment"},{default:n(()=>[...t[9]||(t[9]=[i("Comment - 注释格式",-1)])]),_:1}),o(u,{value:"content"},{default:n(()=>[...t[10]||(t[10]=[i("Content - 内容格式",-1)])]),_:1})]),_:1},8,["value"])])]),_:1})]),_:1}),e("div",U,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[13]||(t[13]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1}),o(x,{title:"队列设置",bordered:!1,style:{width:"100%","margin-top":"10px"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",M,[t[14]||(t[14]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"队列缓冲区大小",-1)),t[15]||(t[15]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 非流式请求的额外排队数(设为 0 则不限制非流式请求数量)"),e("br"),i(" 实际队列上限 = Workers数量 + 缓冲区大小 ")],-1)),o(p,{value:s.queueBuffer,"onUpdate:value":t[3]||(t[3]=l=>s.queueBuffer=l),min:0,max:100,placeholder:"默认为 2",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",j,[t[16]||(t[16]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"图片数量上限",-1)),t[17]||(t[17]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 单次请求最多支持的图片附件数量"),e("br"),i(" 网页最多支持10个附件,超出会被丢弃 ")],-1)),o(p,{value:s.imageLimit,"onUpdate:value":t[4]||(t[4]=l=>s.imageLimit=l),min:1,max:10,placeholder:"默认为 5",style:{width:"100%"}},null,8,["value"])])]),_:1})]),_:1}),e("div",q,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[18]||(t[18]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1})]),_:1})}}},A=b(L,[["__scopeId","data-v-bd32923f"]]);export{A as default};
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
import{_ as b,j as w,k,o as c,b as C,d as S,w as n,c as o,g as e,f as a,h as i}from"./index-CeQVA4cs.js";const B={style:{"margin-bottom":"8px"}},T={style:{"margin-bottom":"8px"}},z={style:{"margin-bottom":"8px"}},U={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},j={style:{"margin-bottom":"8px"}},M={style:{"margin-bottom":"8px"}},q={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},L={__name:"server",setup(N){const d=w(),s=k({port:5173,authToken:"",keepaliveMode:"comment",queueBuffer:2,imageLimit:5});c(async()=>{await d.fetchServerConfig(),Object.assign(s,d.serverConfig)});const m=async()=>{await d.saveServerConfig(s)};return(V,t)=>{const p=a("a-input-number"),r=a("a-col"),y=a("a-input-password"),u=a("a-select-option"),g=a("a-select"),f=a("a-row"),v=a("a-button"),x=a("a-card"),_=a("a-layout");return S(),C(_,{style:{background:"transparent"}},{default:n(()=>[o(x,{title:"服务器设置",bordered:!1,style:{width:"100%"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",B,[t[5]||(t[5]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"监听端口",-1)),t[6]||(t[6]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 设置服务器监听的端口号,默认为 5173 ",-1)),o(p,{value:s.port,"onUpdate:value":t[0]||(t[0]=l=>s.port=l),min:1,max:65535,placeholder:"请输入端口号",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",T,[t[7]||(t[7]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"鉴权 Token",-1)),t[8]||(t[8]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 用于 API 请求鉴权的密钥,留空则不启用鉴权 ",-1)),o(y,{value:s.authToken,"onUpdate:value":t[1]||(t[1]=l=>s.authToken=l),placeholder:"请输入 Token",type:"password"},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",z,[t[11]||(t[11]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"心跳包类型",-1)),t[12]||(t[12]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 选择 SSE 流式响应的心跳包格式 ",-1)),o(g,{value:s.keepaliveMode,"onUpdate:value":t[2]||(t[2]=l=>s.keepaliveMode=l),style:{width:"100%"},placeholder:"请选择心跳包类型"},{default:n(()=>[o(u,{value:"comment"},{default:n(()=>[...t[9]||(t[9]=[i("Comment - 注释格式",-1)])]),_:1}),o(u,{value:"content"},{default:n(()=>[...t[10]||(t[10]=[i("Content - 内容格式",-1)])]),_:1})]),_:1},8,["value"])])]),_:1})]),_:1}),e("div",U,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[13]||(t[13]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1}),o(x,{title:"队列设置",bordered:!1,style:{width:"100%","margin-top":"10px"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",j,[t[14]||(t[14]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"队列缓冲区大小",-1)),t[15]||(t[15]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 非流式请求的额外排队数(设为 0 则不限制非流式请求数量)"),e("br"),i(" 实际队列上限 = Workers数量 + 缓冲区大小 ")],-1)),o(p,{value:s.queueBuffer,"onUpdate:value":t[3]||(t[3]=l=>s.queueBuffer=l),min:0,max:100,placeholder:"默认为 2",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",M,[t[16]||(t[16]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"图片数量上限",-1)),t[17]||(t[17]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 单次请求最多支持的图片附件数量"),e("br"),i(" 网页最多支持10个附件,超出会被丢弃 ")],-1)),o(p,{value:s.imageLimit,"onUpdate:value":t[4]||(t[4]=l=>s.imageLimit=l),min:1,max:10,placeholder:"默认为 5",style:{width:"100%"}},null,8,["value"])])]),_:1})]),_:1}),e("div",q,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[18]||(t[18]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1})]),_:1})}}},A=b(L,[["__scopeId","data-v-bd32923f"]]);export{A as default};
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
import{x as i,j as a,s as r}from"./index-CeQVA4cs.js";const u=i("system",{state:()=>({status:"",version:"1.0.0",systemVersion:"",uptime:0,cpuUsage:0,memoryUsage:{total:0,used:0,free:0},safeMode:{enabled:!1,reason:null},stats:{totalRequests:0,successRate:0,activeWorkers:0,totalWorkers:0,avgResponseTime:0}}),actions:{async fetchStatus(){const t=a();try{const e=await fetch("/admin/status",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.$patch(s)}}catch(e){console.error("Failed to fetch system status:",e)}},async fetchStats(){const t=a();try{const e=await fetch("/admin/stats",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.stats=s}}catch(e){console.error("Failed to fetch stats:",e)}},async restartService(t={}){const e=a(),{loginMode:s,workerName:n}=t;try{const o=await(await fetch("/admin/restart",{method:"POST",headers:{...e.getHeaders(),"Content-Type":"application/json"},body:JSON.stringify({loginMode:s,workerName:n})})).json();return o.success?(r.success(o.message||"服务重启中..."),!0):(r.error("重启失败"),!1)}catch{return r.error("重启请求失败"),!1}},async stopService(){const t=a();try{const s=await(await fetch("/admin/stop",{method:"POST",headers:t.getHeaders()})).json();return s.success?(r.success(s.message||"服务停止中..."),!0):(r.error("停止失败"),!1)}catch{return r.error("停止请求失败"),!1}}}});export{u};
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
import{y as i,k as a,v as r}from"./index-BQYktfbJ.js";const u=i("system",{state:()=>({status:"",version:"1.0.0",systemVersion:"",uptime:0,cpuUsage:0,memoryUsage:{total:0,used:0,free:0},safeMode:{enabled:!1,reason:null},stats:{totalRequests:0,successRate:0,activeWorkers:0,totalWorkers:0,avgResponseTime:0,success:0,failed:0}}),actions:{async fetchStatus(){const t=a();try{const e=await fetch("/admin/status",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.$patch(s)}}catch(e){console.error("Failed to fetch system status:",e)}},async fetchStats(){const t=a();try{const e=await fetch("/admin/stats",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.stats=s}}catch(e){console.error("Failed to fetch stats:",e)}},async restartService(t={}){const e=a(),{loginMode:s,workerName:n}=t;try{const o=await(await fetch("/admin/restart",{method:"POST",headers:{...e.getHeaders(),"Content-Type":"application/json"},body:JSON.stringify({loginMode:s,workerName:n})})).json();return o.success?(r.success(o.message||"服务重启中..."),!0):(r.error("重启失败"),!1)}catch{return r.error("重启请求失败"),!1}},async stopService(){const t=a();try{const s=await(await fetch("/admin/stop",{method:"POST",headers:t.getHeaders()})).json();return s.success?(r.success(s.message||"服务停止中..."),!0):(r.error("停止失败"),!1)}catch{return r.error("停止请求失败"),!1}}}});export{u};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -6,7 +6,7 @@
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WebAI2API</title>
|
||||
<script type="module" crossorigin src="/assets/index-CeQVA4cs.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BQYktfbJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BVr8U7Bl.css">
|
||||
</head>
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
LineChartOutlined,
|
||||
SyncOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
CheckCircleOutlined
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
const systemStore = useSystemStore();
|
||||
@@ -205,6 +206,22 @@ onUnmounted(() => {
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16" style="margin-top: 16px">
|
||||
<a-col :span="12">
|
||||
<a-statistic title="今日成功" :value="systemStore.stats.success || 0">
|
||||
<template #prefix>
|
||||
<CheckCircleOutlined style="color: #52c41a" />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-statistic title="今日失败" :value="systemStore.stats.failed || 0">
|
||||
<template #prefix>
|
||||
<CloseCircleOutlined style="color: #ff4d4f" />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
WarningOutlined,
|
||||
CloseCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
BugOutlined
|
||||
BugOutlined,
|
||||
CheckCircleOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
|
||||
@@ -23,6 +24,11 @@ const refreshInterval = ref(null);
|
||||
const searchText = ref('');
|
||||
const levelFilter = ref('all');
|
||||
|
||||
// 统计查询相关
|
||||
const dateRange = ref([]);
|
||||
const rangeStats = ref({ success: 0, failed: 0, days: 0 });
|
||||
const statsLoading = ref(false);
|
||||
|
||||
// 日志级别配置
|
||||
const levelConfig = {
|
||||
'INFO': { color: '#1890ff', icon: InfoCircleOutlined },
|
||||
@@ -141,6 +147,61 @@ const toggleAutoRefresh = (newState) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 查询日期范围统计
|
||||
const fetchRangeStats = async () => {
|
||||
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||
rangeStats.value = { success: 0, failed: 0, days: 0 };
|
||||
return;
|
||||
}
|
||||
|
||||
statsLoading.value = true;
|
||||
try {
|
||||
const [start, end] = dateRange.value;
|
||||
const res = await fetch(
|
||||
`/admin/stats/range?start=${start.format('YYYY-MM-DD')}&end=${end.format('YYYY-MM-DD')}`,
|
||||
{ headers: settingsStore.getHeaders() }
|
||||
);
|
||||
if (res.ok) {
|
||||
rangeStats.value = await res.json();
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('获取统计失败');
|
||||
} finally {
|
||||
statsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 删除选定范围的统计数据
|
||||
const clearRangeStats = () => {
|
||||
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||
message.warning('请先选择日期范围');
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除 ${dateRange.value[0].format('YYYY-MM-DD')} 至 ${dateRange.value[1].format('YYYY-MM-DD')} 的统计数据吗?`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
try {
|
||||
const [start, end] = dateRange.value;
|
||||
const res = await fetch(
|
||||
`/admin/stats/range?start=${start.format('YYYY-MM-DD')}&end=${end.format('YYYY-MM-DD')}`,
|
||||
{ method: 'DELETE', headers: settingsStore.getHeaders() }
|
||||
);
|
||||
if (res.ok) {
|
||||
message.success('统计数据已删除');
|
||||
rangeStats.value = { success: 0, failed: 0, days: 0 };
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchLogs();
|
||||
});
|
||||
@@ -153,7 +214,47 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-card title="系统日志" :bordered="false">
|
||||
<!-- 统计查询面板 -->
|
||||
<a-card title="请求统计" :bordered="false">
|
||||
<template #extra>
|
||||
<a-button type="link" danger size="small" @click="clearRangeStats"
|
||||
:disabled="!dateRange || dateRange.length !== 2">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
删除统计
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div class="stats-content">
|
||||
<a-range-picker v-model:value="dateRange" :format="'YYYY-MM-DD'" :placeholder="['开始日期', '结束日期']"
|
||||
size="small" style="width: 240px" @change="fetchRangeStats" />
|
||||
|
||||
<a-divider type="vertical" style="height: 32px; margin: 0 16px" />
|
||||
|
||||
<a-spin :spinning="statsLoading" size="small">
|
||||
<div class="stats-numbers">
|
||||
<div class="stat-item success">
|
||||
<CheckCircleOutlined />
|
||||
<span class="stat-value">{{ rangeStats.success }}</span>
|
||||
<span class="stat-label">成功</span>
|
||||
</div>
|
||||
<div class="stat-item error">
|
||||
<CloseCircleOutlined />
|
||||
<span class="stat-value">{{ rangeStats.failed }}</span>
|
||||
<span class="stat-label">失败</span>
|
||||
</div>
|
||||
<div class="stat-item neutral">
|
||||
<span class="stat-value">{{ rangeStats.days }}</span>
|
||||
<span class="stat-label">天</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 系统日志 -->
|
||||
<a-card title="系统日志" :bordered="false" style="margin-top: 24px">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<!-- 第一行:级别筛选和操作按钮 -->
|
||||
@@ -305,4 +406,72 @@ onUnmounted(() => {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 统计内容样式 */
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stats-numbers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.stat-item:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.stat-item.success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.stat-item.error {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.stat-item.neutral {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
/* 响应式:小屏幕统计面板垂直布局 */
|
||||
@media (max-width: 576px) {
|
||||
.stats-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.stats-content .ant-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stats-numbers {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,7 +28,9 @@ export const useSystemStore = defineStore('system', {
|
||||
successRate: 0,
|
||||
activeWorkers: 0,
|
||||
totalWorkers: 0,
|
||||
avgResponseTime: 0
|
||||
avgResponseTime: 0,
|
||||
success: 0,
|
||||
failed: 0
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user