xykj-plus/claude-ecc/log-analyzer/vite.config.ts

116 lines
5.1 KiB
TypeScript
Raw Permalink Normal View History

import { defineConfig, type Plugin } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'node:path';
import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import type { IncomingMessage, ServerResponse } from 'node:http';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// 日志目录:向上一级到项目根,再进 .claude/logs
const LOGS_DIR = path.resolve(__dirname, '../.claude/logs');
function sendJson(res: ServerResponse, data: unknown, status = 200) {
res.statusCode = status;
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.end(JSON.stringify(data));
}
function logApiPlugin(): Plugin {
return {
name: 'log-api',
configureServer(server) {
server.middlewares.use(
async (req: IncomingMessage, res: ServerResponse, next: () => void) => {
const url = req.url ?? '';
if (req.method !== 'GET' || !url.startsWith('/api/')) return next();
try {
// GET /api/folders —— 列出日志目录下的所有子文件夹
if (url === '/api/folders') {
const urlObj = new URL(`http://localhost${url}`);
const baseDir = urlObj.searchParams.get('base') ? decodeURIComponent(urlObj.searchParams.get('base')!) : LOGS_DIR;
let entries: string[] = [];
try { entries = await fs.readdir(baseDir); } catch { /* logs dir 可能不存在 */ }
const folders = await Promise.all(
entries
.filter(async (e) => {
try {
const stat = await fs.stat(path.join(baseDir, e));
return stat.isDirectory();
} catch { return false; }
})
.map(async (folder) => {
const folderPath = path.join(baseDir, folder);
const files = (await fs.readdir(folderPath)).filter(f => f.startsWith('trace-') && f.endsWith('.jsonl'));
return { name: folder, sessionCount: files.length };
})
);
folders.sort((a, b) => a.name.localeCompare(b.name));
return sendJson(res, folders);
}
// GET /api/sessions —— 列出所有 trace-*.jsonl支持 base 和 folder 查询参数)
if (url.startsWith('/api/sessions') && !url.match(/^\/api\/sessions\//)) {
const urlObj = new URL(`http://localhost${url}`);
const baseDir = urlObj.searchParams.get('base') ? decodeURIComponent(urlObj.searchParams.get('base')!) : LOGS_DIR;
const folder = urlObj.searchParams.get('folder');
const targetDir = folder ? path.join(baseDir, folder) : baseDir;
let files: string[] = [];
try { files = await fs.readdir(targetDir); } catch { /* dir 可能不存在 */ }
const sessions = await Promise.all(
files
.filter(f => f.startsWith('trace-') && f.endsWith('.jsonl'))
.map(async (f) => {
const sid = f.slice('trace-'.length, -'.jsonl'.length);
const filePath = path.join(targetDir, f);
const [stat, content] = await Promise.all([
fs.stat(filePath),
fs.readFile(filePath, 'utf8'),
]);
const lines = content.split('\n').filter(l => l.trim().length > 0);
let firstTs: string | null = null;
let lastTs: string | null = null;
try { firstTs = (JSON.parse(lines[0]) as { ts: string }).ts; } catch { /* skip */ }
try { lastTs = (JSON.parse(lines[lines.length - 1]) as { ts: string }).ts; } catch { /* skip */ }
return { sid, filename: f, folder: folder ?? '', mtime: stat.mtime, eventCount: lines.length, firstTs, lastTs };
})
);
sessions.sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime());
return sendJson(res, sessions);
}
// GET /api/sessions/:sid —— 读取单个 trace 文件
const sessionMatch = url.match(/^\/api\/sessions\/([^/?]+)$/);
if (sessionMatch) {
const sid = sessionMatch[1];
const filePath = path.join(LOGS_DIR, `trace-${sid}.jsonl`);
let content: string;
try {
content = await fs.readFile(filePath, 'utf8');
} catch {
return sendJson(res, { error: 'Session not found' }, 404);
}
const records = content
.split('\n')
.filter(l => l.trim().length > 0)
.flatMap(l => { try { return [JSON.parse(l)]; } catch { return []; } });
return sendJson(res, records);
}
next();
} catch (e) {
sendJson(res, { error: String(e) }, 500);
}
}
);
},
};
}
export default defineConfig({
plugins: [react(), logApiPlugin()],
server: { port: 5177 },
});