返回 DeepSeek-Reasonix
upload.ts
根目录 / desktop / electron / src / main / browser / upload.ts
1 import type { ActResult } from "./actions.js";
2 import type { GuestDebugger, GuestPage } from "./guestView.js";
3 import type { LocatedRef } from "./refResolver.js";
4 import { REGISTRY_KEY } from "./snapshot.js";
5 import { acquireDebugger, type DebuggerSender } from "./debuggerLease.js";
6 import { runChildFrame } from "./frameRuntime.js";
7
8 const OBJECT_GROUP = "reasonix-upload";
9
10 // The executable source is fixed. Snapshot metadata crosses the CDP boundary
11 // only as call arguments, never as JavaScript source text.
12 const FIND_UPLOAD_NODE = `function (key, docId, snapshotId, ref) {
13 const registry = globalThis[key];
14 if (!registry || registry.docId !== docId || registry.snapshotId !== snapshotId) return null;
15 const element = registry.refs.get(ref);
16 return element && element.isConnected && element.tagName === 'INPUT' && element.type === 'file' ? element : null;
17 }`;
18
19 interface ExecutionContext {
20 id: number;
21 auxData?: { isDefault?: boolean; frameId?: string };
22 }
23
24 function objectIdOf(result: unknown): string | null {
25 const objectId = (result as { result?: { objectId?: unknown } } | null)?.result?.objectId;
26 return typeof objectId === "string" ? objectId : null;
27 }
28
29 // Runtime.enable replays executionContextCreated for every live context
30 // before its own reply, so a listener registered around it sees them all.
31 async function executionContexts(dbg: GuestDebugger, send: DebuggerSender): Promise<ExecutionContext[]> {
32 const contexts: ExecutionContext[] = [];
33 const listener = (_event: unknown, method: string, params: unknown) => {
34 if (method !== "Runtime.executionContextCreated") return;
35 const context = (params as { context?: ExecutionContext } | null)?.context;
36 if (context && typeof context.id === "number") contexts.push(context);
37 };
38 dbg.on("message", listener);
39 try {
40 await send("Runtime.enable");
41 } finally {
42 dbg.removeListener("message", listener);
43 }
44 return contexts;
45 }
46
47 // Find the original registry node in its execution context, including the
48 // main frame's isolated world. A CSS path would silently select a replacement
49 // input after a rerender or a navigation.
50 async function findObjectId(dbg: GuestDebugger, located: LocatedRef, owned: boolean, send: DebuggerSender): Promise<string | null> {
51 const args = [REGISTRY_KEY, located.binding.docId, located.snapshotId, located.ref].map((value) => ({ value }));
52 try {
53 for (const context of await executionContexts(dbg, send)) {
54 if (located.isMainFrame && context.auxData?.isDefault) continue;
55 try {
56 const objectId = objectIdOf(await send("Runtime.callFunctionOn", { functionDeclaration: FIND_UPLOAD_NODE, executionContextId: context.id, arguments: args, objectGroup: OBJECT_GROUP }));
57 if (objectId) return objectId;
58 } catch {
59 // A context that vanished mid-walk is simply not the one we want.
60 }
61 }
62 } finally {
63 if (owned) await send("Runtime.disable").catch(() => undefined);
64 }
65 return null;
66 }
67
68 export async function uploadFiles(page: GuestPage, located: LocatedRef, files: string[], verify: () => void, dispatch: () => void): Promise<ActResult> {
69 if (located.tag !== "input" || located.type !== "file") return { executed: false, reason: "element is not a file input" };
70 const dbg = page.debugger;
71 if (!located.isMainFrame) {
72 return await runChildFrame(page, located.frame, "", async (contextId, sessionId, send) => {
73 const args = [REGISTRY_KEY, located.binding.docId, located.snapshotId, located.ref].map(value => ({ value }));
74 try {
75 const objectId = objectIdOf(await send("Runtime.callFunctionOn", { functionDeclaration: FIND_UPLOAD_NODE, executionContextId: contextId, arguments: args, objectGroup: OBJECT_GROUP }, sessionId));
76 if (!objectId) return { executed: false, reason: "file input reference expired" };
77 verify(); dispatch();
78 await send("DOM.setFileInputFiles", { objectId, files }, sessionId);
79 return { executed: true };
80 } finally { await send("Runtime.releaseObjectGroup", { objectGroup: OBJECT_GROUP }, sessionId).catch(() => undefined); }
81 }) as ActResult;
82 }
83 const release = acquireDebugger(dbg);
84 try {
85 const objectId = await findObjectId(dbg, located, release.owned, release.send);
86 if (!objectId) return { executed: false, reason: "file input could not be located in the page" };
87 verify();
88 dispatch();
89 await release.send("DOM.setFileInputFiles", { objectId, files });
90 await release.send("Runtime.releaseObjectGroup", { objectGroup: OBJECT_GROUP }).catch(() => undefined);
91 return { executed: true };
92 } finally {
93 release();
94 }
95 }
96
96 lines TYPESCRIPT