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