unbrowse 2.10.2 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -10
- package/bin/unbrowse-wrapper.mjs +14 -30
- package/dist/cli.js +2832 -766
- package/dist/mcp.js +20392 -0
- package/package.json +5 -2
- package/runtime-src/agent-outcome.ts +166 -0
- package/runtime-src/analytics-session.ts +27 -5
- package/runtime-src/api/browse-index.ts +254 -0
- package/runtime-src/api/browse-session.ts +177 -0
- package/runtime-src/api/browse-submit.ts +462 -0
- package/runtime-src/api/routes.ts +348 -281
- package/runtime-src/auth/index.ts +68 -23
- package/runtime-src/cli.ts +574 -165
- package/runtime-src/client/index.ts +171 -18
- package/runtime-src/execution/index.ts +60 -10
- package/runtime-src/execution/robots.ts +167 -0
- package/runtime-src/kuri/client.ts +281 -89
- package/runtime-src/mcp.ts +1065 -0
- package/runtime-src/orchestrator/index.ts +66 -38
- package/runtime-src/orchestrator/timing-economics.ts +80 -0
- package/runtime-src/reverse-engineer/index.ts +33 -3
- package/runtime-src/runtime/setup.ts +8 -8
- package/runtime-src/single-binary.ts +20 -2
- package/runtime-src/types/skill.ts +2 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import type { BrowseSession } from "./browse-session.js";
|
|
2
|
+
import { isRecoverableBrowseFailure } from "./browse-session.js";
|
|
3
|
+
|
|
4
|
+
export interface BrowseSubmitOptions {
|
|
5
|
+
formSelector?: string;
|
|
6
|
+
submitSelector?: string;
|
|
7
|
+
waitFor?: string;
|
|
8
|
+
sameOriginFetchFallback?: boolean;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BrowseSubmitClient {
|
|
13
|
+
evaluate(tabId: string, expression: string): Promise<unknown>;
|
|
14
|
+
getCurrentUrl(tabId: string): Promise<string>;
|
|
15
|
+
getPageHtml(tabId: string): Promise<string>;
|
|
16
|
+
waitForSelector(tabId: string, selector?: string, timeoutMs?: number): Promise<{ status?: string }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BrowseSubmitDeps {
|
|
20
|
+
client: BrowseSubmitClient;
|
|
21
|
+
session: BrowseSession;
|
|
22
|
+
flushCapture?: (session: BrowseSession) => Promise<BrowseSubmitCaptureSyncResult | null>;
|
|
23
|
+
restartCapture: (session: BrowseSession) => Promise<void>;
|
|
24
|
+
rehydratePlugins: (tabId: string) => Promise<unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BrowseSubmitCaptureSyncResult {
|
|
28
|
+
indexed: boolean;
|
|
29
|
+
mode: "http" | "dom" | "none";
|
|
30
|
+
skill_id?: string | null;
|
|
31
|
+
endpoint_count: number;
|
|
32
|
+
request_count?: number;
|
|
33
|
+
background_publish_queued?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BrowseSubmitResult {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
url: string;
|
|
39
|
+
mode: "dom" | "same_origin_fetch" | "noop";
|
|
40
|
+
fallback_used: boolean;
|
|
41
|
+
same_origin_html_rehydrated: boolean;
|
|
42
|
+
recoverable?: boolean;
|
|
43
|
+
reason?: string;
|
|
44
|
+
status?: number;
|
|
45
|
+
wait_for?: string;
|
|
46
|
+
submit_meta?: Record<string, unknown> | null;
|
|
47
|
+
capture_sync?: BrowseSubmitCaptureSyncResult | null;
|
|
48
|
+
rehydrate?: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DEFAULT_SUBMIT_TIMEOUT_MS = 8_000;
|
|
52
|
+
const SUBMIT_POLL_INTERVAL_MS = 250;
|
|
53
|
+
|
|
54
|
+
function sleep(ms: number): Promise<void> {
|
|
55
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
59
|
+
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isUrlWaitHint(value?: string): boolean {
|
|
63
|
+
if (!value) return false;
|
|
64
|
+
return /^https?:\/\//i.test(value) || value.startsWith("/");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function hasMeaningfulPageChange(beforeHtml: string, afterHtml: string): boolean {
|
|
68
|
+
const before = beforeHtml.trim();
|
|
69
|
+
const after = afterHtml.trim();
|
|
70
|
+
if (!after) return false;
|
|
71
|
+
if (!before) return after.length > 64;
|
|
72
|
+
if (before === after) return false;
|
|
73
|
+
if (Math.abs(before.length - after.length) > 48) return true;
|
|
74
|
+
|
|
75
|
+
const beforeBody = before.match(/<body[\s\S]*?>([\s\S]*?)<\/body>/i)?.[1] ?? before;
|
|
76
|
+
const afterBody = after.match(/<body[\s\S]*?>([\s\S]*?)<\/body>/i)?.[1] ?? after;
|
|
77
|
+
return beforeBody.trim() !== afterBody.trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildDomSubmitExpression(options: BrowseSubmitOptions): string {
|
|
81
|
+
return `(function() {
|
|
82
|
+
function findForm(selector) {
|
|
83
|
+
if (selector) return document.querySelector(selector);
|
|
84
|
+
var active = document.activeElement;
|
|
85
|
+
if (active && active.closest) {
|
|
86
|
+
var fromActive = active.closest("form");
|
|
87
|
+
if (fromActive) return fromActive;
|
|
88
|
+
}
|
|
89
|
+
return document.querySelector("form");
|
|
90
|
+
}
|
|
91
|
+
function findSubmitter(form, selector) {
|
|
92
|
+
if (!form) return null;
|
|
93
|
+
if (selector) return document.querySelector(selector);
|
|
94
|
+
var active = document.activeElement;
|
|
95
|
+
if (active && form.contains(active) && /^(submit|image)$/i.test(active.getAttribute("type") || "")) return active;
|
|
96
|
+
return form.querySelector('button[type="submit"], input[type="submit"], input[type="image"], button:not([type])');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var form = findForm(${JSON.stringify(options.formSelector ?? "")});
|
|
100
|
+
if (!form) {
|
|
101
|
+
return JSON.stringify({ ok: false, reason: "form_not_found" });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var submitter = findSubmitter(form, ${JSON.stringify(options.submitSelector ?? "")});
|
|
105
|
+
var meta = {
|
|
106
|
+
ok: true,
|
|
107
|
+
form_action: form.getAttribute("action") || "",
|
|
108
|
+
form_method: (form.getAttribute("method") || "GET").toUpperCase(),
|
|
109
|
+
submitter: submitter ? (submitter.getAttribute("name") || submitter.id || submitter.textContent || submitter.tagName || "").trim() : null,
|
|
110
|
+
submit_selector_used: ${JSON.stringify(options.submitSelector ?? null)},
|
|
111
|
+
form_selector_used: ${JSON.stringify(options.formSelector ?? null)},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (submitter && typeof submitter.click === "function") {
|
|
115
|
+
submitter.click();
|
|
116
|
+
return JSON.stringify({ ...meta, submit_kind: "click" });
|
|
117
|
+
}
|
|
118
|
+
if (typeof form.requestSubmit === "function") {
|
|
119
|
+
form.requestSubmit();
|
|
120
|
+
return JSON.stringify({ ...meta, submit_kind: "requestSubmit" });
|
|
121
|
+
}
|
|
122
|
+
if (typeof form.submit === "function") {
|
|
123
|
+
form.submit();
|
|
124
|
+
return JSON.stringify({ ...meta, submit_kind: "submit" });
|
|
125
|
+
}
|
|
126
|
+
return JSON.stringify({ ok: false, reason: "submit_unavailable" });
|
|
127
|
+
})()`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildSameOriginFetchExpression(options: BrowseSubmitOptions): string {
|
|
131
|
+
return `(async function() {
|
|
132
|
+
function splitPlugins(value) {
|
|
133
|
+
return String(value || "")
|
|
134
|
+
.split(/[\\s,;]+/)
|
|
135
|
+
.map(function(part) { return part.trim(); })
|
|
136
|
+
.filter(Boolean);
|
|
137
|
+
}
|
|
138
|
+
function pluginPath(name) {
|
|
139
|
+
if (/^https?:\\/\\//i.test(name) || name.startsWith("/")) return name;
|
|
140
|
+
return "/etc/designs/wrs/footLibs/js/plugins/" + (name.endsWith(".js") ? name : name + ".js");
|
|
141
|
+
}
|
|
142
|
+
async function bestEffortRehydrate() {
|
|
143
|
+
var modules = Array.from(new Set(
|
|
144
|
+
Array.from(document.querySelectorAll("[data-load-plugins]"))
|
|
145
|
+
.flatMap(function(node) { return splitPlugins(node.getAttribute("data-load-plugins")); })
|
|
146
|
+
));
|
|
147
|
+
if (modules.length === 0) {
|
|
148
|
+
return { attempted: false, loaded: false, nooped: true, reason: "no_plugins", modules: [] };
|
|
149
|
+
}
|
|
150
|
+
if (!window.WRS || typeof window.WRS.require !== "function") {
|
|
151
|
+
return { attempted: false, loaded: false, nooped: true, reason: "missing_wrs_require", modules: modules };
|
|
152
|
+
}
|
|
153
|
+
var requireWrs = window.WRS.require.bind(window.WRS);
|
|
154
|
+
async function loadModules(paths) {
|
|
155
|
+
return await new Promise(function(resolve) {
|
|
156
|
+
var done = false;
|
|
157
|
+
var timer = setTimeout(function() {
|
|
158
|
+
if (done) return;
|
|
159
|
+
done = true;
|
|
160
|
+
resolve({ ok: false, reason: "timeout" });
|
|
161
|
+
}, 1500);
|
|
162
|
+
try {
|
|
163
|
+
requireWrs(paths, function() {
|
|
164
|
+
if (done) return;
|
|
165
|
+
done = true;
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
resolve({ ok: true });
|
|
168
|
+
});
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (done) return;
|
|
171
|
+
done = true;
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
resolve({ ok: false, reason: error && error.message ? error.message : String(error) });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
var configResult = await loadModules(["/etc/designs/wrs/footLibs/js/config.js"]);
|
|
179
|
+
var pluginResult = await loadModules(modules.map(pluginPath));
|
|
180
|
+
for (var i = 0; i < 6; i++) {
|
|
181
|
+
await new Promise(function(resolve) { return setTimeout(resolve, 100); });
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
attempted: true,
|
|
185
|
+
loaded: !!pluginResult.ok,
|
|
186
|
+
nooped: false,
|
|
187
|
+
reason: pluginResult.ok ? undefined : pluginResult.reason,
|
|
188
|
+
config_loaded: !!configResult.ok,
|
|
189
|
+
modules: modules,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function findForm(selector) {
|
|
193
|
+
if (selector) return document.querySelector(selector);
|
|
194
|
+
var active = document.activeElement;
|
|
195
|
+
if (active && active.closest) {
|
|
196
|
+
var fromActive = active.closest("form");
|
|
197
|
+
if (fromActive) return fromActive;
|
|
198
|
+
}
|
|
199
|
+
return document.querySelector("form");
|
|
200
|
+
}
|
|
201
|
+
function findSubmitter(form, selector) {
|
|
202
|
+
if (!form) return null;
|
|
203
|
+
if (selector) return document.querySelector(selector);
|
|
204
|
+
var active = document.activeElement;
|
|
205
|
+
if (active && form.contains(active) && /^(submit|image)$/i.test(active.getAttribute("type") || "")) return active;
|
|
206
|
+
return form.querySelector('button[type="submit"], input[type="submit"], input[type="image"], button:not([type])');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
var form = findForm(${JSON.stringify(options.formSelector ?? "")});
|
|
210
|
+
if (!form) return JSON.stringify({ ok: false, reason: "form_not_found" });
|
|
211
|
+
|
|
212
|
+
var submitter = findSubmitter(form, ${JSON.stringify(options.submitSelector ?? "")});
|
|
213
|
+
var method = (form.getAttribute("method") || "GET").toUpperCase();
|
|
214
|
+
var action = form.getAttribute("action") || window.location.href;
|
|
215
|
+
var targetUrl = new URL(action, window.location.href);
|
|
216
|
+
if (targetUrl.origin !== window.location.origin) {
|
|
217
|
+
return JSON.stringify({ ok: false, reason: "cross_origin", url: targetUrl.href });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
var formData = new FormData(form);
|
|
221
|
+
if (submitter && submitter.name) {
|
|
222
|
+
var submitValue = submitter.value != null ? submitter.value : "";
|
|
223
|
+
if (!formData.has(submitter.name)) formData.append(submitter.name, submitValue);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
var headers = {};
|
|
227
|
+
var requestUrl = targetUrl.href;
|
|
228
|
+
var body;
|
|
229
|
+
if (method === "GET") {
|
|
230
|
+
var params = new URLSearchParams();
|
|
231
|
+
Array.from(formData.entries()).forEach(function(entry) {
|
|
232
|
+
var value = entry[1];
|
|
233
|
+
if (typeof value === "string") params.append(entry[0], value);
|
|
234
|
+
});
|
|
235
|
+
var query = params.toString();
|
|
236
|
+
if (query) requestUrl += (requestUrl.includes("?") ? "&" : "?") + query;
|
|
237
|
+
} else if ((form.enctype || "").includes("application/x-www-form-urlencoded")) {
|
|
238
|
+
var encoded = new URLSearchParams();
|
|
239
|
+
Array.from(formData.entries()).forEach(function(entry) {
|
|
240
|
+
var value = entry[1];
|
|
241
|
+
if (typeof value === "string") encoded.append(entry[0], value);
|
|
242
|
+
});
|
|
243
|
+
body = encoded.toString();
|
|
244
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
245
|
+
} else {
|
|
246
|
+
body = formData;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
var response = await fetch(requestUrl, {
|
|
251
|
+
method: method,
|
|
252
|
+
body: method === "GET" ? undefined : body,
|
|
253
|
+
headers: headers,
|
|
254
|
+
credentials: "include",
|
|
255
|
+
redirect: "follow",
|
|
256
|
+
});
|
|
257
|
+
var contentType = response.headers.get("content-type") || "";
|
|
258
|
+
var text = await response.text();
|
|
259
|
+
var finalUrl = response.url || requestUrl;
|
|
260
|
+
if (!/text\\/html|application\\/xhtml\\+xml/i.test(contentType)) {
|
|
261
|
+
return JSON.stringify({
|
|
262
|
+
ok: false,
|
|
263
|
+
reason: "non_html_response",
|
|
264
|
+
status: response.status,
|
|
265
|
+
url: finalUrl,
|
|
266
|
+
content_type: contentType,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
document.open();
|
|
271
|
+
document.write(text);
|
|
272
|
+
document.close();
|
|
273
|
+
if (finalUrl && finalUrl !== window.location.href) {
|
|
274
|
+
history.replaceState({}, "", finalUrl);
|
|
275
|
+
}
|
|
276
|
+
await new Promise(function(resolve) { return setTimeout(resolve, 50); });
|
|
277
|
+
var rehydrate = await bestEffortRehydrate();
|
|
278
|
+
return JSON.stringify({
|
|
279
|
+
ok: true,
|
|
280
|
+
status: response.status,
|
|
281
|
+
url: finalUrl,
|
|
282
|
+
same_origin_html_rehydrated: true,
|
|
283
|
+
rehydrate: rehydrate,
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return JSON.stringify({
|
|
287
|
+
ok: false,
|
|
288
|
+
reason: error && error.message ? error.message : String(error),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
})()`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function parseJsonString(value: unknown): Record<string, unknown> | null {
|
|
295
|
+
if (typeof value !== "string") return asRecord(value);
|
|
296
|
+
try {
|
|
297
|
+
return asRecord(JSON.parse(value));
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function waitForSubmitOutcome(
|
|
304
|
+
client: BrowseSubmitClient,
|
|
305
|
+
tabId: string,
|
|
306
|
+
beforeUrl: string,
|
|
307
|
+
beforeHtml: string,
|
|
308
|
+
options: BrowseSubmitOptions,
|
|
309
|
+
): Promise<{ ok: boolean; url: string; html: string }> {
|
|
310
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_SUBMIT_TIMEOUT_MS;
|
|
311
|
+
const deadline = Date.now() + timeoutMs;
|
|
312
|
+
const waitFor = options.waitFor?.trim();
|
|
313
|
+
|
|
314
|
+
if (waitFor && !isUrlWaitHint(waitFor)) {
|
|
315
|
+
try {
|
|
316
|
+
const waitResult = await client.waitForSelector(tabId, waitFor, timeoutMs);
|
|
317
|
+
if (waitResult?.status === "found" || waitResult?.status === "ready") {
|
|
318
|
+
const url = await client.getCurrentUrl(tabId).catch(() => beforeUrl);
|
|
319
|
+
const html = await client.getPageHtml(tabId).catch(() => beforeHtml);
|
|
320
|
+
return { ok: true, url, html };
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
// fall through to polling
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
while (Date.now() < deadline) {
|
|
328
|
+
const url = await client.getCurrentUrl(tabId).catch(() => "");
|
|
329
|
+
const html = await client.getPageHtml(tabId).catch(() => "");
|
|
330
|
+
|
|
331
|
+
if (waitFor && isUrlWaitHint(waitFor) && url.includes(waitFor)) {
|
|
332
|
+
return { ok: true, url, html };
|
|
333
|
+
}
|
|
334
|
+
if (url && url !== beforeUrl && !url.startsWith("about:blank")) {
|
|
335
|
+
return { ok: true, url, html };
|
|
336
|
+
}
|
|
337
|
+
if (hasMeaningfulPageChange(beforeHtml, html)) {
|
|
338
|
+
return { ok: true, url: url || beforeUrl, html };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
await sleep(SUBMIT_POLL_INTERVAL_MS);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { ok: false, url: beforeUrl, html: beforeHtml };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function submitBrowseForm(
|
|
348
|
+
deps: BrowseSubmitDeps,
|
|
349
|
+
options: BrowseSubmitOptions = {},
|
|
350
|
+
): Promise<BrowseSubmitResult> {
|
|
351
|
+
const { client, session, flushCapture, restartCapture, rehydratePlugins } = deps;
|
|
352
|
+
const sameOriginFetchFallback = options.sameOriginFetchFallback !== false;
|
|
353
|
+
const beforeUrl = await client.getCurrentUrl(session.tabId).catch(() => session.url);
|
|
354
|
+
const beforeHtml = await client.getPageHtml(session.tabId).catch(() => "");
|
|
355
|
+
|
|
356
|
+
let submitMeta: Record<string, unknown> | null = null;
|
|
357
|
+
let submitError: unknown = null;
|
|
358
|
+
try {
|
|
359
|
+
submitMeta = parseJsonString(await client.evaluate(session.tabId, buildDomSubmitExpression(options)));
|
|
360
|
+
} catch (error) {
|
|
361
|
+
submitError = error;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!submitMeta?.ok && submitMeta?.reason === "form_not_found") {
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
url: beforeUrl || session.url,
|
|
368
|
+
mode: "noop",
|
|
369
|
+
fallback_used: false,
|
|
370
|
+
same_origin_html_rehydrated: false,
|
|
371
|
+
recoverable: false,
|
|
372
|
+
reason: "form_not_found",
|
|
373
|
+
submit_meta: submitMeta,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const domOutcome = await waitForSubmitOutcome(client, session.tabId, beforeUrl, beforeHtml, options);
|
|
378
|
+
if (domOutcome.ok) {
|
|
379
|
+
session.url = domOutcome.url || beforeUrl || session.url;
|
|
380
|
+
let captureSync: BrowseSubmitCaptureSyncResult | null = null;
|
|
381
|
+
if (flushCapture) {
|
|
382
|
+
try {
|
|
383
|
+
captureSync = await flushCapture(session);
|
|
384
|
+
} catch {
|
|
385
|
+
captureSync = null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
await restartCapture(session);
|
|
389
|
+
return {
|
|
390
|
+
ok: true,
|
|
391
|
+
url: session.url,
|
|
392
|
+
mode: "dom",
|
|
393
|
+
fallback_used: false,
|
|
394
|
+
same_origin_html_rehydrated: false,
|
|
395
|
+
wait_for: options.waitFor,
|
|
396
|
+
submit_meta: submitMeta,
|
|
397
|
+
capture_sync: captureSync,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (submitError && !isRecoverableBrowseFailure(submitError) && !sameOriginFetchFallback) {
|
|
402
|
+
throw submitError;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!sameOriginFetchFallback) {
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
url: beforeUrl || session.url,
|
|
409
|
+
mode: "noop",
|
|
410
|
+
fallback_used: false,
|
|
411
|
+
same_origin_html_rehydrated: false,
|
|
412
|
+
recoverable: !!submitError && isRecoverableBrowseFailure(submitError),
|
|
413
|
+
reason: submitError instanceof Error ? submitError.message : "submit_failed",
|
|
414
|
+
submit_meta: submitMeta,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const fallbackPayload = parseJsonString(await client.evaluate(session.tabId, buildSameOriginFetchExpression(options)));
|
|
419
|
+
if (!fallbackPayload?.ok) {
|
|
420
|
+
return {
|
|
421
|
+
ok: false,
|
|
422
|
+
url: String(fallbackPayload?.url ?? beforeUrl ?? session.url),
|
|
423
|
+
mode: "same_origin_fetch",
|
|
424
|
+
fallback_used: true,
|
|
425
|
+
same_origin_html_rehydrated: false,
|
|
426
|
+
recoverable: !!submitError && isRecoverableBrowseFailure(submitError),
|
|
427
|
+
reason: String(fallbackPayload?.reason ?? "same_origin_fetch_failed"),
|
|
428
|
+
status: typeof fallbackPayload?.status === "number" ? fallbackPayload.status as number : undefined,
|
|
429
|
+
submit_meta: submitMeta,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const finalUrl = String(fallbackPayload.url ?? await client.getCurrentUrl(session.tabId).catch(() => beforeUrl));
|
|
434
|
+
session.url = finalUrl || beforeUrl || session.url;
|
|
435
|
+
|
|
436
|
+
let rehydrate = fallbackPayload.rehydrate;
|
|
437
|
+
if (!rehydrate) {
|
|
438
|
+
rehydrate = await rehydratePlugins(session.tabId).catch(() => null);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let captureSync: BrowseSubmitCaptureSyncResult | null = null;
|
|
442
|
+
if (flushCapture) {
|
|
443
|
+
try {
|
|
444
|
+
captureSync = await flushCapture(session);
|
|
445
|
+
} catch {
|
|
446
|
+
captureSync = null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
await restartCapture(session);
|
|
450
|
+
return {
|
|
451
|
+
ok: true,
|
|
452
|
+
url: session.url,
|
|
453
|
+
mode: "same_origin_fetch",
|
|
454
|
+
fallback_used: true,
|
|
455
|
+
same_origin_html_rehydrated: fallbackPayload.same_origin_html_rehydrated === true,
|
|
456
|
+
status: typeof fallbackPayload.status === "number" ? fallbackPayload.status as number : undefined,
|
|
457
|
+
wait_for: options.waitFor,
|
|
458
|
+
submit_meta: submitMeta,
|
|
459
|
+
capture_sync: captureSync,
|
|
460
|
+
rehydrate,
|
|
461
|
+
};
|
|
462
|
+
}
|