116 lines
5.1 KiB
TypeScript
116 lines
5.1 KiB
TypeScript
|
|
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 },
|
|||
|
|
});
|