feat: Archivierte Session-Versionen (OpenClaw .reset.* Files) in Diagnostic
OpenClaw resettet Sessions beim ersten chat.send nach Container-Restart (wenn abortedLastRun / systemSent Inkonsistenz erkannt wurde) und benennt die alte .jsonl in .jsonl.reset.<timestamp>.Z um. Der Inhalt war also gar nicht verloren, nur unsichtbar. Diagnostic: - handleListSessions scannt jetzt auch *.jsonl.reset.* Files - Reset-Files bekommen archived:true + resetAt-Timestamp - Neue UI-Sektion "Archivierte Versionen" (collapsible <details>) mit Export-Button, zeigt aufklappbar alle gesicherten alten Sessions - Aktivieren ist fuer Archive deaktiviert (zerstoert aktive Session) - Loeschen + Export stehen zur Verfuegung tools/export-jsonl-to-md.js: - Standalone Node-Script zum Konvertieren beliebiger .jsonl (auch reset-Files) - Nutzbar via stdin, exakt gleiche Export-Logik wie Diagnostic - Fuer Rettungsaktionen direkt auf der VM Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Exportiert ein OpenClaw Session-JSONL (auch .reset.*) als Markdown.
|
||||
*
|
||||
* Nutzung:
|
||||
* node export-jsonl-to-md.js <input.jsonl> [output.md]
|
||||
*
|
||||
* Oder direkt aus dem aria-core Container:
|
||||
* docker exec aria-core cat /home/node/.openclaw/agents/main/sessions/<ID>.jsonl.reset.<TS> \
|
||||
* | node export-jsonl-to-md.js - > output.md
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const inputArg = process.argv[2];
|
||||
const outputArg = process.argv[3];
|
||||
|
||||
if (!inputArg) {
|
||||
console.error("Usage: export-jsonl-to-md.js <input.jsonl|-> [output.md]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const raw = inputArg === "-" ? fs.readFileSync(0, "utf-8") : fs.readFileSync(inputArg, "utf-8");
|
||||
const lines = raw.split("\n").filter(l => l.trim());
|
||||
|
||||
const blocks = [];
|
||||
for (const line of lines) {
|
||||
let obj;
|
||||
try { obj = JSON.parse(line); } catch { continue; }
|
||||
if (obj.type !== "message" || !obj.message) continue;
|
||||
const role = obj.message.role;
|
||||
if (role !== "user" && role !== "assistant") continue;
|
||||
|
||||
let text = "";
|
||||
const content = obj.message.content;
|
||||
if (typeof content === "string") text = content;
|
||||
else if (Array.isArray(content)) text = content.filter(c => c.type === "text").map(c => c.text || "").join("\n");
|
||||
if (!text) continue;
|
||||
|
||||
if (role === "user") {
|
||||
text = text.replace(/^Sender \(untrusted metadata\):[\s\S]*?```[\s\S]*?```\s*\n*/m, "").trim();
|
||||
text = text.replace(/^\[.*?\]\s*/, "").trim();
|
||||
} else {
|
||||
text = text.replace(/^\[\[reply_to_\w+\]\]\s*/g, "").trim();
|
||||
}
|
||||
if (!text) continue;
|
||||
|
||||
const ts = obj.message.timestamp || obj.timestamp || 0;
|
||||
const when = ts ? new Date(ts).toISOString().replace("T", " ").slice(0, 19) : "";
|
||||
const heading = role === "user" ? "## 🧑 User" : "## 🤖 ARIA";
|
||||
blocks.push(`${heading}${when ? ` — ${when}` : ""}\n\n${text}`);
|
||||
}
|
||||
|
||||
const exportedAt = new Date().toISOString().replace("T", " ").slice(0, 19);
|
||||
const title = inputArg === "-" ? "Session" : inputArg.split("/").pop().replace(/\.jsonl.*/, "");
|
||||
const md = [
|
||||
`# Session: ${title}`,
|
||||
``,
|
||||
`Exportiert: ${exportedAt} `,
|
||||
`Quelle: ${inputArg === "-" ? "stdin" : inputArg}`,
|
||||
`Nachrichten: ${blocks.length}`,
|
||||
``,
|
||||
`---`,
|
||||
``,
|
||||
blocks.join("\n\n---\n\n"),
|
||||
``,
|
||||
].join("\n");
|
||||
|
||||
if (outputArg) {
|
||||
fs.writeFileSync(outputArg, md);
|
||||
console.error(`OK: ${blocks.length} Nachrichten → ${outputArg}`);
|
||||
} else {
|
||||
process.stdout.write(md);
|
||||
}
|
||||
Reference in New Issue
Block a user