feat: 完善 WebUI 功能

This commit is contained in:
foxhui
2025-12-20 18:35:53 +08:00
Unverified
parent c8c7aec0e1
commit 10c96e420f
32 changed files with 1395 additions and 507 deletions
+18
View File
@@ -136,6 +136,13 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
// 9. 提取图片 URL
const img = extractImage(content);
if (img) {
// 检查是否配置了返回 URL
const returnUrl = config?.backend?.adapter?.lmarena?.returnUrl || false;
if (returnUrl) {
logger.info('适配器', '已获取结果,返回 URL', meta);
return { image: img };
}
logger.info('适配器', '已获取结果,正在下载图片...', meta);
const result = await downloadImage(img, context);
if (result.image) {
@@ -170,6 +177,17 @@ export const manifest = {
id: 'lmarena',
displayName: 'LMArena',
// 配置项模式
configSchema: [
{
key: 'returnUrl',
label: '返回图片 URL',
type: 'boolean',
default: false,
note: '开启后直接返回图片 URL (但其他不支持的适配器仍然会返回 Base64)'
}
],
// 入口 URL
getTargetUrl(config, workerConfig) {
return TARGET_URL;
+24 -2
View File
@@ -59,7 +59,7 @@ async function readBody(req) {
* @returns {Function} Admin 路由处理函数
*/
export function createAdminRouter(context) {
const { config, queueManager, tempDir } = context;
const { config, queueManager, tempDir, getSafeMode } = context;
/**
* Admin 路由处理函数
@@ -76,7 +76,8 @@ export function createAdminRouter(context) {
// GET /admin/status - 系统状态
if (method === 'GET' && pathname === '/status') {
const status = getSystemStatus();
sendJson(res, 200, status);
const safeMode = getSafeMode?.() || { enabled: false, reason: null };
sendJson(res, 200, { ...status, safeMode });
return;
}
@@ -175,6 +176,27 @@ export function createAdminRouter(context) {
return;
}
// GET /admin/logs - 读取系统日志
if (method === 'GET' && pathname === '/logs') {
const url = new URL(req.url, `http://${req.headers.host}`);
const lines = parseInt(url.searchParams.get('lines') || '200', 10);
const result = logger.readLogs(lines);
sendJson(res, 200, result);
return;
}
// DELETE /admin/logs - 清除系统日志
if (method === 'DELETE' && pathname === '/logs') {
const success = logger.clearLogs();
if (success) {
logger.info('管理器', '系统日志已清除');
sendJson(res, 200, { success: true, message: '日志已清除' });
} else {
sendJson(res, 500, { success: false, message: '日志清除失败' });
}
return;
}
// GET /admin/data-folders - 列出数据文件夹
if (method === 'GET' && pathname === '/data-folders') {
const workers = config.backend?.pool?.workers || [];
+14 -2
View File
@@ -36,14 +36,14 @@ const WEBUI_DIR = path.join(process.cwd(), 'webui', 'dist');
* @returns {Function} 请求处理函数
*/
export function createGlobalRouter(context) {
const { authToken, config, queueManager, tempDir, loginMode } = context;
const { authToken, config, queueManager, tempDir, loginMode, getSafeMode } = context;
// 创建鉴权中间件
const checkAuth = createAuthMiddleware(authToken);
// 创建子路由处理器
const handleOpenAIRequest = loginMode ? null : createOpenAIRouter(context);
const handleAdminRequest = createAdminRouter({ config, queueManager, tempDir });
const handleAdminRequest = createAdminRouter({ config, queueManager, tempDir, getSafeMode });
/**
* 主路由处理函数
@@ -100,6 +100,18 @@ export function createGlobalRouter(context) {
// OpenAI API (/v1)
if (pathname.startsWith('/v1')) {
// 安全模式下禁用 OpenAI API
const safeMode = getSafeMode?.();
if (safeMode?.enabled) {
res.writeHead(503, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
message: `服务运行在安全模式,OpenAI API 不可用。原因: ${safeMode.reason}`,
type: 'service_unavailable'
}
}));
return;
}
// 登录模式下禁用 OpenAI API
if (!handleOpenAIRequest) {
res.writeHead(503, { 'Content-Type': 'application/json' });
+18 -4
View File
@@ -101,6 +101,16 @@ const queueManager = createQueueManager(
*/
const isLoginMode = process.argv.some(arg => arg.startsWith('-login'));
/**
* 安全模式状态
* 当 Pool 初始化失败时进入安全模式,此时:
* - HTTP 服务器正常启动
* - Admin API 和 WebUI 可用
* - OpenAI API 返回 503
*/
let safeMode = false;
let safeModeReason = null;
const handleRequest = createGlobalRouter({
authToken: AUTH_TOKEN,
backendName,
@@ -112,7 +122,8 @@ const handleRequest = createGlobalRouter({
imageLimit: IMAGE_LIMIT,
queueManager,
config,
loginMode: isLoginMode
loginMode: isLoginMode,
getSafeMode: () => ({ enabled: safeMode, reason: safeModeReason })
});
// ==================== 启动服务器 ====================
@@ -128,12 +139,15 @@ async function startServer() {
logger.info('服务器', '完成后可直接关闭浏览器窗口或按 Ctrl+C 退出');
}
// 预先启动 Pool
// 预先启动工作池(失败时进入安全模式)
try {
await queueManager.initializePool();
} catch (err) {
logger.error('服务器', 'Pool 初始化失败', { error: err.message });
process.exit(1);
logger.error('服务器', '工作池初始化失败', { error: err.message });
logger.warn('服务器', '进入安全模式:WebUI 和 Admin API 可用,OpenAI API 不可用');
logger.warn('服务器', '请通过 配置文件或者 WebUI 修改正确的配置后重启服务');
safeMode = true;
safeModeReason = err.message;
}
// 创建并启动 HTTP 服务器
+113 -1
View File
@@ -4,9 +4,12 @@
*
* - 环境变量:LOG_LEVEL=debug|info|warn|error
* - 输出格式:YYYY-MM-DD HH:mm:ss.SSS [LEVEL] [模块] 消息 | k=v ...
* - 日志文件:data/temp/system.log(超过 5MB 自动轮转)
*/
import process from 'process';
import fs from 'fs';
import path from 'path';
const LEVELS = ['debug', 'info', 'warn', 'error'];
@@ -19,6 +22,49 @@ const COLORS = {
white: '\x1b[37m'
};
// 日志文件配置
const LOG_DIR = path.join(process.cwd(), 'data', 'temp');
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
// 确保日志目录存在
function ensureLogDir() {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
}
// 日志轮转:超过 5MB 时重命名为 .old
function rotateLogIfNeeded() {
try {
if (fs.existsSync(LOG_FILE)) {
const stats = fs.statSync(LOG_FILE);
if (stats.size >= MAX_LOG_SIZE) {
// 删除旧的 .old 文件
if (fs.existsSync(LOG_FILE_OLD)) {
fs.unlinkSync(LOG_FILE_OLD);
}
// 重命名当前日志
fs.renameSync(LOG_FILE, LOG_FILE_OLD);
}
}
} catch (e) {
// 忽略轮转错误
}
}
// 写入日志文件
function writeToFile(line) {
try {
ensureLogDir();
rotateLogIfNeeded();
fs.appendFileSync(LOG_FILE, line + '\n', 'utf8');
} catch (e) {
// 忽略写入错误
}
}
// 根据日志级别获取颜色
function getColor(level) {
switch (level.toLowerCase()) {
@@ -94,6 +140,7 @@ export function log(level, mod, msg, meta = {}) {
const color = getColor(level);
const coloredLine = `${color}${line}${COLORS.reset}`;
// 输出到控制台
if (level === 'error') {
console.error(coloredLine);
} else if (level === 'warn') {
@@ -101,6 +148,66 @@ export function log(level, mod, msg, meta = {}) {
} else {
console.log(coloredLine);
}
// 写入日志文件(不带颜色)
writeToFile(line);
}
/**
* 获取日志文件路径
*/
export function getLogPath() {
return LOG_FILE;
}
/**
* 获取旧日志文件路径
*/
export function getOldLogPath() {
return LOG_FILE_OLD;
}
/**
* 清除日志文件
*/
export function clearLogs() {
try {
if (fs.existsSync(LOG_FILE)) {
fs.unlinkSync(LOG_FILE);
}
if (fs.existsSync(LOG_FILE_OLD)) {
fs.unlinkSync(LOG_FILE_OLD);
}
return true;
} catch (e) {
return false;
}
}
/**
* 读取日志文件(返回最后 N 行)
* @param {number} lines - 读取行数
* @returns {{logs: string[], total: number, file: string}}
*/
export function readLogs(lines = 200) {
const result = { logs: [], total: 0, file: LOG_FILE };
try {
if (!fs.existsSync(LOG_FILE)) {
return result;
}
const content = fs.readFileSync(LOG_FILE, 'utf8');
const allLines = content.split('\n').filter(line => line.trim());
result.total = allLines.length;
// 返回最后 N 行
result.logs = allLines.slice(-lines);
} catch (e) {
// 忽略读取错误
}
return result;
}
export const logger = {
@@ -108,5 +215,10 @@ export const logger = {
info: (mod, msg, meta) => log('info', mod, msg, meta),
warn: (mod, msg, meta) => log('warn', mod, msg, meta),
error: (mod, msg, meta) => log('error', mod, msg, meta),
setLevel: setLogLevel
setLevel: setLogLevel,
getLogPath,
getOldLogPath,
clearLogs,
readLogs
};