Spaces:
Running
Running
github-actions[bot]
sync: upstream b70f787 Merge pull request #84 from huangzt/feature/vue-logs-ui
c6dedd5 | /** | |
| * 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'); | |
| } | |
| } | |