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