返回 DeepSeek-Reasonix
sessionExportOperation.ts
根目录 / desktop / frontend / src / lib / sessionExportOperation.ts
1 import { app } from "./bridge";
2 import { onSessionExportProgress } from "./sessionExportBridge";
3 import type { SessionExportHandle, SessionSelector } from "../generated/desktopContract.generated";
4 import { t } from "./i18n";
5 import { getTranscriptStore } from "./transcriptStore";
6 import { sessionObservationDiagnostics } from "./sessionObservationDiagnostics";
7 import { sessionPipelineDiagnostics } from "./sessionDiagnostics";
8
9 import { cancelled, publish, remove } from "./sessionExportProgress";
10 function assertActive(id: string) { if (cancelled.has(id)) throw new DOMException("Export cancelled", "AbortError"); }
11
12 export async function* exportChunks(handle: SessionExportHandle): AsyncGenerator<string> {
13 let offset = 0;
14 const decoder = new TextDecoder("utf-8", { fatal: true });
15 for (;;) {
16 assertActive(handle.exportId);
17 const chunk = await app.ReadSessionExportChunk(handle.exportId, offset);
18 const bytes = Uint8Array.from(atob(chunk.data), value => value.charCodeAt(0));
19 yield decoder.decode(bytes, { stream: !chunk.done });
20 if (chunk.done) return;
21 if (chunk.nextOffset <= offset) throw new Error("Export cursor did not advance");
22 offset = chunk.nextOffset;
23 }
24 }
25 export async function* exportBlocks(handle: SessionExportHandle): AsyncGenerator<{ kind: string; text: string; label?: string }> {
26 let pending = "";
27 for await (const chunk of exportChunks(handle)) {
28 pending += chunk;
29 for (;;) {
30 const end = pending.indexOf("\n"); if (end < 0) break;
31 const line = pending.slice(0, end); pending = pending.slice(end + 1);
32 if (line) yield JSON.parse(line);
33 }
34 }
35 if (pending.trim()) throw new Error("Incomplete export block stream");
36 }
37
38 export async function runSessionExport(input: {
39 selector: SessionSelector; tabId: string; format: string; title: string; remote: boolean;
40 residentItems: number; runningStream: boolean; unresolvedTools: number;
41 }): Promise<{ text?: string; files: number; cancelled: boolean }> {
42 let handle: SessionExportHandle | undefined;
43 const off = onSessionExportProgress(value => { if (handle?.exportId === value.exportId && !cancelled.has(value.exportId)) publish(value); });
44 try {
45 const route = input.selector.ref ? `session-id:${input.selector.ref.sessionId}` : input.selector.sessionPath ?? "";
46 const observation = JSON.stringify({ binding: getTranscriptStore().exportObservation(input.tabId, route), lifecycleDiagnostics: sessionObservationDiagnostics(route, input.tabId), readDiagnostics: sessionPipelineDiagnostics(), capturedAt: new Date().toISOString(), tabId: input.tabId, remote: input.remote, residentItems: input.residentItems, runningStream: input.runningStream, unresolvedTools: input.unresolvedTools });
47 handle = await app.BeginSessionExportForTarget(input.selector, input.tabId, input.format, input.title, observation);
48 if (!handle.exportId) return { files: 0, cancelled: true };
49 publish({ exportId: handle.exportId, title: input.title, phase: "preparing", records: 0, pages: 0 });
50 let text: string | undefined;
51 if (input.format === "clipboard") {
52 const parts: string[] = [];
53 for await (const part of exportChunks(handle)) parts.push(part);
54 try { text = parts.join(""); } catch { throw new Error(t("topicBar.exportClipboardTooLarge")); }
55 } else if (input.format === "pdf" || input.format === "image") {
56 const { renderSessionExportPages, blobToBase64 } = await import("./sessionExport");
57 let index = 0;
58 for await (const page of renderSessionExportPages(exportBlocks(handle), input.format)) {
59 assertActive(handle.exportId);
60 // Bound each RPC payload independently of the encoded page size.
61 for (let offset = 0; offset < page.blob.size; offset += 1 << 20) {
62 const end = Math.min(offset + (1 << 20), page.blob.size);
63 await app.AppendSessionExportPage(handle.exportId, { index, offset, data: await blobToBase64(page.blob.slice(offset, end)), done: end === page.blob.size, width: page.width, height: page.height });
64 }
65 index++;
66 }
67 }
68 assertActive(handle.exportId);
69 const result = await app.FinishSessionExport(handle.exportId);
70 return { text, files: (result.paths ?? []).length, cancelled: false };
71 } catch (error) {
72 if (handle?.exportId) await app.CancelSessionExport(handle.exportId).catch(() => {});
73 if (handle && cancelled.has(handle.exportId)) return { files: 0, cancelled: true };
74 throw error;
75 } finally {
76 off(); if (handle?.exportId) { remove(handle.exportId); cancelled.delete(handle.exportId); }
77 }
78 }
79
79 lines TYPESCRIPT