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