返回 DeepSeek-Reasonix
inboxGuidanceSubmit.ts
根目录 / desktop / frontend / src / lib / inboxGuidanceSubmit.ts
1 import type { AppBindings } from "./bridge";
2 import type { StructuredInvocationSubmit } from "./invocationDisplay";
3 import { resolveActiveTurnId } from "./inboxSubmit";
4 import { confirmFollowup, followupNotSubmitted, followupSessionKey, pendingFollowups, type PendingFollowup } from "./pendingFollowup";
5
6 type InboxEnqueueBindings = Pick<AppBindings, "EnqueueInboxFollowup" | "EnqueueInboxFollowupWithInvocations" | "EnqueueInboxSteer" | "EnqueueInboxSteerForTurn" | "EnqueueForAttachmentTarget">;
7
8 // Keep request construction with the lazy submission owner while callers
9 // capture the session target before crossing the module-loading boundary.
10 export function enqueueGuidanceForTarget(binding: AppBindings, target: PendingFollowup["target"], tabId: string, text: string, turnId?: string) {
11 return enqueueTrackedGuidance(binding, {
12 tabId, target, key: `guidance-${crypto.randomUUID()}`, display: text, submit: text, draft: text,
13 }, turnId);
14 }
15
16 // Non-Composer callers share its unresolved-request owner so a lost receipt
17 // cannot cause a second POST when the user retries guidance.
18 export async function enqueueTrackedGuidance(binding: AppBindings, request: PendingFollowup, turnId?: string) {
19 const { target } = request;
20 const pendingKey = followupSessionKey(target?.sessionPath, target?.hostId, target?.workspace);
21 const unresolved = pendingKey ? pendingFollowups.get(pendingKey) : undefined;
22 if (unresolved) {
23 const receipt = await confirmFollowup(binding, unresolved);
24 pendingFollowups.clear(pendingKey, unresolved);
25 if (unresolved.submit === request.submit) return receipt;
26 // A different instruction still needs its own receipt; acknowledging the
27 // previous request must never clear a newer draft as if it was delivered.
28 }
29 if (pendingKey) pendingFollowups.set(pendingKey, request);
30 try {
31 const receipt = await enqueueComposerGuidance(binding, request, false, turnId);
32 if (receipt?.error) throw new Error(receipt.error);
33 if (!receipt?.itemId) throw new Error("Follow-up receipt unconfirmed");
34 if (pendingKey) pendingFollowups.clear(pendingKey, request);
35 return receipt;
36 } catch (error) {
37 if (pendingKey && followupNotSubmitted(error)) pendingFollowups.clear(pendingKey, request);
38 throw error;
39 }
40 }
41
42 export async function enqueueComposerGuidance(binding: AppBindings, request: PendingFollowup, queueOnly: boolean, turnId?: string) {
43 const { target, structured, tabId, display, submit, key } = request;
44 // Structured invocations and image submissions require their own turn.
45 if (queueOnly || structured) {
46 if (target && !structured?.attachments?.length) {
47 if (!binding.EnqueueInboxFollowupForTarget) throw new Error("reasonix_error:inbox_not_submitted — target submission unavailable");
48 return binding.EnqueueInboxFollowupForTarget(target, display, submit, structured?.invocations ?? [], key);
49 }
50 return enqueueInboxGuidance(binding, tabId, display, submit, structured, { idempotency: key });
51 }
52 if (target && !structured) {
53 // Discovery is read-only. If no turn can be established, preserve the input
54 // as a follow-up using the original session target and idempotency key.
55 const followup = () => {
56 if (!binding.EnqueueInboxFollowupForTarget) throw new Error("reasonix_error:inbox_not_submitted — target submission unavailable");
57 return binding.EnqueueInboxFollowupForTarget(target, display, submit, [], key);
58 };
59 const activeTurnId = await resolveActiveTurnId(binding, tabId, turnId).catch(() => undefined);
60 if (!activeTurnId || !binding.InboxQueueForTarget) return followup();
61 const result = await binding.InboxQueueForTarget(target, { kind: "enqueue_steer", text: submit, display, turnId: activeTurnId, idempotencyKey: key });
62 // Only explicit unsupported guarantees no mutation. A transport failure or
63 // absent receipt is uncertain and must use receipt recovery, never resend.
64 if (result.outcome === "unavailable" && result.reason === "unsupported") return followup();
65 if (result.outcome === "unavailable" && result.reason === "session_changed") {
66 // Remote selection can change after the POST committed. Retain the
67 // pending request for receipt recovery instead of declaring it unsent.
68 throw new Error("Follow-up receipt unconfirmed — session_changed");
69 }
70 if (result.receipt?.error) throw new Error(result.receipt.error);
71 if (!result.receipt?.itemId) throw new Error("Follow-up receipt unconfirmed");
72 return result.receipt;
73 }
74 return enqueueInboxGuidanceForActiveTurn(binding, tabId, display, submit, structured, turnId, key);
75 }
76
77 export async function enqueueInboxGuidanceForActiveTurn(
78 binding: InboxEnqueueBindings & Pick<AppBindings, "ListTabs">,
79 tabId: string,
80 display: string,
81 submit: string,
82 structured?: StructuredInvocationSubmit,
83 knownTurnId?: string,
84 idempotency?: string,
85 ) {
86 const turnId = !structured && typeof binding.EnqueueInboxSteerForTurn === "function"
87 ? await resolveActiveTurnId(binding, tabId, knownTurnId)
88 : knownTurnId;
89 return enqueueInboxGuidance(binding, tabId, display, submit, structured, { steer: true, turnId, idempotency });
90 }
91
92 export function enqueueInboxGuidance(
93 binding: InboxEnqueueBindings,
94 tabId: string,
95 display: string,
96 submit: string,
97 structured?: StructuredInvocationSubmit,
98 opts?: { steer?: boolean; turnId?: string; idempotency?: string },
99 ) {
100 if (structured) {
101 if (structured.attachments?.length) {
102 if (!structured.attachmentTarget || !binding.EnqueueForAttachmentTarget) {
103 return Promise.reject(new Error("unsupported: attachments-v2"));
104 }
105 return binding.EnqueueForAttachmentTarget(
106 structured.attachmentTarget,
107 structured.attachmentSubmissionId || opts?.idempotency || `image-${crypto.randomUUID()}`,
108 structured.input.trim(),
109 structured.display.trim() || display,
110 structured.invocations,
111 structured.attachments,
112 );
113 }
114 return binding.EnqueueInboxFollowupWithInvocations(
115 tabId,
116 structured.display.trim() || display,
117 structured.input.trim(),
118 structured.invocations,
119 opts?.idempotency ?? "",
120 );
121 }
122 if (opts?.steer && typeof binding.EnqueueInboxSteer === "function") {
123 if (typeof binding.EnqueueInboxSteerForTurn === "function") {
124 if (!opts.turnId) return Promise.reject(new Error("active turn id is unavailable; refresh and try again"));
125 return binding.EnqueueInboxSteerForTurn(tabId, opts.turnId, display, submit || display, opts.idempotency ?? "");
126 }
127 return binding.EnqueueInboxSteer(tabId, display, submit || display, opts.idempotency ?? "");
128 }
129 return binding.EnqueueInboxFollowup(tabId, display, submit || display, opts?.idempotency ?? "");
130 }
131
131 lines TYPESCRIPT