返回 DeepSeek-Reasonix
transcriptToolContent.ts
根目录 / desktop / frontend / src / lib / transcriptToolContent.ts
1 import type { Item } from "./useController";
2 import type { HistoryContentRef } from "./types";
3 import type { TranscriptBackend, SessionTranscript } from "./transcriptStoreTypes";
4 import { itemIdForToolCall, toolCallKey } from "./transcriptRecordProjection";
5 import { fileDiffFromWire } from "./tools";
6 type ToolContentOwner = { sessions: Map<string, SessionTranscript>; backend: TranscriptBackend; requestFullContent(tabId: string, entryId: string, field: string): Promise<string | undefined> };
7 export async function readTranscriptToolContent(owner: ToolContentOwner, tabId: string, item: Extract<Item, { kind: "tool" }>, value: Record<string, unknown>): Promise<string | undefined> {
8 const session = [...owner.sessions.values()].find(session => session.tabId === tabId &&
9 (item.sourceEntryId ? session.byId.has(item.sourceEntryId)
10 : [...session.contributions.values()].some(items => items.some(candidate => candidate.id === item.id))));
11 if (!session) return undefined;
12 const entryId = item.sourceEntryId
13 ?? [...session.contributions].find(([, items]) => items.some(candidate => candidate.id === item.id))?.[0];
14 const record = entryId && session.byId.get(entryId);
15 if (!record) return undefined;
16 let calls = record.message.toolCalls ?? [];
17 const matchesItem = (call: (typeof calls)[number], index: number) =>
18 session.toolCallDisplayIds.get(toolCallKey(record.entryId, index))
19 === item.id || itemIdForToolCall(call.id, `he:${record.entryId}:tc${index}`) === item.id;
20 let callIndex = calls.findIndex(matchesItem);
21 let call = calls[callIndex];
22 let resultId = session.matchTables.get(record.entryId)?.get(callIndex);
23 let result = resultId ? session.byId.get(resultId) : record.message.role === "tool" ? record : undefined;
24 if (record.refs.some(ref => ref.field === "canonicalMessage")) {
25 await owner.requestFullContent(tabId, record.entryId, "content");
26 calls = record.message.toolCalls ?? [];
27 callIndex = calls.findIndex(matchesItem);
28 call = calls[callIndex];
29 resultId = session.matchTables.get(record.entryId)?.get(callIndex);
30 result = resultId ? session.byId.get(resultId) : record.message.role === "tool" ? record : undefined;
31 }
32 if (result?.refs.some(ref => ref.field === "canonicalMessage")) {
33 await owner.requestFullContent(tabId, result.entryId, "content");
34 }
35 const generation = session.generation;
36 const refs = [
37 ...record.refs.filter(ref => call && ref.toolCallId === call.id && (ref.field === "toolArguments" || ref.field === "toolDiff")),
38 ...(result?.refs.filter(ref => ref.field === "content" || ref.field === "toolResultError") ?? []),
39 ];
40 if (refs.some(ref => ref.field === "toolArguments" || ref.field === "toolDiff") && !call?.id && calls.filter(call => !call.id).length > 1) throw new Error("Ambiguous legacy tool reference");
41 const full: Record<string, unknown> = { ...value, execution: result?.message.execution ?? value.execution };
42 if (call) full.args = call.arguments;
43 if (!result && call?.resultObservation?.contentRef) {
44 const contentRef = call.resultObservation.contentRef;
45 const ref: HistoryContentRef = { entryId: `m:${call.resultObservation.messageId}`, field: "canonicalMessage", size: contentRef.bytes, chunks: Math.ceil(contentRef.bytes / (1 << 20)), canonicalRef: contentRef, revision: session.revision, revKnown: true, digest: contentRef.digest };
46 let bytes = "";
47 for (let index = 0; index < ref.chunks; index++) {
48 const chunk = await owner.backend.HistoryContentForTab(tabId, ref, index);
49 if (owner.sessions.get(session.key) !== session || generation !== session.generation || chunk.stale) throw new Error("Tool reference expired; retry");
50 bytes += chunk.data ?? "";
51 }
52 if (bytes.length !== contentRef.bytes) throw new Error("Incomplete tool result content");
53 const decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(bytes, char => char.charCodeAt(0))));
54 full.output = decoded.raw_content || decoded.content || "";
55 full.execution = decoded.tool_execution ?? full.execution;
56 }
57
58 for (const ref of refs) {
59 let data = "";
60 for (let index = 0; index < Math.max(1, ref.chunks); index++) {
61 const chunk = await owner.backend.HistoryContentForTab(tabId, ref, index);
62 if (owner.sessions.get(session.key) !== session || generation !== session.generation || chunk.stale) throw new Error("Tool reference expired; retry");
63 data += chunk.data ?? "";
64 if (chunk.done) break;
65 }
66 if (new TextEncoder().encode(data).byteLength !== ref.size) throw new Error("Incomplete tool content");
67 if (ref.field === "toolArguments") full.args = data;
68 else if (ref.field === "content") full.output = data;
69 else if (ref.field === "toolResultError") full.error = data;
70 else full.diff = call ? fileDiffFromWire({ ...call, diff: data }) ?? data : data;
71 }
72 return JSON.stringify(full, null, 2);
73 }
74
74 lines TYPESCRIPT