Spaces:
Running
Running
File size: 4,045 Bytes
c6dedd5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | /**
* log-viewer.ts - 全链路日志 Web UI v4
*
* 静态文件分离版:HTML/CSS/JS 放在 public/ 目录,此文件只包含 API 路由和文件服务
*/
import type { Request, Response } from 'express';
import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { getAllLogs, getRequestSummaries, getStats, getRequestPayload, subscribeToLogs, subscribeToSummaries, clearAllLogs } from './logger.js';
// ==================== 静态文件路径 ====================
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const publicDir = join(__dirname, '..', 'public');
function readPublicFile(filename: string): string {
return readFileSync(join(publicDir, filename), 'utf-8');
}
// ==================== API 路由 ====================
export function apiGetLogs(req: Request, res: Response): void {
const { requestId, level, source, limit, since } = req.query;
res.json(getAllLogs({
requestId: requestId as string, level: level as any, source: source as any,
limit: limit ? parseInt(limit as string) : 200,
since: since ? parseInt(since as string) : undefined,
}));
}
export function apiGetRequests(req: Request, res: Response): void {
res.json(getRequestSummaries(req.query.limit ? parseInt(req.query.limit as string) : 50));
}
export function apiGetStats(_req: Request, res: Response): void {
res.json(getStats());
}
/** GET /api/payload/:requestId - 获取请求的完整参数和响应 */
export function apiGetPayload(req: Request, res: Response): void {
const payload = getRequestPayload(req.params.requestId as string);
if (!payload) { res.status(404).json({ error: 'Not found' }); return; }
res.json(payload);
}
/** POST /api/logs/clear - 清空所有日志 */
export function apiClearLogs(_req: Request, res: Response): void {
const result = clearAllLogs();
res.json({ success: true, ...result });
}
export function apiLogsStream(req: Request, res: Response): void {
res.writeHead(200, {
'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache',
'Connection': 'keep-alive', 'X-Accel-Buffering': 'no',
});
const sse = (event: string, data: string) => 'event: ' + event + '\ndata: ' + data + '\n\n';
try { res.write(sse('stats', JSON.stringify(getStats()))); } catch { /**/ }
const unsubLog = subscribeToLogs(e => { try { res.write(sse('log', JSON.stringify(e))); } catch { /**/ } });
const unsubSummary = subscribeToSummaries(s => {
try { res.write(sse('summary', JSON.stringify(s))); res.write(sse('stats', JSON.stringify(getStats()))); } catch { /**/ }
});
const hb = setInterval(() => { try { res.write(': heartbeat\n\n'); } catch { /**/ } }, 15000);
req.on('close', () => { unsubLog(); unsubSummary(); clearInterval(hb); });
}
// ==================== 页面服务 ====================
export function serveLogViewer(_req: Request, res: Response): void {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.send(readPublicFile('logs.html'));
}
export function serveLogViewerLogin(_req: Request, res: Response): void {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.send(readPublicFile('login.html'));
}
export function serveVueApp(_req: Request, res: Response): void {
res.sendFile(join(publicDir, 'vue', 'index.html'));
}
/** 静态文件路由 - CSS/JS */
export function servePublicFile(req: Request, res: Response): void {
const file = req.params[0]; // e.g. "logs.css" or "logs.js"
const ext = file.split('.').pop();
const mimeTypes: Record<string, string> = {
'css': 'text/css',
'js': 'application/javascript',
'html': 'text/html',
};
try {
const content = readPublicFile(file);
res.setHeader('Content-Type', (mimeTypes[ext || ''] || 'text/plain') + '; charset=utf-8');
res.send(content);
} catch {
res.status(404).send('Not found');
}
}
|