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');
    }
}