@steipete/oracle 0.14.1 → 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 +16 -6
- package/dist/bin/oracle.js +569 -0
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +1 -0
- package/dist/docs-site/RELEASING.html +410 -0
- package/dist/docs-site/agents.html +374 -0
- package/dist/docs-site/anthropic.html +368 -0
- package/dist/docs-site/bridge.html +416 -0
- package/dist/docs-site/browser-mode.html +594 -0
- package/dist/docs-site/chromium-forks.html +347 -0
- package/dist/docs-site/cli-reference.html +346 -0
- package/dist/docs-site/configuration.html +462 -0
- package/dist/docs-site/favicon.svg +14 -0
- package/dist/docs-site/followup.html +375 -0
- package/dist/docs-site/gemini.html +383 -0
- package/dist/docs-site/grok.html +325 -0
- package/dist/docs-site/index.html +360 -0
- package/dist/docs-site/install.html +335 -0
- package/dist/docs-site/linux.html +321 -0
- package/dist/docs-site/llms.txt +43 -0
- package/dist/docs-site/manual-tests.html +596 -0
- package/dist/docs-site/mcp.html +391 -0
- package/dist/docs-site/multimodel.html +364 -0
- package/dist/docs-site/mythical-pro-agents.html +360 -0
- package/dist/docs-site/notifier.html +338 -0
- package/dist/docs-site/openai-endpoints.html +399 -0
- package/dist/docs-site/openrouter.html +344 -0
- package/dist/docs-site/quickstart.html +369 -0
- package/dist/docs-site/refactor/ux.html +532 -0
- package/dist/docs-site/sessions.html +388 -0
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +79 -0
- package/dist/docs-site/spec.html +363 -0
- package/dist/docs-site/testing.html +320 -0
- package/dist/docs-site/tui-debug.html +326 -0
- package/dist/docs-site/windows-work.html +323 -0
- package/dist/docs-site/windows.html +320 -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 +344 -128
- package/dist/src/browser/actions/modelSelection.js +75 -9
- package/dist/src/browser/actions/promptComposer.js +71 -14
- package/dist/src/browser/actions/thinkingStatus.js +19 -1
- package/dist/src/browser/actions/thinkingTime.js +65 -20
- package/dist/src/browser/artifacts.js +193 -14
- package/dist/src/browser/chatgptFiles.js +674 -91
- package/dist/src/browser/chromeCookies.js +312 -0
- package/dist/src/browser/chromeLifecycle.js +35 -4
- package/dist/src/browser/constants.js +5 -0
- package/dist/src/browser/deepResearchResult.js +23 -0
- package/dist/src/browser/index.js +112 -41
- package/dist/src/browser/keytarShim.js +56 -0
- package/dist/src/browser/profileCopy.js +93 -0
- package/dist/src/browser/sessionRunner.js +9 -3
- package/dist/src/browser/windowsCookies.js +219 -0
- package/dist/src/cli/bridge/client.js +4 -1
- package/dist/src/cli/bridge/doctor.js +19 -0
- package/dist/src/cli/browserConfig.js +17 -1
- package/dist/src/cli/runOptions.js +11 -2
- package/dist/src/cli/sessionDisplay.js +6 -1
- package/dist/src/cli/sessionRunner.js +41 -17
- 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/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +45 -43
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/vendor/oracle-notifier/README.md +26 -0
- package/vendor/oracle-notifier/build-notifier.sh +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
|
}
|
|
@@ -458,7 +622,7 @@ async function configureBrowserDownloadPath(params) {
|
|
|
458
622
|
}
|
|
459
623
|
return false;
|
|
460
624
|
}
|
|
461
|
-
function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabels = [], allowGenericDownloadLabels = true) {
|
|
625
|
+
function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabels = [], allowGenericDownloadLabels = true, options = {}) {
|
|
462
626
|
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
463
627
|
? Math.floor(minTurnIndex)
|
|
464
628
|
: -1;
|
|
@@ -466,12 +630,24 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
466
630
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
467
631
|
const expectedLabelsLiteral = JSON.stringify(expectedLabels);
|
|
468
632
|
const allowGenericDownloadLabelsLiteral = JSON.stringify(allowGenericDownloadLabels);
|
|
633
|
+
const markClickedLiteral = JSON.stringify(options.markClicked === true);
|
|
634
|
+
const returnDiagnosticsLiteral = JSON.stringify(options.returnDiagnostics === true);
|
|
635
|
+
const maxClicksLiteral = typeof options.maxClicks === "number" &&
|
|
636
|
+
Number.isFinite(options.maxClicks) &&
|
|
637
|
+
options.maxClicks > 0
|
|
638
|
+
? Math.floor(options.maxClicks)
|
|
639
|
+
: 0;
|
|
469
640
|
return `(() => {
|
|
470
641
|
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
471
642
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
472
643
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
473
644
|
const EXPECTED_LABELS = ${expectedLabelsLiteral};
|
|
474
645
|
const ALLOW_GENERIC_DOWNLOAD_LABELS = ${allowGenericDownloadLabelsLiteral};
|
|
646
|
+
const MARK_CLICKED = ${markClickedLiteral};
|
|
647
|
+
const RETURN_DIAGNOSTICS = ${returnDiagnosticsLiteral};
|
|
648
|
+
const MAX_CLICKS = ${maxClicksLiteral};
|
|
649
|
+
const HAS_EXPECTED_LABELS = EXPECTED_LABELS.length > 0;
|
|
650
|
+
const CLICKED_ATTRIBUTE = 'data-oracle-download-clicked';
|
|
475
651
|
const isAssistantTurn = (node) => {
|
|
476
652
|
if (!(node instanceof HTMLElement)) return false;
|
|
477
653
|
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
@@ -482,40 +658,333 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
482
658
|
if (testId.includes('assistant')) return true;
|
|
483
659
|
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
484
660
|
};
|
|
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
|
+
}
|
|
669
|
+
};
|
|
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
|
+
}
|
|
682
|
+
};
|
|
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
|
+
};
|
|
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);
|
|
485
765
|
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
486
|
-
const
|
|
766
|
+
const expectedMatches = new Set();
|
|
767
|
+
const genericBehaviorMatches = new Set();
|
|
768
|
+
const genericFallbackMatches = new Set();
|
|
769
|
+
const genericAllMatches = new Set();
|
|
770
|
+
let inspectedCount = 0;
|
|
487
771
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
488
772
|
const turn = turns[index];
|
|
489
773
|
if (!isAssistantTurn(turn)) continue;
|
|
490
774
|
if (MIN_TURN_INDEX >= 0 && index < MIN_TURN_INDEX) continue;
|
|
491
775
|
const messageRoot = turn.querySelector(ASSISTANT_SELECTOR) || turn;
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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);
|
|
507
792
|
});
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
793
|
+
if (genericBehavior.length === 0) {
|
|
794
|
+
controls.filter(genericFallbackButton).forEach((info) => {
|
|
795
|
+
genericFallbackMatches.add(info);
|
|
796
|
+
genericAllMatches.add(info);
|
|
797
|
+
});
|
|
798
|
+
}
|
|
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';
|
|
807
|
+
const selected = expectedMatches.size > 0
|
|
808
|
+
? expectedMatches
|
|
809
|
+
: HAS_EXPECTED_LABELS
|
|
810
|
+
? genericBehaviorMatches.size > 0
|
|
811
|
+
? genericBehaviorMatches
|
|
812
|
+
: genericFallbackMatches
|
|
813
|
+
: genericAllMatches;
|
|
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();
|
|
818
|
+
});
|
|
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,
|
|
516
828
|
}));
|
|
829
|
+
const clicked = clickedDiagnostics.map(({ text, ariaLabel, testId }) => ({ text, ariaLabel, testId }));
|
|
830
|
+
return RETURN_DIAGNOSTICS ? { inspectedCount, selectedCategory, clicked: clickedDiagnostics } : clicked;
|
|
517
831
|
})()`;
|
|
518
832
|
}
|
|
833
|
+
function describeDownloadableFile(file) {
|
|
834
|
+
return (file.filename ??
|
|
835
|
+
file.label ??
|
|
836
|
+
filenameFromUrl(file.sandboxUrl) ??
|
|
837
|
+
filenameFromUrl(file.downloadUrl) ??
|
|
838
|
+
filenameFromUrl(file.url) ??
|
|
839
|
+
file.sandboxUrl ??
|
|
840
|
+
file.downloadUrl ??
|
|
841
|
+
file.url);
|
|
842
|
+
}
|
|
843
|
+
function expectedDownloadedFilename(file) {
|
|
844
|
+
const filename = file.filename ??
|
|
845
|
+
filenameFromUrl(file.sandboxUrl) ??
|
|
846
|
+
filenameFromUrl(file.downloadUrl) ??
|
|
847
|
+
filenameFromUrl(file.url);
|
|
848
|
+
const basename = path.basename(String(filename ?? "").trim());
|
|
849
|
+
return basename && basename !== "." ? basename : undefined;
|
|
850
|
+
}
|
|
851
|
+
async function moveDownloadedFileToExpectedName(filePath, file) {
|
|
852
|
+
const filename = expectedDownloadedFilename(file);
|
|
853
|
+
if (!filename) {
|
|
854
|
+
return filePath;
|
|
855
|
+
}
|
|
856
|
+
const targetPath = path.join(path.dirname(filePath), filename);
|
|
857
|
+
if (path.resolve(targetPath) === path.resolve(filePath)) {
|
|
858
|
+
return filePath;
|
|
859
|
+
}
|
|
860
|
+
const expected = path.parse(filename);
|
|
861
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
862
|
+
const duplicatePattern = new RegExp(`^${escapeRegExp(expected.name)} ?\\(\\d+\\)${escapeRegExp(expected.ext)}$`);
|
|
863
|
+
if (!duplicatePattern.test(path.basename(filePath))) {
|
|
864
|
+
return filePath;
|
|
865
|
+
}
|
|
866
|
+
const targetExists = await fs
|
|
867
|
+
.stat(targetPath)
|
|
868
|
+
.then((stat) => stat.isFile())
|
|
869
|
+
.catch(() => false);
|
|
870
|
+
if (targetExists) {
|
|
871
|
+
return filePath;
|
|
872
|
+
}
|
|
873
|
+
await fs.rename(filePath, targetPath);
|
|
874
|
+
return targetPath;
|
|
875
|
+
}
|
|
876
|
+
async function clickAssistantDownloadButtons(params) {
|
|
877
|
+
const expression = buildClickAssistantDownloadButtonsExpression(params.minTurnIndex, params.expectedLabels ?? [], params.allowGenericDownloadLabels, { markClicked: params.markClicked, maxClicks: params.maxClicks, returnDiagnostics: true });
|
|
878
|
+
const deadline = Date.now() + (params.timeoutMs ?? DOWNLOAD_BUTTON_WAIT_MS);
|
|
879
|
+
let lastInspectedCount = 0;
|
|
880
|
+
let lastSelectedCategory;
|
|
881
|
+
while (Date.now() < deadline) {
|
|
882
|
+
const { result } = await params.Runtime.evaluate({
|
|
883
|
+
expression,
|
|
884
|
+
returnByValue: true,
|
|
885
|
+
});
|
|
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
|
+
}
|
|
899
|
+
if (clicked.length > 0) {
|
|
900
|
+
return {
|
|
901
|
+
clicked,
|
|
902
|
+
inspectedCount: lastInspectedCount,
|
|
903
|
+
selectedCategory: lastSelectedCategory,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
907
|
+
}
|
|
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
|
+
};
|
|
966
|
+
}
|
|
967
|
+
async function savedBrowserFileFromPath(filePath) {
|
|
968
|
+
const filename = path.basename(filePath);
|
|
969
|
+
const stat = await fs.stat(filePath);
|
|
970
|
+
const mimeType = mimeTypeFromFilename(filename);
|
|
971
|
+
const validation = await validateArtifactFile({ path: filePath, filename, mimeType });
|
|
972
|
+
return {
|
|
973
|
+
kind: "file",
|
|
974
|
+
path: filePath,
|
|
975
|
+
label: filename,
|
|
976
|
+
mimeType,
|
|
977
|
+
sizeBytes: stat.size,
|
|
978
|
+
sourceUrl: "browser-download",
|
|
979
|
+
sha256: await computeFileSha256(filePath),
|
|
980
|
+
validation,
|
|
981
|
+
transfer: { status: "not-needed" },
|
|
982
|
+
origin: { mode: "local" },
|
|
983
|
+
url: "browser-download",
|
|
984
|
+
finalUrl: "browser-download",
|
|
985
|
+
filename,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
519
988
|
export async function saveAssistantDownloadButtonArtifacts(params) {
|
|
520
989
|
if ((!params.sessionId && !params.downloadPath) ||
|
|
521
990
|
(!params.Client && !params.Browser && !params.Page)) {
|
|
@@ -539,41 +1008,94 @@ export async function saveAssistantDownloadButtonArtifacts(params) {
|
|
|
539
1008
|
params.logger?.("[browser] Browser download path could not be configured; skipping button fallback.");
|
|
540
1009
|
return [];
|
|
541
1010
|
}
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
1011
|
+
const buttonWaitMs = params.buttonWaitMs ?? DOWNLOAD_BUTTON_WAIT_MS;
|
|
1012
|
+
const downloadWaitMs = params.downloadWaitMs ?? DOWNLOAD_BUTTON_WAIT_MS;
|
|
1013
|
+
const expectedFiles = params.files ?? [];
|
|
1014
|
+
if (expectedFiles.length === 0) {
|
|
1015
|
+
const clickedResult = await clickAssistantDownloadButtons({
|
|
1016
|
+
Runtime: params.Runtime,
|
|
1017
|
+
minTurnIndex: params.minTurnIndex,
|
|
1018
|
+
expectedLabels: [],
|
|
1019
|
+
allowGenericDownloadLabels: params.allowGenericDownloadLabels,
|
|
1020
|
+
timeoutMs: buttonWaitMs,
|
|
1021
|
+
logger: params.logger,
|
|
549
1022
|
});
|
|
550
|
-
clicked
|
|
551
|
-
|
|
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"}).`);
|
|
1025
|
+
return [];
|
|
1026
|
+
}
|
|
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);
|
|
1031
|
+
return Promise.all(downloaded.map(savedBrowserFileFromPath));
|
|
1032
|
+
}
|
|
1033
|
+
let clickedCount = 0;
|
|
1034
|
+
let knownEntries = before;
|
|
1035
|
+
const downloadedPaths = [];
|
|
1036
|
+
const missingFiles = [];
|
|
1037
|
+
const unattemptedFiles = [];
|
|
1038
|
+
for (const [fileIndex, file] of expectedFiles.entries()) {
|
|
1039
|
+
const expectedLabels = resolveDownloadButtonLabels([file]);
|
|
1040
|
+
const displayName = describeDownloadableFile(file);
|
|
1041
|
+
let clickResult = await clickAssistantDownloadButtons({
|
|
1042
|
+
Runtime: params.Runtime,
|
|
1043
|
+
minTurnIndex: params.minTurnIndex,
|
|
1044
|
+
expectedLabels,
|
|
1045
|
+
allowGenericDownloadLabels: params.allowGenericDownloadLabels === true,
|
|
1046
|
+
markClicked: true,
|
|
1047
|
+
maxClicks: 1,
|
|
1048
|
+
timeoutMs: buttonWaitMs,
|
|
1049
|
+
logger: params.logger,
|
|
1050
|
+
});
|
|
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) {
|
|
1070
|
+
missingFiles.push(displayName);
|
|
1071
|
+
knownEntries = new Set(await fs.readdir(artifactsDir).catch(() => []));
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
clickedCount += clickResult.clicked.length;
|
|
1075
|
+
const downloaded = await waitForCompletedDownloadFiles(artifactsDir, knownEntries, 1, downloadWaitMs);
|
|
1076
|
+
if (downloaded.length === 0) {
|
|
1077
|
+
missingFiles.push(displayName);
|
|
1078
|
+
unattemptedFiles.push(...expectedFiles.slice(fileIndex + 1).map(describeDownloadableFile));
|
|
1079
|
+
missingFiles.push(...unattemptedFiles);
|
|
1080
|
+
params.logger?.(`[browser] Download timed out for ${displayName}${unattemptedFiles.length > 0
|
|
1081
|
+
? `; skipped remaining expected file(s) to avoid misassigning a late completion: ${unattemptedFiles.join(", ")}`
|
|
1082
|
+
: ""}`);
|
|
552
1083
|
break;
|
|
553
1084
|
}
|
|
554
|
-
await
|
|
1085
|
+
const normalizedDownloads = await Promise.all(downloaded.map((filePath, index) => index === 0 ? moveDownloadedFileToExpectedName(filePath, file) : filePath));
|
|
1086
|
+
downloadedPaths.push(...normalizedDownloads);
|
|
1087
|
+
knownEntries = new Set(await fs.readdir(artifactsDir).catch(() => []));
|
|
555
1088
|
}
|
|
556
|
-
if (
|
|
557
|
-
params.logger?.("[browser] No assistant download
|
|
558
|
-
return [];
|
|
1089
|
+
if (clickedCount === 0) {
|
|
1090
|
+
params.logger?.("[browser] No assistant download controls found for button fallback.");
|
|
559
1091
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
path: filePath,
|
|
568
|
-
label: filename,
|
|
569
|
-
mimeType: mimeTypeFromFilename(filename),
|
|
570
|
-
sizeBytes: stat.size,
|
|
571
|
-
sourceUrl: "browser-download",
|
|
572
|
-
url: "browser-download",
|
|
573
|
-
finalUrl: "browser-download",
|
|
574
|
-
filename,
|
|
575
|
-
};
|
|
576
|
-
}));
|
|
1092
|
+
else {
|
|
1093
|
+
params.logger?.(`[browser] Clicked ${clickedCount} assistant download control(s).`);
|
|
1094
|
+
}
|
|
1095
|
+
if (missingFiles.length > 0) {
|
|
1096
|
+
params.logger?.(`[browser] Download button fallback did not save expected file(s): ${missingFiles.join(", ")}`);
|
|
1097
|
+
}
|
|
1098
|
+
return Promise.all([...new Set(downloadedPaths)].map(savedBrowserFileFromPath));
|
|
577
1099
|
}
|
|
578
1100
|
async function fetchDownloadWithNode(downloadUrl, getCookieHeader) {
|
|
579
1101
|
let currentUrl = new URL(downloadUrl);
|
|
@@ -585,7 +1107,11 @@ async function fetchDownloadWithNode(downloadUrl, getCookieHeader) {
|
|
|
585
1107
|
isKnownChatGptFileDownloadUrl(currentUrl)) {
|
|
586
1108
|
const cookieHeader = await getCookieHeader(currentUrl.href);
|
|
587
1109
|
if (!cookieHeader) {
|
|
588
|
-
throw
|
|
1110
|
+
throw formatDownloadFailure({
|
|
1111
|
+
strategy: "node-fetch",
|
|
1112
|
+
finalUrl: currentUrl.href,
|
|
1113
|
+
message: "missing ChatGPT cookies for file download",
|
|
1114
|
+
});
|
|
589
1115
|
}
|
|
590
1116
|
headers.cookie = cookieHeader;
|
|
591
1117
|
}
|
|
@@ -593,29 +1119,58 @@ async function fetchDownloadWithNode(downloadUrl, getCookieHeader) {
|
|
|
593
1119
|
headers,
|
|
594
1120
|
redirect: "manual",
|
|
595
1121
|
});
|
|
1122
|
+
const contentType = response.headers.get("content-type");
|
|
596
1123
|
if (response.status >= 300 && response.status < 400) {
|
|
597
1124
|
const location = response.headers.get("location");
|
|
598
1125
|
if (!location) {
|
|
599
|
-
|
|
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
|
+
});
|
|
600
1136
|
}
|
|
601
1137
|
const redirectedUrl = new URL(location, currentUrl);
|
|
602
1138
|
if (redirectedUrl.protocol !== "https:") {
|
|
603
|
-
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
|
+
});
|
|
604
1147
|
}
|
|
605
1148
|
currentUrl = redirectedUrl;
|
|
606
1149
|
continue;
|
|
607
1150
|
}
|
|
608
1151
|
if (!response.ok) {
|
|
609
|
-
|
|
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
|
+
});
|
|
610
1161
|
}
|
|
611
1162
|
return {
|
|
612
1163
|
buffer: Buffer.from(await response.arrayBuffer()),
|
|
613
1164
|
contentDisposition: response.headers.get("content-disposition"),
|
|
614
|
-
contentType
|
|
615
|
-
finalUrl: response.url,
|
|
1165
|
+
contentType,
|
|
1166
|
+
finalUrl: response.url || currentUrl.href,
|
|
616
1167
|
};
|
|
617
1168
|
}
|
|
618
|
-
throw
|
|
1169
|
+
throw formatDownloadFailure({
|
|
1170
|
+
strategy: "node-fetch",
|
|
1171
|
+
finalUrl: currentUrl.href,
|
|
1172
|
+
message: `download exceeded ${DOWNLOAD_REDIRECT_LIMIT} redirects`,
|
|
1173
|
+
});
|
|
619
1174
|
}
|
|
620
1175
|
async function fetchDownloadWithBrowser(Runtime, downloadUrl) {
|
|
621
1176
|
const expression = `(() => {
|
|
@@ -650,14 +1205,24 @@ async function fetchDownloadWithBrowser(Runtime, downloadUrl) {
|
|
|
650
1205
|
if (!value) {
|
|
651
1206
|
throw new Error("browser download returned no value");
|
|
652
1207
|
}
|
|
1208
|
+
const contentType = typeof value.contentType === "string" ? value.contentType : null;
|
|
1209
|
+
const finalUrl = typeof value.url === "string" ? value.url : downloadUrl;
|
|
653
1210
|
if (!value.ok) {
|
|
654
|
-
|
|
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
|
+
});
|
|
655
1220
|
}
|
|
656
1221
|
return {
|
|
657
1222
|
buffer: Buffer.from(String(value.base64 ?? ""), "base64"),
|
|
658
1223
|
contentDisposition: typeof value.contentDisposition === "string" ? value.contentDisposition : null,
|
|
659
|
-
contentType
|
|
660
|
-
finalUrl
|
|
1224
|
+
contentType,
|
|
1225
|
+
finalUrl,
|
|
661
1226
|
};
|
|
662
1227
|
}
|
|
663
1228
|
export async function saveChatGptDownloadableFiles(params) {
|
|
@@ -685,13 +1250,17 @@ export async function saveChatGptDownloadableFiles(params) {
|
|
|
685
1250
|
const sandboxDownloadUrl = downloadUrlFromSandboxUrl(file.sandboxUrl ?? file.url);
|
|
686
1251
|
const downloadUrl = explicitDownloadUrl ?? sandboxDownloadUrl;
|
|
687
1252
|
if (!downloadUrl) {
|
|
688
|
-
const source = file.sandboxUrl ?? file.filename ?? file.url;
|
|
689
|
-
|
|
1253
|
+
const source = sanitizeCandidateFilename(file.sandboxUrl ?? file.filename ?? file.url);
|
|
1254
|
+
const message = `${source}: no ChatGPT download URL found`;
|
|
1255
|
+
errors.push(message);
|
|
690
1256
|
failedFiles.push(file);
|
|
1257
|
+
logger?.(`[browser] Skipping downloadable file ${index + 1}/${files.length}: ${message}`);
|
|
691
1258
|
continue;
|
|
692
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}`);
|
|
693
1262
|
try {
|
|
694
|
-
const downloaded =
|
|
1263
|
+
const downloaded = strategy === "browser-fetch"
|
|
695
1264
|
? await fetchDownloadWithBrowser(params.Runtime, downloadUrl)
|
|
696
1265
|
: await fetchDownloadWithNode(downloadUrl, getCookieHeader);
|
|
697
1266
|
const contentType = downloaded.contentType;
|
|
@@ -708,7 +1277,7 @@ export async function saveChatGptDownloadableFiles(params) {
|
|
|
708
1277
|
contents: downloaded.buffer,
|
|
709
1278
|
label: file.label || filename,
|
|
710
1279
|
mimeType: contentType ?? file.mimeType,
|
|
711
|
-
sourceUrl: file.sandboxUrl ??
|
|
1280
|
+
sourceUrl: file.sandboxUrl ?? "chatgpt-file-endpoint",
|
|
712
1281
|
logger,
|
|
713
1282
|
});
|
|
714
1283
|
if (artifact) {
|
|
@@ -727,9 +1296,14 @@ export async function saveChatGptDownloadableFiles(params) {
|
|
|
727
1296
|
}
|
|
728
1297
|
catch (error) {
|
|
729
1298
|
const message = error instanceof Error ? error.message : String(error);
|
|
730
|
-
|
|
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}`);
|
|
731
1305
|
failedFiles.push(file);
|
|
732
|
-
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}`);
|
|
733
1307
|
}
|
|
734
1308
|
}
|
|
735
1309
|
return {
|
|
@@ -741,16 +1315,24 @@ export async function saveChatGptDownloadableFiles(params) {
|
|
|
741
1315
|
};
|
|
742
1316
|
}
|
|
743
1317
|
export async function collectChatGptFileArtifacts(params) {
|
|
744
|
-
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
|
+
});
|
|
745
1323
|
const textFiles = readTextDownloadableFiles(params.answerText);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}
|
|
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.`);
|
|
749
1326
|
const allFiles = dedupeFiles([...files, ...textFiles]);
|
|
750
1327
|
if (allFiles.length === 0) {
|
|
751
1328
|
return { files: [], savedFiles: [], fileCount: 0 };
|
|
752
1329
|
}
|
|
753
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
|
+
});
|
|
754
1336
|
const saved = await saveChatGptDownloadableFiles({
|
|
755
1337
|
Network: params.Network,
|
|
756
1338
|
Runtime: params.Runtime,
|
|
@@ -775,6 +1357,7 @@ export async function collectChatGptFileArtifacts(params) {
|
|
|
775
1357
|
if (savedFiles.length === 0 && !saved.saved) {
|
|
776
1358
|
const detail = saved.errors.length > 0 ? `\n${saved.errors.join("\n")}` : "";
|
|
777
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.`);
|
|
778
1361
|
}
|
|
779
1362
|
else {
|
|
780
1363
|
params.logger?.(`[browser] Saved ${savedFiles.length} downloadable file artifact(s).`);
|