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