



























@@ -3,7 +3,7 @@ import path from "node:path";
33import { fileURLToPath } from "node:url";
44import {
55migrateSessionEntries,
6-parseSessionEntries,
6+type FileEntry as PiSessionFileEntry,
77type SessionEntry as PiSessionEntry,
88type SessionHeader,
99} from "@earendil-works/pi-coding-agent";
@@ -28,6 +28,17 @@ interface SessionData {
2828tools?: Array<{ name: string; description?: string; parameters?: unknown }>;
2929}
303031+type SessionExportJsonlWarning = {
32+code: "invalid-session-json" | "invalid-session-row";
33+row: number;
34+};
35+36+type SessionExportWarningSummary = {
37+code: SessionExportJsonlWarning["code"];
38+count: number;
39+rows: number[];
40+};
41+3142async function loadTemplate(fileName: string): Promise<string> {
3243return await fsp.readFile(path.join(EXPORT_HTML_DIR, fileName), "utf-8");
3344}
@@ -144,20 +155,104 @@ async function writeNewDefaultExportFile(filePath: string, html: string): Promis
144155}
145156throw new Error(`Could not find an unused export filename near ${filePath}`);
146157}
158+159+function isRecord(value: unknown): value is Record<string, unknown> {
160+return Boolean(value) && typeof value === "object" && !Array.isArray(value);
161+}
162+163+function isSessionFileEntry(value: unknown): value is PiSessionFileEntry {
164+if (!isRecord(value) || typeof value.type !== "string") {
165+return false;
166+}
167+if (value.type !== "message") {
168+return true;
169+}
170+const message = value.message;
171+return isRecord(message) && typeof message.role === "string";
172+}
173+174+function parseSessionEntriesWithWarnings(content: string): {
175+entries: PiSessionFileEntry[];
176+warnings: SessionExportJsonlWarning[];
177+} {
178+const entries: PiSessionFileEntry[] = [];
179+const warnings: SessionExportJsonlWarning[] = [];
180+const rows = content.split(/\r?\n/u);
181+for (const [index, rawLine] of rows.entries()) {
182+const line = rawLine.trim();
183+if (!line) {
184+continue;
185+}
186+try {
187+const parsed = JSON.parse(line) as unknown;
188+if (!isSessionFileEntry(parsed)) {
189+warnings.push({ code: "invalid-session-row", row: index + 1 });
190+continue;
191+}
192+entries.push(parsed);
193+} catch {
194+warnings.push({ code: "invalid-session-json", row: index + 1 });
195+}
196+}
197+return { entries, warnings };
198+}
199+200+function summarizeSessionExportWarnings(
201+warnings: SessionExportJsonlWarning[],
202+): SessionExportWarningSummary[] {
203+const summaries = new Map<SessionExportJsonlWarning["code"], SessionExportWarningSummary>();
204+for (const warning of warnings) {
205+const summary = summaries.get(warning.code);
206+if (summary) {
207+summary.count += 1;
208+if (summary.rows.length < 20) {
209+summary.rows.push(warning.row);
210+}
211+continue;
212+}
213+summaries.set(warning.code, {
214+code: warning.code,
215+count: 1,
216+rows: [warning.row],
217+});
218+}
219+return [...summaries.values()];
220+}
221+222+function formatSkippedRows(count: number): string {
223+return `${count.toLocaleString()} malformed transcript ${count === 1 ? "row" : "rows"}`;
224+}
225+226+function formatSessionExportWarning(summary: SessionExportWarningSummary): string {
227+const rows = summary.rows.length > 0 ? ` rows ${summary.rows.join(", ")}` : "";
228+const verb = summary.count === 1 ? "was" : "were";
229+switch (summary.code) {
230+case "invalid-session-json":
231+return `⚠️ Skipped ${formatSkippedRows(summary.count)} that ${verb} not valid JSON.${rows}`;
232+case "invalid-session-row":
233+return summary.count === 1
234+ ? `⚠️ Skipped ${formatSkippedRows(summary.count)} that was not a session entry.${rows}`
235+ : `⚠️ Skipped ${formatSkippedRows(summary.count)} that were not session entries.${rows}`;
236+}
237+const unreachable: never = summary.code;
238+return unreachable;
239+}
240+147241async function readSessionDataFromTranscript(sessionFile: string): Promise<{
148242header: SessionHeader | null;
149243entries: PiSessionEntry[];
150244leafId: string | null;
245+warnings: SessionExportWarningSummary[];
151246}> {
152247const raw = await fsp.readFile(sessionFile, "utf-8");
153-const fileEntries = parseSessionEntries(raw);
248+const { entries: fileEntries, warnings } = parseSessionEntriesWithWarnings(raw);
154249migrateSessionEntries(fileEntries);
155250const header =
156251fileEntries.find((entry): entry is SessionHeader => entry.type === "session") ?? null;
157252const entries = fileEntries.filter((entry): entry is PiSessionEntry => entry.type !== "session");
158253const lastEntry = entries.at(-1);
159254const leafId = typeof lastEntry?.id === "string" ? lastEntry.id : null;
160-return { header, entries, leafId };
255+return { header, entries, leafId, warnings: summarizeSessionExportWarnings(warnings) };
161256}
162257163258export async function buildExportSessionReply(params: HandleCommandsParams): Promise<ReplyPayload> {
@@ -179,7 +274,7 @@ export async function buildExportSessionReply(params: HandleCommandsParams): Pro
179274}
180275181276// 2. Load session entries
182-const { entries, header, leafId } = await readSessionDataFromTranscript(sessionFile);
277+const { entries, header, leafId, warnings } = await readSessionDataFromTranscript(sessionFile);
183278184279// 3. Build full system prompt
185280const { systemPrompt, tools } = await resolveCommandsSystemPromptBundle({
@@ -234,6 +329,7 @@ export async function buildExportSessionReply(params: HandleCommandsParams): Pro
234329"",
235330`📄 File: ${displayPath}`,
236331`📊 Entries: ${entries.length}`,
332+ ...warnings.map(formatSessionExportWarning),
237333`🧠 System prompt: ${systemPrompt.length.toLocaleString()} chars`,
238334`🔧 Tools: ${tools.length}`,
239335].join("\n"),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。