/** * Session jsonl 解析器 — 从 jsonl 文件增量解析 toolCall/toolResult */ import { readFileSync, statSync } from "node:fs"; import type { ToolDailyStats } from "./types"; /** session jsonl 中一行的结构 */ interface SessionEntry { type: string; toolName?: string; isError?: boolean; [key: string]: unknown; } /** 解析结果 */ export interface ParseResult { /** 按工具名分组的增量统计 */ stats: Record; /** 新的 byte offset(处理到的位置) */ newOffset: number; } /** * 从 jsonl 文件增量解析 toolCall 和 toolResult 条目。 * 只处理 cursor 之后的新内容。 */ export function parseNewEntries( jsonlPath: string, cursor: number, ): ParseResult | null { let fileSize = 0; try { fileSize = statSync(jsonlPath).size; } catch { return null; // 文件不存在 } if (cursor >= fileSize) return null; // 没有新数据 const raw = readFileSync(jsonlPath, "utf-8"); const newContent = raw.substring(cursor); const lines = newContent.split("\n").filter((l) => l.trim()); const stats: Record = {}; for (const line of lines) { let entry: SessionEntry; try { entry = JSON.parse(line); } catch { continue; } if (entry.type === "toolCall" && entry.toolName) { if (!stats[entry.toolName]) { stats[entry.toolName] = { ai: 0, user: 0, errors: 0 }; } stats[entry.toolName].ai += 1; } else if (entry.type === "toolResult" && entry.toolName && entry.isError) { if (!stats[entry.toolName]) { stats[entry.toolName] = { ai: 0, user: 0, errors: 0 }; } stats[entry.toolName].errors += 1; } } return { stats, newOffset: Buffer.byteLength(raw, "utf-8"), }; }