返回 DeepSeek-Reasonix
attachmentSubmit.ts
根目录 / desktop / frontend / src / lib / attachmentSubmit.ts
1 import type { AppBindings } from "./bridge";
2 import type { StructuredInvocationSubmit } from "./invocationDisplay";
3 import type { ComposerTarget } from "../generated/desktopContract.generated";
4 import type { WorkspaceReference } from "./composerDraftTypes";
5
6 export async function restoreExternalFolderReferences(app: AppBindings, target: ComposerTarget, refs: WorkspaceReference[]) {
7 const external = refs.filter(ref => ref.isDir && ref.path.startsWith("__reasonix_external_folder/"));
8 if (!external.length) return;
9 const token = await captureAttachmentTarget(app, target, ["AttachDroppedForTarget"]);
10 try {
11 for (const ref of external) {
12 if (!ref.displayPath) throw new Error("External folder source is unavailable; reattach the folder before sending");
13 const restored = await app.AttachDroppedForTarget!(token, ref.displayPath);
14 if (restored.kind !== "workspace" || !restored.isDir || restored.path !== ref.path) {
15 throw new Error("External folder changed; reattach the folder before sending");
16 }
17 }
18 } finally { await app.ReleaseAttachmentTarget?.(token); }
19 }
20
21 const pendingImageSubmissions = new Map<string, { fingerprint: string; id: string }>();
22
23 export function imageSubmissionIdentity(draft: string, fingerprint: string): string {
24 const previous = pendingImageSubmissions.get(draft);
25 if (previous?.fingerprint === fingerprint) return previous.id;
26 const id = `image-${crypto.randomUUID()}`;
27 pendingImageSubmissions.set(draft, { fingerprint, id });
28 return id;
29 }
30
31 export function settleImageSubmission(draft: string, id?: string): void {
32 if (pendingImageSubmissions.get(draft)?.id === id) pendingImageSubmissions.delete(draft);
33 }
34
35 export async function prepareImageSubmission(
36 app: AppBindings,
37 target: ComposerTarget,
38 draftKey: string,
39 fingerprint: string,
40 attachments: Array<{ draftId?: string; clientAttachmentId?: string; recoveryPath?: string; displayName?: string }>,
41 structured: StructuredInvocationSubmit | undefined,
42 display: string,
43 input: string,
44 ) {
45 const token = await captureImageTarget(app, target);
46 const submissionId = imageSubmissionIdentity(draftKey, fingerprint);
47 const restored = [];
48 try {
49 for (const [index, item] of attachments.entries()) {
50 let draftId = item.draftId;
51 if (!draftId && item.recoveryPath) {
52 if (!app.AttachmentDataURLForTarget || !app.StageImageForTarget) throw new Error("unsupported: attachments-v2");
53 const data = await app.AttachmentDataURLForTarget(token, item.recoveryPath);
54 const staged = await app.StageImageForTarget(token, `${submissionId}:restore:${index}`, item.displayName || "image", "", data);
55 draftId = staged.draftId;
56 }
57 if (!draftId) throw new Error("Image source is unavailable; reattach the image before sending");
58 restored.push({ clientAttachmentId: item.clientAttachmentId || `image-${index + 1}`, draftId });
59 }
60 } catch (error) {
61 await app.ReleaseAttachmentTarget?.(token).catch(() => {});
62 throw error;
63 }
64 return {
65 token,
66 submissionId,
67 structured: {
68 display: structured?.display ?? display,
69 input: structured?.input ?? input,
70 invocations: structured?.invocations ?? [],
71 attachmentTarget: token,
72 attachmentSubmissionId: submissionId,
73 attachments: restored,
74 } satisfies StructuredInvocationSubmit,
75 };
76 }
77
78 export function submitAttachmentTurn(app: AppBindings, submissionId: string, structured: StructuredInvocationSubmit, original: string, initialGoal?: { goal: string; toolApprovalMode?: string }) {
79 if (!app.StartTurnForAttachmentTarget || !structured.attachmentTarget) throw new Error("unsupported: attachments-v2");
80 return app.StartTurnForAttachmentTarget(structured.attachmentTarget, submissionId, {
81 input: structured.input, display: structured.display, original,
82 goal: initialGoal?.goal, toolApprovalMode: initialGoal?.toolApprovalMode,
83 invocations: structured.invocations, attachments: structured.attachments ?? [],
84 });
85 }
86
87 export function readFileAsDataURL(file: File): Promise<string> {
88 return new Promise((resolve, reject) => {
89 const reader = new FileReader();
90 reader.onload = () => resolve(String(reader.result));
91 reader.onerror = () => reject(reader.error);
92 reader.readAsDataURL(file);
93 });
94 }
95
96 export async function captureImageTarget(app: AppBindings, target: ComposerTarget): Promise<string> {
97 return captureAttachmentTarget(app, target, target.kind === "draft"
98 ? ["StageImageForTarget", "AttachmentDataURLForTarget"]
99 : ["StageImageForTarget", "ReadDraftImageForTarget"]);
100 }
101
102 export async function captureAttachmentTarget(
103 app: AppBindings,
104 target: ComposerTarget,
105 required: ReadonlyArray<keyof AppBindings>,
106 ): Promise<string> {
107 if (!app.CaptureAttachmentTarget || required.some((name) => typeof app[name] !== "function")) {
108 throw new Error("unsupported: attachments-v2");
109 }
110 const captured = await app.CaptureAttachmentTarget(target);
111 if (!captured.capabilities.includes("attachments-v2")) throw new Error("unsupported: attachments-v2");
112 return captured.token;
113 }
114
115 export async function stageImageFile(app: AppBindings, target: string, draftKey: string, file: File, persistentTarget?: ComposerTarget) {
116 const dataURL = await readFileAsDataURL(file);
117 const staged = await app.StageImageForTarget!(target, `${draftKey}:${file.name}:${file.lastModified}`, file.name, file.type, dataURL);
118 const path = staged.draftId ? `draft:${staged.draftId}` : staged.path;
119 if (!path) throw new Error("attachment staging returned no source");
120 const previewUrl = staged.draftId
121 ? await app.ReadDraftImageForTarget!(target, staged.draftId)
122 : await app.AttachmentDataURLForTarget!(target, path);
123 const recoveryPath = persistentTarget && staged.draftId ? await app.SavePastedImageForComposerTarget(persistentTarget, dataURL) : undefined;
124 return { path, previewUrl, displayName: file.name, draftId: staged.draftId || undefined, recoveryPath, file };
125 }
126
126 lines TYPESCRIPT