@steipete/oracle 0.15.0 → 0.15.1
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/dist/bin/oracle-cli.js +14 -6
- package/dist/docs-site/bridge.html +17 -1
- package/dist/docs-site/browser-mode.html +2 -2
- package/dist/docs-site/configuration.html +12 -2
- package/dist/docs-site/openai-endpoints.html +12 -0
- package/dist/src/browser/actions/assistantResponse.js +40 -31
- package/dist/src/browser/actions/attachments.js +28 -1
- package/dist/src/browser/actions/deepResearch.js +212 -67
- package/dist/src/browser/actions/modelSelection.js +30 -7
- package/dist/src/browser/actions/promptComposer.js +71 -14
- package/dist/src/browser/actions/thinkingStatus.js +19 -1
- package/dist/src/browser/artifacts.js +191 -6
- package/dist/src/browser/chatgptFiles.js +525 -91
- package/dist/src/browser/constants.js +5 -0
- package/dist/src/browser/index.js +30 -30
- package/dist/src/browser/sessionRunner.js +9 -3
- package/dist/src/cli/bridge/client.js +4 -1
- package/dist/src/cli/bridge/doctor.js +19 -0
- package/dist/src/cli/runOptions.js +11 -2
- package/dist/src/cli/sessionDisplay.js +6 -1
- package/dist/src/cli/sessionRunner.js +28 -10
- package/dist/src/config.js +3 -0
- package/dist/src/oracle/client.js +2 -0
- package/dist/src/oracle/modelResolver.js +85 -0
- package/dist/src/oracle/multiModelRunner.js +4 -1
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/remote/client.js +253 -22
- package/dist/src/remote/health.js +27 -0
- package/dist/src/remote/server.js +239 -4
- package/dist/src/remote/types.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +13 -13
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -1,10 +1,133 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
4
|
-
import { resolveSessionArtifactsDir, writeBinaryBrowserArtifact } from "./artifacts.js";
|
|
4
|
+
import { computeFileSha256, resolveSessionArtifactsDir, sanitizeArtifactFilename, validateArtifactFile, writeBinaryBrowserArtifact, } from "./artifacts.js";
|
|
5
5
|
const CHATGPT_DOWNLOAD_BASE_URL = "https://chatgpt.com/";
|
|
6
6
|
const DOWNLOAD_BUTTON_WAIT_MS = 15_000;
|
|
7
7
|
const DOWNLOAD_REDIRECT_LIMIT = 5;
|
|
8
|
+
const DIAGNOSTIC_BODY_SNIPPET_BYTES = 180;
|
|
9
|
+
class ChatGptDownloadError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "ChatGptDownloadError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function safeDiagnosticText(value, maxLength = DIAGNOSTIC_BODY_SNIPPET_BYTES) {
|
|
16
|
+
const compact = value
|
|
17
|
+
.replace(/https?:\/\/[^\s"'<>]+/gi, (match) => {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(match);
|
|
20
|
+
return `${url.origin}${url.pathname}${url.search ? "?[redacted]" : ""}`;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return "[redacted-url]";
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
.replace(/("?(?:access_token|authorization|bearer|cookie|id_token|key|secret|session|signature|sig|token)"?\s*[:=]\s*)"?[^,"'\s}]+"?/gi, "$1[redacted]")
|
|
27
|
+
.replace(/\b(access[_ -]?token|authorization|bearer|cookie|id[_ -]?token|api[_ -]?key|secret|session|signature|sig|token)\b\s+["']?[a-z0-9._~+/=-]{4,}/gi, "$1 [redacted]")
|
|
28
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
29
|
+
.replace(/\s+/g, " ")
|
|
30
|
+
.trim();
|
|
31
|
+
return compact.length > maxLength ? `${compact.slice(0, maxLength)}…` : compact;
|
|
32
|
+
}
|
|
33
|
+
function classifyResponseBodyKind(contentType) {
|
|
34
|
+
const value = String(contentType ?? "").toLowerCase();
|
|
35
|
+
if (value.includes("json"))
|
|
36
|
+
return "json";
|
|
37
|
+
if (value.includes("html"))
|
|
38
|
+
return "html";
|
|
39
|
+
if (value.startsWith("text/") || value.includes("xml"))
|
|
40
|
+
return "text";
|
|
41
|
+
return "binary";
|
|
42
|
+
}
|
|
43
|
+
function decodeDiagnosticBodySnippet(body, contentType) {
|
|
44
|
+
const bodyKind = classifyResponseBodyKind(contentType);
|
|
45
|
+
if (body.length === 0 || bodyKind === "binary") {
|
|
46
|
+
return { bodyKind };
|
|
47
|
+
}
|
|
48
|
+
let text = body.subarray(0, DIAGNOSTIC_BODY_SNIPPET_BYTES * 4).toString("utf8");
|
|
49
|
+
if (bodyKind === "html") {
|
|
50
|
+
text = text.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<[^>]+>/g, " ");
|
|
51
|
+
}
|
|
52
|
+
return { bodyKind, bodySnippet: safeDiagnosticText(text) };
|
|
53
|
+
}
|
|
54
|
+
async function readDiagnosticResponseBody(response, contentType) {
|
|
55
|
+
const reader = response.body?.getReader();
|
|
56
|
+
if (!reader) {
|
|
57
|
+
return { bodyKind: classifyResponseBodyKind(contentType) };
|
|
58
|
+
}
|
|
59
|
+
const limit = DIAGNOSTIC_BODY_SNIPPET_BYTES * 4;
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let total = 0;
|
|
62
|
+
try {
|
|
63
|
+
while (total < limit) {
|
|
64
|
+
const { done, value } = await reader.read();
|
|
65
|
+
if (done)
|
|
66
|
+
break;
|
|
67
|
+
const remaining = limit - total;
|
|
68
|
+
const chunk = Buffer.from(value.subarray(0, remaining));
|
|
69
|
+
chunks.push(chunk);
|
|
70
|
+
total += chunk.length;
|
|
71
|
+
if (total >= limit) {
|
|
72
|
+
await reader.cancel().catch(() => undefined);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return decodeDiagnosticBodySnippet(Buffer.concat(chunks), contentType);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { bodyKind: classifyResponseBodyKind(contentType) };
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
reader.releaseLock();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function classifyUrlKind(value) {
|
|
86
|
+
const raw = String(value ?? "");
|
|
87
|
+
if (!raw)
|
|
88
|
+
return "unknown";
|
|
89
|
+
if (raw.startsWith("sandbox:"))
|
|
90
|
+
return "sandbox";
|
|
91
|
+
if (raw === "browser-download")
|
|
92
|
+
return "browser-download";
|
|
93
|
+
try {
|
|
94
|
+
const url = new URL(raw, CHATGPT_DOWNLOAD_BASE_URL);
|
|
95
|
+
if (!isAllowedChatGptHost(url.hostname))
|
|
96
|
+
return "external-https";
|
|
97
|
+
const pathName = url.pathname.toLowerCase();
|
|
98
|
+
if (pathName === "/backend-api/sandbox/download")
|
|
99
|
+
return "chatgpt-sandbox-download";
|
|
100
|
+
if (/^\/backend-api\/files\/[^/]+\/(?:download|content)\/?$/.test(pathName)) {
|
|
101
|
+
return "chatgpt-file-endpoint";
|
|
102
|
+
}
|
|
103
|
+
if (pathName === "/backend-api/estuary/content")
|
|
104
|
+
return "chatgpt-estuary-content";
|
|
105
|
+
return "chatgpt-other";
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return "unknown";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function formatDownloadFailure(params) {
|
|
112
|
+
const parts = [`download failed via ${params.strategy}`];
|
|
113
|
+
if (params.status !== undefined)
|
|
114
|
+
parts.push(`status=${params.status}`);
|
|
115
|
+
if (params.statusText)
|
|
116
|
+
parts.push(`statusText=${safeDiagnosticText(params.statusText, 80)}`);
|
|
117
|
+
if (params.contentType)
|
|
118
|
+
parts.push(`contentType=${safeDiagnosticText(params.contentType, 80)}`);
|
|
119
|
+
parts.push(`finalUrlKind=${classifyUrlKind(params.finalUrl)}`);
|
|
120
|
+
if (params.bodyKind)
|
|
121
|
+
parts.push(`bodyKind=${params.bodyKind}`);
|
|
122
|
+
if (params.bodySnippet)
|
|
123
|
+
parts.push(`bodySnippet=${JSON.stringify(params.bodySnippet)}`);
|
|
124
|
+
if (params.message)
|
|
125
|
+
parts.push(`reason=${safeDiagnosticText(params.message, 140)}`);
|
|
126
|
+
return new ChatGptDownloadError(parts.join(" "));
|
|
127
|
+
}
|
|
128
|
+
function sanitizeCandidateFilename(value) {
|
|
129
|
+
return sanitizeArtifactFilename(String(value ?? ""), "artifact.bin");
|
|
130
|
+
}
|
|
8
131
|
function isAllowedChatGptHost(hostname) {
|
|
9
132
|
const value = hostname.toLowerCase();
|
|
10
133
|
return value === "chatgpt.com" || value === "chat.openai.com";
|
|
@@ -210,34 +333,57 @@ function buildAssistantDownloadableFilesExpression(minTurnIndex) {
|
|
|
210
333
|
return part;
|
|
211
334
|
}
|
|
212
335
|
};
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
336
|
+
const hrefKind = (value) => {
|
|
337
|
+
if (!value) return '';
|
|
338
|
+
if (isSandboxUrl(value)) return 'sandbox';
|
|
339
|
+
if (isChatGptDownloadUrl(value)) return 'chatgpt-file-endpoint';
|
|
340
|
+
return '';
|
|
341
|
+
};
|
|
342
|
+
const collectValues = (node) => {
|
|
343
|
+
const values = [];
|
|
344
|
+
if (String(node.tagName || '').toLowerCase() === 'a') {
|
|
345
|
+
values.push(node.getAttribute('href') || '', node.href || '', node.getAttribute('download') || '');
|
|
346
|
+
}
|
|
347
|
+
for (const attribute of Array.from(node.attributes || [])) {
|
|
217
348
|
values.push(String(attribute.value || ''));
|
|
218
349
|
}
|
|
350
|
+
for (const anchor of Array.from(node.querySelectorAll?.('a[href], a[download]') || [])) {
|
|
351
|
+
values.push(anchor.getAttribute('href') || '', anchor.href || '', anchor.getAttribute('download') || '');
|
|
352
|
+
for (const attribute of Array.from(anchor.attributes || [])) {
|
|
353
|
+
values.push(String(attribute.value || ''));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return values;
|
|
357
|
+
};
|
|
358
|
+
const serializeCandidate = (node) => {
|
|
359
|
+
if (!(node instanceof HTMLElement)) return null;
|
|
360
|
+
const values = collectValues(node);
|
|
219
361
|
const downloadUrl = values.find(isChatGptDownloadUrl) || '';
|
|
220
362
|
const sandboxUrl = values.find(isSandboxUrl) || '';
|
|
221
363
|
if (!downloadUrl && !sandboxUrl) return null;
|
|
222
|
-
const label = (
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
basename(sandboxUrl) ||
|
|
226
|
-
basename(downloadUrl) ||
|
|
227
|
-
label ||
|
|
228
|
-
'';
|
|
364
|
+
const label = (node.textContent || node.getAttribute('aria-label') || node.getAttribute('title') || '').trim();
|
|
365
|
+
const downloadAttr = String(node.tagName || '').toLowerCase() === 'a' ? node.getAttribute('download') || '' : '';
|
|
366
|
+
const filename = downloadAttr || basename(sandboxUrl) || basename(downloadUrl) || label || '';
|
|
229
367
|
return {
|
|
230
|
-
url: downloadUrl || sandboxUrl ||
|
|
368
|
+
url: downloadUrl || sandboxUrl || values.find(hrefKind) || '',
|
|
231
369
|
downloadUrl,
|
|
232
370
|
sandboxUrl,
|
|
233
371
|
filename,
|
|
234
372
|
label,
|
|
235
|
-
mimeType:
|
|
373
|
+
mimeType: node.getAttribute('type') || '',
|
|
236
374
|
};
|
|
237
375
|
};
|
|
238
376
|
const serializeFiles = (root) =>
|
|
239
|
-
Array.from(root.querySelectorAll(
|
|
240
|
-
|
|
377
|
+
Array.from(root.querySelectorAll([
|
|
378
|
+
'a[href]',
|
|
379
|
+
'a[download]',
|
|
380
|
+
'button',
|
|
381
|
+
'[role="button"]',
|
|
382
|
+
'[data-testid]',
|
|
383
|
+
'[aria-label]',
|
|
384
|
+
'[title]',
|
|
385
|
+
].join(',')))
|
|
386
|
+
.map(serializeCandidate)
|
|
241
387
|
.filter(Boolean);
|
|
242
388
|
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
243
389
|
const files = [];
|
|
@@ -296,6 +442,23 @@ function filenameFromContentDisposition(value) {
|
|
|
296
442
|
}
|
|
297
443
|
return /filename="?([^";]+)"?/i.exec(header)?.[1]?.trim();
|
|
298
444
|
}
|
|
445
|
+
function classifyDownloadableFileSourceKind(file, downloadUrl) {
|
|
446
|
+
if (file.sandboxUrl || file.url.startsWith("sandbox:")) {
|
|
447
|
+
return "sandbox";
|
|
448
|
+
}
|
|
449
|
+
if ((downloadUrl ?? file.downloadUrl ?? file.url) === "browser-download") {
|
|
450
|
+
return "browser-download";
|
|
451
|
+
}
|
|
452
|
+
return "chatgpt-file-endpoint";
|
|
453
|
+
}
|
|
454
|
+
function describeDownloadableCandidate(file, downloadUrl) {
|
|
455
|
+
const filename = sanitizeCandidateFilename(file.filename ??
|
|
456
|
+
file.label ??
|
|
457
|
+
filenameFromUrl(file.sandboxUrl) ??
|
|
458
|
+
filenameFromUrl(file.downloadUrl) ??
|
|
459
|
+
filenameFromUrl(file.url));
|
|
460
|
+
return `filename=${filename} source=${classifyDownloadableFileSourceKind(file, downloadUrl)} urlKind=${classifyUrlKind(downloadUrl ?? file.downloadUrl ?? file.sandboxUrl ?? file.url)}`;
|
|
461
|
+
}
|
|
299
462
|
function filenameFromUrl(value) {
|
|
300
463
|
const raw = String(value ?? "")
|
|
301
464
|
.split(/[?#]/)[0]
|
|
@@ -432,6 +595,7 @@ async function configureBrowserDownloadPath(params) {
|
|
|
432
595
|
await params.Client.send("Browser.setDownloadBehavior", {
|
|
433
596
|
behavior: "allow",
|
|
434
597
|
downloadPath: params.downloadPath,
|
|
598
|
+
eventsEnabled: true,
|
|
435
599
|
});
|
|
436
600
|
return true;
|
|
437
601
|
}
|
|
@@ -467,6 +631,7 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
467
631
|
const expectedLabelsLiteral = JSON.stringify(expectedLabels);
|
|
468
632
|
const allowGenericDownloadLabelsLiteral = JSON.stringify(allowGenericDownloadLabels);
|
|
469
633
|
const markClickedLiteral = JSON.stringify(options.markClicked === true);
|
|
634
|
+
const returnDiagnosticsLiteral = JSON.stringify(options.returnDiagnostics === true);
|
|
470
635
|
const maxClicksLiteral = typeof options.maxClicks === "number" &&
|
|
471
636
|
Number.isFinite(options.maxClicks) &&
|
|
472
637
|
options.maxClicks > 0
|
|
@@ -479,6 +644,7 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
479
644
|
const EXPECTED_LABELS = ${expectedLabelsLiteral};
|
|
480
645
|
const ALLOW_GENERIC_DOWNLOAD_LABELS = ${allowGenericDownloadLabelsLiteral};
|
|
481
646
|
const MARK_CLICKED = ${markClickedLiteral};
|
|
647
|
+
const RETURN_DIAGNOSTICS = ${returnDiagnosticsLiteral};
|
|
482
648
|
const MAX_CLICKS = ${maxClicksLiteral};
|
|
483
649
|
const HAS_EXPECTED_LABELS = EXPECTED_LABELS.length > 0;
|
|
484
650
|
const CLICKED_ATTRIBUTE = 'data-oracle-download-clicked';
|
|
@@ -492,55 +658,152 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
492
658
|
if (testId.includes('assistant')) return true;
|
|
493
659
|
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
494
660
|
};
|
|
495
|
-
const
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
return
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
});
|
|
661
|
+
const basename = (value) => {
|
|
662
|
+
const raw = String(value || '').split(/[?#]/)[0].replace(/\\/+$/g, '');
|
|
663
|
+
const part = raw.slice(raw.lastIndexOf('/') + 1);
|
|
664
|
+
try {
|
|
665
|
+
return decodeURIComponent(part);
|
|
666
|
+
} catch {
|
|
667
|
+
return part;
|
|
668
|
+
}
|
|
504
669
|
};
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
|
|
670
|
+
const isSafeSandboxUrl = (value) => {
|
|
671
|
+
const raw = String(value || '').trim();
|
|
672
|
+
if (!raw.startsWith('sandbox:/mnt/data/')) return false;
|
|
673
|
+
try {
|
|
674
|
+
const pathName = decodeURI(new URL(raw).pathname);
|
|
675
|
+
return pathName.startsWith('/mnt/data/') &&
|
|
676
|
+
!pathName.includes('\\\\') &&
|
|
677
|
+
!pathName.includes('\\0') &&
|
|
678
|
+
!pathName.split('/').includes('..');
|
|
679
|
+
} catch {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
508
682
|
};
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
683
|
+
const isChatGptDownloadUrl = (value) => {
|
|
684
|
+
const raw = String(value || '').trim();
|
|
685
|
+
if (!raw || raw.startsWith('sandbox:') || raw.startsWith('blob:')) return false;
|
|
686
|
+
const isSafeSandboxPath = (pathName) => {
|
|
687
|
+
const normalized = String(pathName || '');
|
|
688
|
+
return normalized.startsWith('/mnt/data/') &&
|
|
689
|
+
!normalized.includes('\\\\') &&
|
|
690
|
+
!normalized.includes('\\0') &&
|
|
691
|
+
!normalized.split('/').includes('..');
|
|
692
|
+
};
|
|
693
|
+
try {
|
|
694
|
+
const url = new URL(raw, 'https://chatgpt.com');
|
|
695
|
+
const host = url.hostname.toLowerCase();
|
|
696
|
+
const allowedHost = host === 'chatgpt.com' || host === 'chat.openai.com';
|
|
697
|
+
const pathName = url.pathname.toLowerCase();
|
|
698
|
+
const isKnownFileDownload =
|
|
699
|
+
(pathName === '/backend-api/sandbox/download' && isSafeSandboxPath(url.searchParams.get('path') || '')) ||
|
|
700
|
+
/^\\/backend-api\\/files\\/[^/]+\\/(?:download|content)\\/?$/.test(pathName) ||
|
|
701
|
+
(pathName === '/backend-api/estuary/content' && String(url.searchParams.get('id') || '').startsWith('file_'));
|
|
702
|
+
return allowedHost && url.protocol === 'https:' && !url.port && isKnownFileDownload;
|
|
703
|
+
} catch {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
const safeLower = (value) => String(value || '').trim().toLowerCase();
|
|
708
|
+
const controlInfo = (control) => {
|
|
709
|
+
const href = String(control.tagName || '').toLowerCase() === 'a' ? (control.getAttribute('href') || control.href || '') : '';
|
|
710
|
+
const text = (control.textContent || '').trim();
|
|
711
|
+
const ariaLabel = control.getAttribute('aria-label') || '';
|
|
712
|
+
const title = control.getAttribute('title') || '';
|
|
713
|
+
const testId = control.getAttribute('data-testid') || '';
|
|
714
|
+
const role = control.getAttribute('role') || '';
|
|
715
|
+
const download = String(control.tagName || '').toLowerCase() === 'a' ? (control.getAttribute('download') || '') : '';
|
|
716
|
+
const className = String(control.className || '');
|
|
717
|
+
const attributeValues = Array.from(control.attributes || []).map((attribute) => String(attribute.value || ''));
|
|
718
|
+
return {
|
|
719
|
+
control,
|
|
720
|
+
tagName: control.tagName || '',
|
|
721
|
+
text,
|
|
722
|
+
ariaLabel,
|
|
723
|
+
title,
|
|
724
|
+
testId,
|
|
725
|
+
role,
|
|
726
|
+
href,
|
|
727
|
+
download,
|
|
728
|
+
className,
|
|
729
|
+
attributeValues,
|
|
730
|
+
haystack: [text, ariaLabel, title, testId, role, href, download, basename(href), ...attributeValues]
|
|
731
|
+
.map(safeLower)
|
|
732
|
+
.filter(Boolean),
|
|
733
|
+
};
|
|
515
734
|
};
|
|
735
|
+
const safeClickableControl = (info) =>
|
|
736
|
+
safeLower(info.tagName) !== 'a' || isSafeSandboxUrl(info.href) || isChatGptDownloadUrl(info.href);
|
|
737
|
+
const expectedFileControl = (info) => {
|
|
738
|
+
return EXPECTED_LABELS.some((rawLabel) => {
|
|
739
|
+
const label = safeLower(rawLabel);
|
|
740
|
+
if (!label) return false;
|
|
741
|
+
const downloadLabel = 'download ' + label;
|
|
742
|
+
return info.haystack.some((value) =>
|
|
743
|
+
value === label ||
|
|
744
|
+
value.startsWith(label + ' ') ||
|
|
745
|
+
value === downloadLabel ||
|
|
746
|
+
value.startsWith(downloadLabel + ' ') ||
|
|
747
|
+
value.endsWith('/' + label) ||
|
|
748
|
+
value.includes('/' + label + '?') ||
|
|
749
|
+
value.includes('/' + label + '#') ||
|
|
750
|
+
value.includes('path=%2fmnt%2fdata%2f' + encodeURIComponent(label).toLowerCase()) ||
|
|
751
|
+
value.includes('sandbox:/mnt/data/' + label)
|
|
752
|
+
);
|
|
753
|
+
});
|
|
754
|
+
};
|
|
755
|
+
const hasDownloadIntent = (info) =>
|
|
756
|
+
info.haystack.some((value) =>
|
|
757
|
+
value === 'download' ||
|
|
758
|
+
/^download\\b/.test(value) ||
|
|
759
|
+
value.includes('download') ||
|
|
760
|
+
value === 'download-files-turn-action-button'
|
|
761
|
+
);
|
|
762
|
+
const genericBehaviorButton = (info) =>
|
|
763
|
+
ALLOW_GENERIC_DOWNLOAD_LABELS && info.className.includes('behavior-btn') && hasDownloadIntent(info);
|
|
764
|
+
const genericFallbackButton = (info) => ALLOW_GENERIC_DOWNLOAD_LABELS && hasDownloadIntent(info);
|
|
516
765
|
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
517
766
|
const expectedMatches = new Set();
|
|
518
767
|
const genericBehaviorMatches = new Set();
|
|
519
768
|
const genericFallbackMatches = new Set();
|
|
520
769
|
const genericAllMatches = new Set();
|
|
770
|
+
let inspectedCount = 0;
|
|
521
771
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
522
772
|
const turn = turns[index];
|
|
523
773
|
if (!isAssistantTurn(turn)) continue;
|
|
524
774
|
if (MIN_TURN_INDEX >= 0 && index < MIN_TURN_INDEX) continue;
|
|
525
775
|
const messageRoot = turn.querySelector(ASSISTANT_SELECTOR) || turn;
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
776
|
+
const controls = Array.from(messageRoot.querySelectorAll([
|
|
777
|
+
'button',
|
|
778
|
+
'a[href]',
|
|
779
|
+
'a[download]',
|
|
780
|
+
'[role="button"]',
|
|
781
|
+
].join(',')))
|
|
782
|
+
.filter((control) => control instanceof HTMLElement)
|
|
783
|
+
.filter((control) => !(MARK_CLICKED && control.getAttribute(CLICKED_ATTRIBUTE) === 'true'))
|
|
784
|
+
.map(controlInfo)
|
|
785
|
+
.filter(safeClickableControl);
|
|
786
|
+
inspectedCount += controls.length;
|
|
787
|
+
controls.filter(expectedFileControl).forEach((info) => expectedMatches.add(info));
|
|
788
|
+
const genericBehavior = controls.filter(genericBehaviorButton);
|
|
789
|
+
genericBehavior.forEach((info) => {
|
|
790
|
+
genericBehaviorMatches.add(info);
|
|
791
|
+
genericAllMatches.add(info);
|
|
536
792
|
});
|
|
537
793
|
if (genericBehavior.length === 0) {
|
|
538
|
-
|
|
539
|
-
genericFallbackMatches.add(
|
|
540
|
-
genericAllMatches.add(
|
|
794
|
+
controls.filter(genericFallbackButton).forEach((info) => {
|
|
795
|
+
genericFallbackMatches.add(info);
|
|
796
|
+
genericAllMatches.add(info);
|
|
541
797
|
});
|
|
542
798
|
}
|
|
543
799
|
}
|
|
800
|
+
const selectedCategory = expectedMatches.size > 0
|
|
801
|
+
? 'expected-label'
|
|
802
|
+
: HAS_EXPECTED_LABELS
|
|
803
|
+
? genericBehaviorMatches.size > 0
|
|
804
|
+
? 'generic-behavior'
|
|
805
|
+
: 'generic-fallback'
|
|
806
|
+
: 'generic-all';
|
|
544
807
|
const selected = expectedMatches.size > 0
|
|
545
808
|
? expectedMatches
|
|
546
809
|
: HAS_EXPECTED_LABELS
|
|
@@ -548,16 +811,23 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
548
811
|
? genericBehaviorMatches
|
|
549
812
|
: genericFallbackMatches
|
|
550
813
|
: genericAllMatches;
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
if (MARK_CLICKED)
|
|
554
|
-
|
|
814
|
+
const selectedControls = Array.from(selected).slice(0, MAX_CLICKS > 0 ? MAX_CLICKS : undefined);
|
|
815
|
+
selectedControls.forEach((info) => {
|
|
816
|
+
if (MARK_CLICKED) info.control.setAttribute(CLICKED_ATTRIBUTE, 'true');
|
|
817
|
+
info.control.click();
|
|
555
818
|
});
|
|
556
|
-
|
|
557
|
-
text:
|
|
558
|
-
ariaLabel:
|
|
559
|
-
|
|
819
|
+
const clickedDiagnostics = selectedControls.map((info) => ({
|
|
820
|
+
text: info.text,
|
|
821
|
+
ariaLabel: info.ariaLabel,
|
|
822
|
+
title: info.title,
|
|
823
|
+
testId: info.testId,
|
|
824
|
+
tagName: info.tagName,
|
|
825
|
+
role: info.role,
|
|
826
|
+
hrefKind: info.href ? (info.href.startsWith('sandbox:') ? 'sandbox' : 'link') : '',
|
|
827
|
+
category: selectedCategory,
|
|
560
828
|
}));
|
|
829
|
+
const clicked = clickedDiagnostics.map(({ text, ariaLabel, testId }) => ({ text, ariaLabel, testId }));
|
|
830
|
+
return RETURN_DIAGNOSTICS ? { inspectedCount, selectedCategory, clicked: clickedDiagnostics } : clicked;
|
|
561
831
|
})()`;
|
|
562
832
|
}
|
|
563
833
|
function describeDownloadableFile(file) {
|
|
@@ -604,31 +874,112 @@ async function moveDownloadedFileToExpectedName(filePath, file) {
|
|
|
604
874
|
return targetPath;
|
|
605
875
|
}
|
|
606
876
|
async function clickAssistantDownloadButtons(params) {
|
|
607
|
-
const expression = buildClickAssistantDownloadButtonsExpression(params.minTurnIndex, params.expectedLabels ?? [], params.allowGenericDownloadLabels, { markClicked: params.markClicked, maxClicks: params.maxClicks });
|
|
877
|
+
const expression = buildClickAssistantDownloadButtonsExpression(params.minTurnIndex, params.expectedLabels ?? [], params.allowGenericDownloadLabels, { markClicked: params.markClicked, maxClicks: params.maxClicks, returnDiagnostics: true });
|
|
608
878
|
const deadline = Date.now() + (params.timeoutMs ?? DOWNLOAD_BUTTON_WAIT_MS);
|
|
879
|
+
let lastInspectedCount = 0;
|
|
880
|
+
let lastSelectedCategory;
|
|
609
881
|
while (Date.now() < deadline) {
|
|
610
882
|
const { result } = await params.Runtime.evaluate({
|
|
611
883
|
expression,
|
|
612
884
|
returnByValue: true,
|
|
613
885
|
});
|
|
614
|
-
const
|
|
886
|
+
const value = result?.value;
|
|
887
|
+
const diagnosticValue = value && typeof value === "object" && !Array.isArray(value)
|
|
888
|
+
? value
|
|
889
|
+
: undefined;
|
|
890
|
+
const clicked = Array.isArray(value)
|
|
891
|
+
? value
|
|
892
|
+
: Array.isArray(diagnosticValue?.clicked)
|
|
893
|
+
? diagnosticValue.clicked
|
|
894
|
+
: [];
|
|
895
|
+
if (diagnosticValue) {
|
|
896
|
+
lastInspectedCount = Number(diagnosticValue.inspectedCount ?? lastInspectedCount);
|
|
897
|
+
lastSelectedCategory = diagnosticValue.selectedCategory ?? lastSelectedCategory;
|
|
898
|
+
}
|
|
615
899
|
if (clicked.length > 0) {
|
|
616
|
-
return
|
|
900
|
+
return {
|
|
901
|
+
clicked,
|
|
902
|
+
inspectedCount: lastInspectedCount,
|
|
903
|
+
selectedCategory: lastSelectedCategory,
|
|
904
|
+
};
|
|
617
905
|
}
|
|
618
906
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
619
907
|
}
|
|
620
|
-
return
|
|
908
|
+
return {
|
|
909
|
+
clicked: [],
|
|
910
|
+
inspectedCount: lastInspectedCount,
|
|
911
|
+
selectedCategory: lastSelectedCategory,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function summarizeClickedControls(clicked) {
|
|
915
|
+
return clicked
|
|
916
|
+
.slice(0, 5)
|
|
917
|
+
.map((control) => {
|
|
918
|
+
const tag = control.tagName ? `tag=${safeDiagnosticText(control.tagName, 24)}` : "tag=?";
|
|
919
|
+
const category = control.category
|
|
920
|
+
? `category=${safeDiagnosticText(control.category, 40)}`
|
|
921
|
+
: "category=?";
|
|
922
|
+
const testId = control.testId ? ` testId=${safeDiagnosticText(control.testId, 60)}` : "";
|
|
923
|
+
const aria = control.ariaLabel ? ` aria=${safeDiagnosticText(control.ariaLabel, 60)}` : "";
|
|
924
|
+
const text = control.text ? ` text=${safeDiagnosticText(control.text, 60)}` : "";
|
|
925
|
+
const hrefKind = control.hrefKind
|
|
926
|
+
? ` hrefKind=${safeDiagnosticText(control.hrefKind, 40)}`
|
|
927
|
+
: "";
|
|
928
|
+
return `${tag} ${category}${testId}${aria}${text}${hrefKind}`;
|
|
929
|
+
})
|
|
930
|
+
.join("; ");
|
|
931
|
+
}
|
|
932
|
+
async function clickGeneratedDownloadUrl(params) {
|
|
933
|
+
const filename = expectedDownloadedFilename(params.file) ?? "download";
|
|
934
|
+
const expression = `(() => {
|
|
935
|
+
const anchor = document.createElement('a');
|
|
936
|
+
anchor.href = ${JSON.stringify(params.downloadUrl)};
|
|
937
|
+
anchor.download = ${JSON.stringify(filename)};
|
|
938
|
+
anchor.rel = 'noopener';
|
|
939
|
+
anchor.style.display = 'none';
|
|
940
|
+
anchor.setAttribute('data-oracle-generated-download-anchor', 'true');
|
|
941
|
+
document.body.appendChild(anchor);
|
|
942
|
+
anchor.click();
|
|
943
|
+
setTimeout(() => anchor.remove(), 0);
|
|
944
|
+
return {
|
|
945
|
+
inspectedCount: 1,
|
|
946
|
+
selectedCategory: 'generated-download-url',
|
|
947
|
+
clicked: [{
|
|
948
|
+
text: '',
|
|
949
|
+
ariaLabel: '',
|
|
950
|
+
title: '',
|
|
951
|
+
testId: 'oracle-generated-download-anchor',
|
|
952
|
+
tagName: 'A',
|
|
953
|
+
role: '',
|
|
954
|
+
hrefKind: ${JSON.stringify(classifyUrlKind(params.downloadUrl))},
|
|
955
|
+
category: 'generated-download-url',
|
|
956
|
+
}],
|
|
957
|
+
};
|
|
958
|
+
})()`;
|
|
959
|
+
const { result } = await params.Runtime.evaluate({ expression, returnByValue: true });
|
|
960
|
+
const value = result?.value;
|
|
961
|
+
return {
|
|
962
|
+
clicked: Array.isArray(value?.clicked) ? value.clicked : [],
|
|
963
|
+
inspectedCount: Number(value?.inspectedCount ?? 1),
|
|
964
|
+
selectedCategory: value?.selectedCategory ?? "generated-download-url",
|
|
965
|
+
};
|
|
621
966
|
}
|
|
622
967
|
async function savedBrowserFileFromPath(filePath) {
|
|
623
968
|
const filename = path.basename(filePath);
|
|
624
969
|
const stat = await fs.stat(filePath);
|
|
970
|
+
const mimeType = mimeTypeFromFilename(filename);
|
|
971
|
+
const validation = await validateArtifactFile({ path: filePath, filename, mimeType });
|
|
625
972
|
return {
|
|
626
973
|
kind: "file",
|
|
627
974
|
path: filePath,
|
|
628
975
|
label: filename,
|
|
629
|
-
mimeType
|
|
976
|
+
mimeType,
|
|
630
977
|
sizeBytes: stat.size,
|
|
631
978
|
sourceUrl: "browser-download",
|
|
979
|
+
sha256: await computeFileSha256(filePath),
|
|
980
|
+
validation,
|
|
981
|
+
transfer: { status: "not-needed" },
|
|
982
|
+
origin: { mode: "local" },
|
|
632
983
|
url: "browser-download",
|
|
633
984
|
finalUrl: "browser-download",
|
|
634
985
|
filename,
|
|
@@ -661,19 +1012,22 @@ export async function saveAssistantDownloadButtonArtifacts(params) {
|
|
|
661
1012
|
const downloadWaitMs = params.downloadWaitMs ?? DOWNLOAD_BUTTON_WAIT_MS;
|
|
662
1013
|
const expectedFiles = params.files ?? [];
|
|
663
1014
|
if (expectedFiles.length === 0) {
|
|
664
|
-
const
|
|
1015
|
+
const clickedResult = await clickAssistantDownloadButtons({
|
|
665
1016
|
Runtime: params.Runtime,
|
|
666
1017
|
minTurnIndex: params.minTurnIndex,
|
|
667
1018
|
expectedLabels: [],
|
|
668
1019
|
allowGenericDownloadLabels: params.allowGenericDownloadLabels,
|
|
669
1020
|
timeoutMs: buttonWaitMs,
|
|
1021
|
+
logger: params.logger,
|
|
670
1022
|
});
|
|
671
|
-
if (clicked.length === 0) {
|
|
672
|
-
params.logger?.(
|
|
1023
|
+
if (clickedResult.clicked.length === 0) {
|
|
1024
|
+
params.logger?.(`[browser] No assistant download controls found for button fallback (inspected ${clickedResult.inspectedCount} control(s); category=${clickedResult.selectedCategory ?? "none"}).`);
|
|
673
1025
|
return [];
|
|
674
1026
|
}
|
|
675
|
-
params.logger?.(`[browser] Clicked ${clicked.length} assistant download
|
|
676
|
-
|
|
1027
|
+
params.logger?.(`[browser] Clicked ${clickedResult.clicked.length} assistant download control(s) ` +
|
|
1028
|
+
`(inspected ${clickedResult.inspectedCount}; category=${clickedResult.selectedCategory ?? "unknown"}; ` +
|
|
1029
|
+
`details=${summarizeClickedControls(clickedResult.clicked)}).`);
|
|
1030
|
+
const downloaded = await waitForCompletedDownloadFiles(artifactsDir, before, clickedResult.clicked.length, downloadWaitMs);
|
|
677
1031
|
return Promise.all(downloaded.map(savedBrowserFileFromPath));
|
|
678
1032
|
}
|
|
679
1033
|
let clickedCount = 0;
|
|
@@ -683,7 +1037,8 @@ export async function saveAssistantDownloadButtonArtifacts(params) {
|
|
|
683
1037
|
const unattemptedFiles = [];
|
|
684
1038
|
for (const [fileIndex, file] of expectedFiles.entries()) {
|
|
685
1039
|
const expectedLabels = resolveDownloadButtonLabels([file]);
|
|
686
|
-
const
|
|
1040
|
+
const displayName = describeDownloadableFile(file);
|
|
1041
|
+
let clickResult = await clickAssistantDownloadButtons({
|
|
687
1042
|
Runtime: params.Runtime,
|
|
688
1043
|
minTurnIndex: params.minTurnIndex,
|
|
689
1044
|
expectedLabels,
|
|
@@ -691,14 +1046,32 @@ export async function saveAssistantDownloadButtonArtifacts(params) {
|
|
|
691
1046
|
markClicked: true,
|
|
692
1047
|
maxClicks: 1,
|
|
693
1048
|
timeoutMs: buttonWaitMs,
|
|
1049
|
+
logger: params.logger,
|
|
694
1050
|
});
|
|
695
|
-
|
|
696
|
-
if (clicked.length
|
|
1051
|
+
params.logger?.(`[browser] Button fallback inspected ${clickResult.inspectedCount} control(s) for ${sanitizeCandidateFilename(displayName)}; category=${clickResult.selectedCategory ?? "none"}; clicked=${clickResult.clicked.length}.`);
|
|
1052
|
+
if (clickResult.clicked.length > 0) {
|
|
1053
|
+
params.logger?.(`[browser] Button fallback clicked control detail(s): ${summarizeClickedControls(clickResult.clicked)}`);
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
const explicitDownloadUrl = normalizeChatGptDownloadUrl(file.downloadUrl ?? file.url);
|
|
1057
|
+
const sandboxDownloadUrl = downloadUrlFromSandboxUrl(file.sandboxUrl ?? file.url);
|
|
1058
|
+
const downloadUrl = explicitDownloadUrl ?? sandboxDownloadUrl;
|
|
1059
|
+
if (downloadUrl) {
|
|
1060
|
+
params.logger?.(`[browser] No matching assistant control for ${sanitizeCandidateFilename(displayName)}; trying scoped generated browser download for urlKind=${classifyUrlKind(downloadUrl)}.`);
|
|
1061
|
+
clickResult = await clickGeneratedDownloadUrl({
|
|
1062
|
+
Runtime: params.Runtime,
|
|
1063
|
+
file,
|
|
1064
|
+
downloadUrl,
|
|
1065
|
+
logger: params.logger,
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (clickResult.clicked.length === 0) {
|
|
697
1070
|
missingFiles.push(displayName);
|
|
698
1071
|
knownEntries = new Set(await fs.readdir(artifactsDir).catch(() => []));
|
|
699
1072
|
continue;
|
|
700
1073
|
}
|
|
701
|
-
clickedCount += clicked.length;
|
|
1074
|
+
clickedCount += clickResult.clicked.length;
|
|
702
1075
|
const downloaded = await waitForCompletedDownloadFiles(artifactsDir, knownEntries, 1, downloadWaitMs);
|
|
703
1076
|
if (downloaded.length === 0) {
|
|
704
1077
|
missingFiles.push(displayName);
|
|
@@ -714,10 +1087,10 @@ export async function saveAssistantDownloadButtonArtifacts(params) {
|
|
|
714
1087
|
knownEntries = new Set(await fs.readdir(artifactsDir).catch(() => []));
|
|
715
1088
|
}
|
|
716
1089
|
if (clickedCount === 0) {
|
|
717
|
-
params.logger?.("[browser] No assistant download
|
|
1090
|
+
params.logger?.("[browser] No assistant download controls found for button fallback.");
|
|
718
1091
|
}
|
|
719
1092
|
else {
|
|
720
|
-
params.logger?.(`[browser] Clicked ${clickedCount} assistant download
|
|
1093
|
+
params.logger?.(`[browser] Clicked ${clickedCount} assistant download control(s).`);
|
|
721
1094
|
}
|
|
722
1095
|
if (missingFiles.length > 0) {
|
|
723
1096
|
params.logger?.(`[browser] Download button fallback did not save expected file(s): ${missingFiles.join(", ")}`);
|
|
@@ -734,7 +1107,11 @@ async function fetchDownloadWithNode(downloadUrl, getCookieHeader) {
|
|
|
734
1107
|
isKnownChatGptFileDownloadUrl(currentUrl)) {
|
|
735
1108
|
const cookieHeader = await getCookieHeader(currentUrl.href);
|
|
736
1109
|
if (!cookieHeader) {
|
|
737
|
-
throw
|
|
1110
|
+
throw formatDownloadFailure({
|
|
1111
|
+
strategy: "node-fetch",
|
|
1112
|
+
finalUrl: currentUrl.href,
|
|
1113
|
+
message: "missing ChatGPT cookies for file download",
|
|
1114
|
+
});
|
|
738
1115
|
}
|
|
739
1116
|
headers.cookie = cookieHeader;
|
|
740
1117
|
}
|
|
@@ -742,29 +1119,58 @@ async function fetchDownloadWithNode(downloadUrl, getCookieHeader) {
|
|
|
742
1119
|
headers,
|
|
743
1120
|
redirect: "manual",
|
|
744
1121
|
});
|
|
1122
|
+
const contentType = response.headers.get("content-type");
|
|
745
1123
|
if (response.status >= 300 && response.status < 400) {
|
|
746
1124
|
const location = response.headers.get("location");
|
|
747
1125
|
if (!location) {
|
|
748
|
-
|
|
1126
|
+
const body = await readDiagnosticResponseBody(response, contentType);
|
|
1127
|
+
throw formatDownloadFailure({
|
|
1128
|
+
strategy: "node-fetch",
|
|
1129
|
+
status: response.status,
|
|
1130
|
+
statusText: response.statusText,
|
|
1131
|
+
contentType,
|
|
1132
|
+
finalUrl: currentUrl.href,
|
|
1133
|
+
...body,
|
|
1134
|
+
message: "download redirect missing location",
|
|
1135
|
+
});
|
|
749
1136
|
}
|
|
750
1137
|
const redirectedUrl = new URL(location, currentUrl);
|
|
751
1138
|
if (redirectedUrl.protocol !== "https:") {
|
|
752
|
-
throw
|
|
1139
|
+
throw formatDownloadFailure({
|
|
1140
|
+
strategy: "node-fetch",
|
|
1141
|
+
status: response.status,
|
|
1142
|
+
statusText: response.statusText,
|
|
1143
|
+
contentType,
|
|
1144
|
+
finalUrl: redirectedUrl.href,
|
|
1145
|
+
message: `download redirect rejected: ${redirectedUrl.protocol}`,
|
|
1146
|
+
});
|
|
753
1147
|
}
|
|
754
1148
|
currentUrl = redirectedUrl;
|
|
755
1149
|
continue;
|
|
756
1150
|
}
|
|
757
1151
|
if (!response.ok) {
|
|
758
|
-
|
|
1152
|
+
const body = await readDiagnosticResponseBody(response, contentType);
|
|
1153
|
+
throw formatDownloadFailure({
|
|
1154
|
+
strategy: "node-fetch",
|
|
1155
|
+
status: response.status,
|
|
1156
|
+
statusText: response.statusText,
|
|
1157
|
+
contentType,
|
|
1158
|
+
finalUrl: response.url || currentUrl.href,
|
|
1159
|
+
...body,
|
|
1160
|
+
});
|
|
759
1161
|
}
|
|
760
1162
|
return {
|
|
761
1163
|
buffer: Buffer.from(await response.arrayBuffer()),
|
|
762
1164
|
contentDisposition: response.headers.get("content-disposition"),
|
|
763
|
-
contentType
|
|
764
|
-
finalUrl: response.url,
|
|
1165
|
+
contentType,
|
|
1166
|
+
finalUrl: response.url || currentUrl.href,
|
|
765
1167
|
};
|
|
766
1168
|
}
|
|
767
|
-
throw
|
|
1169
|
+
throw formatDownloadFailure({
|
|
1170
|
+
strategy: "node-fetch",
|
|
1171
|
+
finalUrl: currentUrl.href,
|
|
1172
|
+
message: `download exceeded ${DOWNLOAD_REDIRECT_LIMIT} redirects`,
|
|
1173
|
+
});
|
|
768
1174
|
}
|
|
769
1175
|
async function fetchDownloadWithBrowser(Runtime, downloadUrl) {
|
|
770
1176
|
const expression = `(() => {
|
|
@@ -799,14 +1205,24 @@ async function fetchDownloadWithBrowser(Runtime, downloadUrl) {
|
|
|
799
1205
|
if (!value) {
|
|
800
1206
|
throw new Error("browser download returned no value");
|
|
801
1207
|
}
|
|
1208
|
+
const contentType = typeof value.contentType === "string" ? value.contentType : null;
|
|
1209
|
+
const finalUrl = typeof value.url === "string" ? value.url : downloadUrl;
|
|
802
1210
|
if (!value.ok) {
|
|
803
|
-
|
|
1211
|
+
const body = decodeDiagnosticBodySnippet(Buffer.from(String(value.base64 ?? ""), "base64"), contentType);
|
|
1212
|
+
throw formatDownloadFailure({
|
|
1213
|
+
strategy: "browser-fetch",
|
|
1214
|
+
status: value.status,
|
|
1215
|
+
statusText: value.statusText,
|
|
1216
|
+
contentType,
|
|
1217
|
+
finalUrl,
|
|
1218
|
+
...body,
|
|
1219
|
+
});
|
|
804
1220
|
}
|
|
805
1221
|
return {
|
|
806
1222
|
buffer: Buffer.from(String(value.base64 ?? ""), "base64"),
|
|
807
1223
|
contentDisposition: typeof value.contentDisposition === "string" ? value.contentDisposition : null,
|
|
808
|
-
contentType
|
|
809
|
-
finalUrl
|
|
1224
|
+
contentType,
|
|
1225
|
+
finalUrl,
|
|
810
1226
|
};
|
|
811
1227
|
}
|
|
812
1228
|
export async function saveChatGptDownloadableFiles(params) {
|
|
@@ -834,13 +1250,17 @@ export async function saveChatGptDownloadableFiles(params) {
|
|
|
834
1250
|
const sandboxDownloadUrl = downloadUrlFromSandboxUrl(file.sandboxUrl ?? file.url);
|
|
835
1251
|
const downloadUrl = explicitDownloadUrl ?? sandboxDownloadUrl;
|
|
836
1252
|
if (!downloadUrl) {
|
|
837
|
-
const source = file.sandboxUrl ?? file.filename ?? file.url;
|
|
838
|
-
|
|
1253
|
+
const source = sanitizeCandidateFilename(file.sandboxUrl ?? file.filename ?? file.url);
|
|
1254
|
+
const message = `${source}: no ChatGPT download URL found`;
|
|
1255
|
+
errors.push(message);
|
|
839
1256
|
failedFiles.push(file);
|
|
1257
|
+
logger?.(`[browser] Skipping downloadable file ${index + 1}/${files.length}: ${message}`);
|
|
840
1258
|
continue;
|
|
841
1259
|
}
|
|
1260
|
+
const strategy = params.Runtime && sandboxDownloadUrl && !explicitDownloadUrl ? "browser-fetch" : "node-fetch";
|
|
1261
|
+
logger?.(`[browser] Download candidate ${index + 1}/${files.length}: ${describeDownloadableCandidate(file, downloadUrl)} strategy=${strategy}`);
|
|
842
1262
|
try {
|
|
843
|
-
const downloaded =
|
|
1263
|
+
const downloaded = strategy === "browser-fetch"
|
|
844
1264
|
? await fetchDownloadWithBrowser(params.Runtime, downloadUrl)
|
|
845
1265
|
: await fetchDownloadWithNode(downloadUrl, getCookieHeader);
|
|
846
1266
|
const contentType = downloaded.contentType;
|
|
@@ -857,7 +1277,7 @@ export async function saveChatGptDownloadableFiles(params) {
|
|
|
857
1277
|
contents: downloaded.buffer,
|
|
858
1278
|
label: file.label || filename,
|
|
859
1279
|
mimeType: contentType ?? file.mimeType,
|
|
860
|
-
sourceUrl: file.sandboxUrl ??
|
|
1280
|
+
sourceUrl: file.sandboxUrl ?? "chatgpt-file-endpoint",
|
|
861
1281
|
logger,
|
|
862
1282
|
});
|
|
863
1283
|
if (artifact) {
|
|
@@ -876,9 +1296,14 @@ export async function saveChatGptDownloadableFiles(params) {
|
|
|
876
1296
|
}
|
|
877
1297
|
catch (error) {
|
|
878
1298
|
const message = error instanceof Error ? error.message : String(error);
|
|
879
|
-
|
|
1299
|
+
const safeMessage = safeDiagnosticText(message, 500);
|
|
1300
|
+
const filename = sanitizeCandidateFilename(file.filename ??
|
|
1301
|
+
filenameFromUrl(file.sandboxUrl) ??
|
|
1302
|
+
filenameFromUrl(file.downloadUrl) ??
|
|
1303
|
+
file.url);
|
|
1304
|
+
errors.push(`${filename}: ${safeMessage}`);
|
|
880
1305
|
failedFiles.push(file);
|
|
881
|
-
logger?.(`[browser] Failed to save downloadable file ${index + 1}/${files.length}: ${
|
|
1306
|
+
logger?.(`[browser] Failed to save downloadable file ${index + 1}/${files.length} (${describeDownloadableCandidate(file, downloadUrl)} strategy=${strategy}): ${safeMessage}`);
|
|
882
1307
|
}
|
|
883
1308
|
}
|
|
884
1309
|
return {
|
|
@@ -890,16 +1315,24 @@ export async function saveChatGptDownloadableFiles(params) {
|
|
|
890
1315
|
};
|
|
891
1316
|
}
|
|
892
1317
|
export async function collectChatGptFileArtifacts(params) {
|
|
893
|
-
const files = await readAssistantDownloadableFiles(params.Runtime, params.minTurnIndex ?? undefined).catch(() =>
|
|
1318
|
+
const files = await readAssistantDownloadableFiles(params.Runtime, params.minTurnIndex ?? undefined).catch((error) => {
|
|
1319
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1320
|
+
params.logger?.(`[browser] Failed to inspect assistant DOM file candidates: ${safeDiagnosticText(message, 180)}`);
|
|
1321
|
+
return [];
|
|
1322
|
+
});
|
|
894
1323
|
const textFiles = readTextDownloadableFiles(params.answerText);
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
}
|
|
1324
|
+
params.logger?.(`[browser] Found ${files.length} DOM downloadable file candidate(s).`);
|
|
1325
|
+
params.logger?.(`[browser] Found ${textFiles.length} downloadable file link(s) in captured answer text.`);
|
|
898
1326
|
const allFiles = dedupeFiles([...files, ...textFiles]);
|
|
899
1327
|
if (allFiles.length === 0) {
|
|
900
1328
|
return { files: [], savedFiles: [], fileCount: 0 };
|
|
901
1329
|
}
|
|
902
1330
|
params.logger?.(`[browser] Found ${allFiles.length} downloadable file candidate(s).`);
|
|
1331
|
+
allFiles.forEach((file, index) => {
|
|
1332
|
+
const explicitDownloadUrl = normalizeChatGptDownloadUrl(file.downloadUrl ?? file.url);
|
|
1333
|
+
const sandboxDownloadUrl = downloadUrlFromSandboxUrl(file.sandboxUrl ?? file.url);
|
|
1334
|
+
params.logger?.(`[browser] Candidate ${index + 1}/${allFiles.length}: ${describeDownloadableCandidate(file, explicitDownloadUrl ?? sandboxDownloadUrl)}`);
|
|
1335
|
+
});
|
|
903
1336
|
const saved = await saveChatGptDownloadableFiles({
|
|
904
1337
|
Network: params.Network,
|
|
905
1338
|
Runtime: params.Runtime,
|
|
@@ -924,6 +1357,7 @@ export async function collectChatGptFileArtifacts(params) {
|
|
|
924
1357
|
if (savedFiles.length === 0 && !saved.saved) {
|
|
925
1358
|
const detail = saved.errors.length > 0 ? `\n${saved.errors.join("\n")}` : "";
|
|
926
1359
|
params.logger?.(`[browser] Auto-save for downloadable files failed; returning metadata only.${detail}`);
|
|
1360
|
+
params.logger?.(`[browser] WARNING: ${allFiles.length} downloadable candidate(s) existed, but no local browser-host artifact was saved; bridge artifact-ready will not be emitted until ChatGPT file capture succeeds.`);
|
|
927
1361
|
}
|
|
928
1362
|
else {
|
|
929
1363
|
params.logger?.(`[browser] Saved ${savedFiles.length} downloadable file artifact(s).`);
|