返回 DeepSeek-Reasonix
nativeTranscriptHistory.ts
根目录 / desktop / frontend / src / lib / nativeTranscriptHistory.ts
1 import { app } from "./bridge";
2 import { historyMessageIdentity } from "./historyItemIds";
3 import type { TranscriptSnapshot } from "./transcriptProtocol";
4 import type { HistoryEntry, HistoryWindowPageView, HistoryWindowRequestView } from "./types";
5 import type { HistoryOutlinePage, HistoryOutlineRequest } from "../generated/desktopContract.generated";
6
7 type Binding = { snapshotId: string; total: number; sequence: number; remote: boolean };
8 const bindings = new Map<string, Binding>();
9 type Cursor = { snapshotId: string; boundary: number; direction: "older" | "newer" };
10 const cursor = (snapshotId: string, boundary: number, direction: Cursor["direction"]) => JSON.stringify({ snapshotId, boundary, direction });
11 const staleWindow = (): HistoryWindowPageView => ({ entries: [], status: "stale_cursor", olderCursor: "", newerCursor: "", hasOlder: false, hasNewer: false,
12 totalTurns: 0, startTurn: 0, endTurn: 0, revision: 0, revisionKnown: false, digest: "" });
13
14 export function bindNativeTranscriptHistory(tabId: string, snapshot: TranscriptSnapshot, remote: boolean): () => void {
15 const binding = { snapshotId: snapshot.snapshotId, total: snapshot.totalRecords, sequence: snapshot.coveredThroughSeq, remote };
16 bindings.set(tabId, binding);
17 return () => { if (bindings.get(tabId) === binding) bindings.delete(tabId); };
18 }
19
20 export function nativeSnapshotWindow(snapshot: TranscriptSnapshot): HistoryWindowPageView {
21 const entries: HistoryEntry[] = snapshot.records.map(record => {
22 const messageId = historyMessageIdentity(record.message, record.id);
23 const entryId = messageId ? `m:${messageId}` : record.message.recordId ?? record.id;
24 return { entryId, order: record.order, turn: record.message.historyTurn ?? 0, message: { ...record.message, messageId, recordId: entryId },
25 refs: record.refs.map(ref => ({ entryId, field: ref.path[0], size: ref.bytes, chunks: 1,
26 revision: snapshot.coveredThroughSeq, revKnown: true, digest: snapshot.snapshotId, transcriptRef: ref })) };
27 });
28 const end = entries.length ? entries[entries.length - 1].order + 1 : snapshot.before;
29 const turns = entries.map(entry => entry.turn).filter(turn => turn > 0);
30 return { entries, status: snapshot.stale ? "stale_cursor" : snapshot.notFound ? "not_found" : "ready",
31 olderCursor: snapshot.hasOlder ? cursor(snapshot.snapshotId, snapshot.before, "older") : "",
32 newerCursor: end < snapshot.totalRecords ? cursor(snapshot.snapshotId, end, "newer") : "",
33 hasOlder: snapshot.hasOlder, hasNewer: end < snapshot.totalRecords, totalTurns: snapshot.totalTurns,
34 startTurn: turns.length ? Math.min(...turns) : 0, endTurn: turns.length ? Math.max(...turns) : 0,
35 revision: snapshot.coveredThroughSeq, revisionKnown: true, digest: snapshot.snapshotId };
36 }
37
38 // Select the reader from the negotiated binding, never from a failed request.
39 export async function readNativeTranscriptWindow(tabId: string, req: HistoryWindowRequestView): Promise<HistoryWindowPageView | undefined> {
40 const binding = bindings.get(tabId);
41 if (!binding) return undefined;
42 const read = binding.remote ? app.RemoteTranscriptPageForTab : app.TranscriptPageForTab;
43 if (!read) throw new Error("Native transcript paging is unavailable");
44 const limit = Math.max(1, Math.min(req.limit ?? 32, 100));
45 if (!req.cursor && (!req.anchor || req.anchor === "newest")) {
46 const snapshot = await read(tabId, { records: limit, ...(req.generation ? { snapshotId: req.generation } : {}) });
47 if (bindings.get(tabId) !== binding) return staleWindow();
48 if (!req.generation && !snapshot.stale) {
49 binding.snapshotId = snapshot.snapshotId; binding.total = snapshot.totalRecords; binding.sequence = snapshot.coveredThroughSeq;
50 }
51 return nativeSnapshotWindow(snapshot);
52 }
53 let snapshotId = req.generation || binding.snapshotId, boundary = binding.total;
54 let direction: Cursor["direction"] = req.direction === "newer" ? "newer" : "older";
55 if (req.cursor) {
56 let parsed: Cursor;
57 try { parsed = JSON.parse(req.cursor); } catch { return staleWindow(); }
58 if (!parsed || typeof parsed.snapshotId !== "string" || !Number.isSafeInteger(parsed.boundary) || parsed.boundary < 0 || !["older", "newer"].includes(parsed.direction)) return staleWindow();
59 snapshotId = parsed.snapshotId; boundary = parsed.boundary; direction = parsed.direction;
60 if (req.generation && req.generation !== snapshotId) return staleWindow();
61 } else if (req.anchor === "turn") {
62 const outline = binding.remote ? app.RemoteTranscriptOutlineForTab : app.TranscriptOutlineForTab;
63 if (!outline) throw new Error("Native transcript outline is unavailable");
64 const page = await outline(tabId, { snapshotId, offset: Math.max(0, (req.turn ?? 1) - 1), entries: 1 });
65 if (bindings.get(tabId) !== binding) return staleWindow();
66 if (page.stale || !page.entries.length) {
67 const view = nativeSnapshotWindow(await read(tabId, { snapshotId, before: 0, records: 1 }));
68 if (bindings.get(tabId) !== binding) return staleWindow();
69 return { ...view, status: page.stale ? "stale_cursor" : "not_found" };
70 }
71 boundary = page.entries[0].order; direction = "newer";
72 } else if (req.anchor === "message") {
73 const snapshot = await read(tabId, { snapshotId, messageId: req.messageId, records: limit });
74 return { ...nativeSnapshotWindow(snapshot), ...(bindings.get(tabId) !== binding ? { status: "stale_cursor" as const } : {}) };
75 }
76 let snapshot = await read(tabId, { snapshotId, before: direction === "newer" ? boundary + limit : boundary, records: limit });
77 if (direction === "newer" && !snapshot.stale && snapshot.records.length && snapshot.records[0].order > boundary) {
78 // Byte limits may shorten a backwards page. Never skip the forward prefix.
79 snapshot = await read(tabId, { snapshotId, before: boundary + 1, records: 1 });
80 }
81 if (direction === "newer" && !snapshot.stale) {
82 // The server clamps before to the cut's end. Its backwards window can
83 // then overlap the preceding page; keep only the requested suffix.
84 const records = snapshot.records.filter(record => record.order >= boundary);
85 snapshot = { ...snapshot, records, before: records[0]?.order ?? Math.min(boundary, snapshot.totalRecords),
86 hasOlder: (records[0]?.order ?? boundary) > 0 };
87 }
88 if (bindings.get(tabId) !== binding) return { ...nativeSnapshotWindow(snapshot), status: "stale_cursor" };
89 return nativeSnapshotWindow(snapshot);
90 }
91
92 export async function readNativeTranscriptOutline(tabId: string, req: HistoryOutlineRequest): Promise<HistoryOutlinePage | undefined> {
93 const binding = bindings.get(tabId);
94 if (!binding) return undefined;
95 const read = binding.remote ? app.RemoteTranscriptOutlineForTab : app.TranscriptOutlineForTab;
96 if (!read) throw new Error("Native transcript outline is unavailable");
97 if (!req.generation) await readNativeTranscriptWindow(tabId, { anchor: "newest", limit: 1 });
98 const snapshotId = req.generation || binding.snapshotId;
99 const page = await read(tabId, { snapshotId, offset: Math.max(0, (req.startTurn ?? 1) - 1), entries: Math.min(req.limit ?? 128, 1000) });
100 return { status: page.stale || bindings.get(tabId) !== binding ? "stale_cursor" : "ready", generation: snapshotId,
101 snapshotSequence: req.snapshotSequence ?? binding.sequence, coverageSequence: binding.sequence, totalTurns: page.total,
102 entries: page.entries.map(entry => ({ messageId: entry.messageId ?? entry.id, turn: entry.turn, position: entry.order, prompt: entry.prompt, answer: entry.answer })),
103 nextTurn: page.nextOffset + 1, done: page.done };
104 }
105
105 lines TYPESCRIPT