feat: Session export as markdown in Diagnostic
- ⬇ Button per Session-Zeile — exportiert auch inaktive Sessions - Server parst JSONL, extrahiert User/Assistant-Nachrichten mit Timestamp - Metadata-Prefix wird entfernt, Markdown mit # Session-Header generiert - Browser-Download via Blob + download-Attribut Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1274,6 +1274,8 @@ wss.on("connection", (ws) => {
|
||||
handleListSessions(ws);
|
||||
} else if (msg.action === "read_session") {
|
||||
handleReadSession(ws, msg.sessionPath);
|
||||
} else if (msg.action === "export_session") {
|
||||
handleExportSession(ws, msg.sessionPath, msg.sessionKey);
|
||||
} else if (msg.action === "delete_session") {
|
||||
handleDeleteSession(ws, msg.sessionPath);
|
||||
} else if (msg.action === "set_active_session") {
|
||||
@@ -1656,6 +1658,68 @@ async function handleReadSession(clientWs, sessionPath) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExportSession(clientWs, sessionPath, sessionKey) {
|
||||
if (!sessionPath || sessionPath.includes("..") || !sessionPath.startsWith(SESSIONS_DIR)) {
|
||||
clientWs.send(JSON.stringify({ type: "session_export", ok: false, error: "Ungueltiger Pfad" }));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const safePath = sessionPath.replace(/'/g, "");
|
||||
const raw = await dockerExec("aria-core", `cat '${safePath}'`);
|
||||
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 = sessionKey || sessionPath.split("/").pop().replace(".jsonl", "");
|
||||
const markdown = [
|
||||
`# Session: ${title}`,
|
||||
``,
|
||||
`Exportiert: ${exportedAt} `,
|
||||
`Quelle: ${sessionPath}`,
|
||||
``,
|
||||
`---`,
|
||||
``,
|
||||
blocks.join("\n\n---\n\n"),
|
||||
``,
|
||||
].join("\n");
|
||||
|
||||
const safeKey = (sessionKey || "session").replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
const filename = `${exportedAt.slice(0, 10)}_${safeKey}.md`;
|
||||
clientWs.send(JSON.stringify({ type: "session_export", ok: true, filename, markdown }));
|
||||
log("info", "server", `Session exportiert: ${filename} (${blocks.length} Nachrichten)`);
|
||||
} catch (err) {
|
||||
log("error", "server", `Session-Export fehlgeschlagen: ${err.message}`);
|
||||
clientWs.send(JSON.stringify({ type: "session_export", ok: false, error: err.message }));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteSession(clientWs, sessionPath) {
|
||||
if (!sessionPath || sessionPath.includes("..") || !sessionPath.startsWith(SESSIONS_DIR)) {
|
||||
clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: "Ungueltiger Pfad" }));
|
||||
|
||||
Reference in New Issue
Block a user