@steipete/oracle 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -61
- package/dist/bin/oracle-cli.js +77 -68
- package/dist/bin/oracle-mcp.js +0 -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 +400 -0
- package/dist/docs-site/browser-mode.html +593 -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 +452 -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 +387 -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 +91 -9
- package/dist/src/browser/actions/deepResearch.js +180 -52
- package/dist/src/browser/actions/modelSelection.js +111 -24
- package/dist/src/browser/actions/navigation.js +358 -67
- package/dist/src/browser/actions/promptComposer.js +241 -46
- package/dist/src/browser/actions/thinkingTime.js +308 -89
- package/dist/src/browser/artifacts.js +19 -0
- package/dist/src/browser/chatgptFiles.js +797 -0
- package/dist/src/browser/chatgptImages.js +130 -3
- package/dist/src/browser/chromeLifecycle.js +4 -0
- package/dist/src/browser/config.js +4 -0
- package/dist/src/browser/index.js +353 -41
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/policies.js +2 -6
- package/dist/src/browser/projectSourcesRunner.js +14 -2
- package/dist/src/browser/prompt.js +11 -15
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattach.js +6 -2
- package/dist/src/browser/reattachability.js +22 -10
- package/dist/src/browser/recoverConversation.js +73 -0
- package/dist/src/browser/sessionRunner.js +4 -1
- package/dist/src/cli/bridge/doctor.js +7 -2
- package/dist/src/cli/browserConfig.js +13 -2
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +129 -54
- package/dist/src/cli/engine.js +6 -2
- package/dist/src/cli/followup.js +72 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +20 -0
- package/dist/src/cli/reattachGuidance.js +8 -0
- package/dist/src/cli/runOptions.js +10 -31
- package/dist/src/cli/sessionCommand.js +4 -0
- package/dist/src/cli/sessionDisplay.js +17 -3
- package/dist/src/cli/sessionLineage.js +7 -4
- package/dist/src/cli/sessionRunner.js +16 -1
- package/dist/src/config.js +164 -9
- package/dist/src/gemini-web/client.js +5 -10
- package/dist/src/gemini-web/executor.js +1 -24
- package/dist/src/gemini-web/models.js +83 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/chatgptImage.js +127 -0
- package/dist/src/mcp/tools/consult.js +261 -167
- package/dist/src/mcp/types.js +2 -0
- package/dist/src/mcp/utils.js +87 -1
- package/dist/src/oracle/config.js +27 -1
- package/dist/src/oracle/files.js +4 -6
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/oracle/markdown.js +36 -3
- package/dist/src/oracle/modelResolver.js +21 -13
- package/dist/src/oracle/promptAssembly.js +2 -4
- package/dist/src/oracle/request.js +3 -5
- package/dist/src/oracle/tokenStats.js +5 -1
- package/dist/src/oracle.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 +20 -29
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
4
|
+
import { resolveSessionArtifactsDir, writeBinaryBrowserArtifact } from "./artifacts.js";
|
|
5
|
+
const CHATGPT_DOWNLOAD_BASE_URL = "https://chatgpt.com/";
|
|
6
|
+
const DOWNLOAD_BUTTON_WAIT_MS = 15_000;
|
|
7
|
+
const DOWNLOAD_REDIRECT_LIMIT = 5;
|
|
8
|
+
function isAllowedChatGptHost(hostname) {
|
|
9
|
+
const value = hostname.toLowerCase();
|
|
10
|
+
return value === "chatgpt.com" || value === "chat.openai.com";
|
|
11
|
+
}
|
|
12
|
+
function isSafeSandboxPath(value) {
|
|
13
|
+
const pathName = String(value ?? "");
|
|
14
|
+
if (!pathName.startsWith("/mnt/data/")) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (pathName.includes("\\") || pathName.includes("\0")) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return !pathName.split("/").includes("..");
|
|
21
|
+
}
|
|
22
|
+
function isKnownChatGptFileDownloadUrl(url) {
|
|
23
|
+
const pathName = url.pathname.toLowerCase();
|
|
24
|
+
if (pathName === "/backend-api/sandbox/download") {
|
|
25
|
+
return isSafeSandboxPath(url.searchParams.get("path"));
|
|
26
|
+
}
|
|
27
|
+
if (/^\/backend-api\/files\/[^/]+\/(?:download|content)\/?$/.test(pathName)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (pathName === "/backend-api/estuary/content") {
|
|
31
|
+
return (url.searchParams.get("id") ?? "").startsWith("file_");
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function normalizeChatGptDownloadUrl(value) {
|
|
36
|
+
const raw = String(value ?? "").trim();
|
|
37
|
+
if (!raw || raw.startsWith("sandbox:") || raw.startsWith("blob:")) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
let url;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(raw, CHATGPT_DOWNLOAD_BASE_URL);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (!isAllowedChatGptHost(url.hostname)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (url.protocol !== "https:" || url.port) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (!isKnownChatGptFileDownloadUrl(url)) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return url.href;
|
|
57
|
+
}
|
|
58
|
+
function normalizeSandboxPath(value) {
|
|
59
|
+
const raw = String(value ?? "").trim();
|
|
60
|
+
if (!raw.startsWith("sandbox:/mnt/data/")) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
let pathName;
|
|
64
|
+
try {
|
|
65
|
+
pathName = decodeURI(new URL(raw).pathname);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
pathName = raw.slice("sandbox:".length);
|
|
69
|
+
}
|
|
70
|
+
return isSafeSandboxPath(pathName) ? pathName : undefined;
|
|
71
|
+
}
|
|
72
|
+
function normalizeSandboxUrl(value) {
|
|
73
|
+
const pathName = normalizeSandboxPath(value);
|
|
74
|
+
return pathName ? `sandbox:${pathName}` : undefined;
|
|
75
|
+
}
|
|
76
|
+
function downloadUrlFromSandboxUrl(value) {
|
|
77
|
+
const pathName = normalizeSandboxPath(value);
|
|
78
|
+
if (!pathName) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const url = new URL("/backend-api/sandbox/download", CHATGPT_DOWNLOAD_BASE_URL);
|
|
82
|
+
url.searchParams.set("path", pathName);
|
|
83
|
+
return url.href;
|
|
84
|
+
}
|
|
85
|
+
function dedupeFiles(files) {
|
|
86
|
+
const deduped = [];
|
|
87
|
+
const aliases = new Map();
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const fileAliases = [file.downloadUrl, file.sandboxUrl, file.url].filter((value) => Boolean(value));
|
|
90
|
+
const existingIndex = fileAliases
|
|
91
|
+
.map((alias) => aliases.get(alias))
|
|
92
|
+
.find((index) => index !== undefined);
|
|
93
|
+
if (existingIndex === undefined) {
|
|
94
|
+
const index = deduped.length;
|
|
95
|
+
deduped.push(file);
|
|
96
|
+
for (const alias of fileAliases) {
|
|
97
|
+
aliases.set(alias, index);
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const existing = deduped[existingIndex];
|
|
102
|
+
deduped[existingIndex] = {
|
|
103
|
+
...file,
|
|
104
|
+
...existing,
|
|
105
|
+
downloadUrl: existing.downloadUrl ?? file.downloadUrl,
|
|
106
|
+
sandboxUrl: existing.sandboxUrl ?? file.sandboxUrl,
|
|
107
|
+
filename: existing.filename ?? file.filename,
|
|
108
|
+
label: existing.label ?? file.label,
|
|
109
|
+
mimeType: existing.mimeType ?? file.mimeType,
|
|
110
|
+
url: existing.downloadUrl ??
|
|
111
|
+
file.downloadUrl ??
|
|
112
|
+
existing.sandboxUrl ??
|
|
113
|
+
file.sandboxUrl ??
|
|
114
|
+
existing.url ??
|
|
115
|
+
file.url,
|
|
116
|
+
};
|
|
117
|
+
for (const alias of fileAliases) {
|
|
118
|
+
aliases.set(alias, existingIndex);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return deduped;
|
|
122
|
+
}
|
|
123
|
+
function readTextDownloadableFiles(value) {
|
|
124
|
+
const text = String(value ?? "");
|
|
125
|
+
if (!text) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const matches = text.match(/(?:https:\/\/[^\s)\]'"<>]+|sandbox:\/mnt\/data\/[^\s)\]'"<>]+)/g);
|
|
129
|
+
if (!matches) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const files = [];
|
|
133
|
+
for (const candidate of matches) {
|
|
134
|
+
const downloadUrl = normalizeChatGptDownloadUrl(candidate);
|
|
135
|
+
const sandboxUrl = normalizeSandboxUrl(candidate);
|
|
136
|
+
if (!downloadUrl && !sandboxUrl) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
files.push({
|
|
140
|
+
url: downloadUrl ?? sandboxUrl ?? candidate,
|
|
141
|
+
downloadUrl,
|
|
142
|
+
sandboxUrl,
|
|
143
|
+
filename: filenameFromUrl(sandboxUrl ?? downloadUrl ?? candidate),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return dedupeFiles(files);
|
|
147
|
+
}
|
|
148
|
+
function buildAssistantDownloadableFilesExpression(minTurnIndex) {
|
|
149
|
+
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
150
|
+
? Math.floor(minTurnIndex)
|
|
151
|
+
: -1;
|
|
152
|
+
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
153
|
+
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
154
|
+
return `(() => {
|
|
155
|
+
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
156
|
+
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
157
|
+
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
158
|
+
const isAssistantTurn = (node) => {
|
|
159
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
160
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
161
|
+
if (turnAttr === 'assistant') return true;
|
|
162
|
+
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
163
|
+
if (role === 'assistant') return true;
|
|
164
|
+
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
165
|
+
if (testId.includes('assistant')) return true;
|
|
166
|
+
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
167
|
+
};
|
|
168
|
+
const isChatGptDownloadUrl = (value) => {
|
|
169
|
+
const raw = String(value || '').trim();
|
|
170
|
+
if (!raw || raw.startsWith('sandbox:') || raw.startsWith('blob:')) return false;
|
|
171
|
+
const isSafeSandboxPath = (path) => {
|
|
172
|
+
const value = String(path || '');
|
|
173
|
+
return value.startsWith('/mnt/data/') &&
|
|
174
|
+
!value.includes('\\\\') &&
|
|
175
|
+
!value.includes('\\0') &&
|
|
176
|
+
!value.split('/').includes('..');
|
|
177
|
+
};
|
|
178
|
+
try {
|
|
179
|
+
const url = new URL(raw, location.origin || 'https://chatgpt.com');
|
|
180
|
+
const host = url.hostname.toLowerCase();
|
|
181
|
+
const allowedHost = host === 'chatgpt.com' || host === 'chat.openai.com';
|
|
182
|
+
const pathName = url.pathname.toLowerCase();
|
|
183
|
+
const isKnownFileDownload =
|
|
184
|
+
(pathName === '/backend-api/sandbox/download' && isSafeSandboxPath(url.searchParams.get('path') || '')) ||
|
|
185
|
+
/^\\/backend-api\\/files\\/[^/]+\\/(?:download|content)\\/?$/.test(pathName) ||
|
|
186
|
+
(pathName === '/backend-api/estuary/content' && String(url.searchParams.get('id') || '').startsWith('file_'));
|
|
187
|
+
return allowedHost && url.protocol === 'https:' && !url.port && isKnownFileDownload;
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const isSandboxUrl = (value) => {
|
|
193
|
+
const raw = String(value || '').trim();
|
|
194
|
+
if (!raw.startsWith('sandbox:/mnt/data/')) return false;
|
|
195
|
+
try {
|
|
196
|
+
return decodeURI(new URL(raw).pathname).startsWith('/mnt/data/') &&
|
|
197
|
+
!decodeURI(new URL(raw).pathname).includes('\\\\') &&
|
|
198
|
+
!decodeURI(new URL(raw).pathname).includes('\\0') &&
|
|
199
|
+
!decodeURI(new URL(raw).pathname).split('/').includes('..');
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const basename = (value) => {
|
|
205
|
+
const raw = String(value || '').split(/[?#]/)[0].replace(/\\/+$/g, '');
|
|
206
|
+
const part = raw.slice(raw.lastIndexOf('/') + 1);
|
|
207
|
+
try {
|
|
208
|
+
return decodeURIComponent(part);
|
|
209
|
+
} catch {
|
|
210
|
+
return part;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const serializeAnchor = (anchor) => {
|
|
214
|
+
const hrefAttr = anchor.getAttribute('href') || '';
|
|
215
|
+
const values = [hrefAttr, anchor.href || ''];
|
|
216
|
+
for (const attribute of Array.from(anchor.attributes || [])) {
|
|
217
|
+
values.push(String(attribute.value || ''));
|
|
218
|
+
}
|
|
219
|
+
const downloadUrl = values.find(isChatGptDownloadUrl) || '';
|
|
220
|
+
const sandboxUrl = values.find(isSandboxUrl) || '';
|
|
221
|
+
if (!downloadUrl && !sandboxUrl) return null;
|
|
222
|
+
const label = (anchor.textContent || anchor.getAttribute('aria-label') || anchor.title || '').trim();
|
|
223
|
+
const filename =
|
|
224
|
+
anchor.getAttribute('download') ||
|
|
225
|
+
basename(sandboxUrl) ||
|
|
226
|
+
basename(downloadUrl) ||
|
|
227
|
+
label ||
|
|
228
|
+
'';
|
|
229
|
+
return {
|
|
230
|
+
url: downloadUrl || sandboxUrl || hrefAttr || anchor.href || '',
|
|
231
|
+
downloadUrl,
|
|
232
|
+
sandboxUrl,
|
|
233
|
+
filename,
|
|
234
|
+
label,
|
|
235
|
+
mimeType: anchor.getAttribute('type') || '',
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
const serializeFiles = (root) =>
|
|
239
|
+
Array.from(root.querySelectorAll('a[href], a[download]'))
|
|
240
|
+
.map(serializeAnchor)
|
|
241
|
+
.filter(Boolean);
|
|
242
|
+
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
243
|
+
const files = [];
|
|
244
|
+
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
245
|
+
const turn = turns[index];
|
|
246
|
+
if (!isAssistantTurn(turn)) continue;
|
|
247
|
+
if (MIN_TURN_INDEX >= 0 && index < MIN_TURN_INDEX) continue;
|
|
248
|
+
const messageRoot = turn.querySelector(ASSISTANT_SELECTOR) || turn;
|
|
249
|
+
files.push(...serializeFiles(messageRoot));
|
|
250
|
+
}
|
|
251
|
+
return files;
|
|
252
|
+
})()`;
|
|
253
|
+
}
|
|
254
|
+
export async function readAssistantDownloadableFiles(Runtime, minTurnIndex) {
|
|
255
|
+
const { result } = await Runtime.evaluate({
|
|
256
|
+
expression: buildAssistantDownloadableFilesExpression(minTurnIndex),
|
|
257
|
+
returnByValue: true,
|
|
258
|
+
});
|
|
259
|
+
const raw = Array.isArray(result?.value) ? result.value : [];
|
|
260
|
+
const normalized = [];
|
|
261
|
+
for (const item of raw) {
|
|
262
|
+
const downloadUrl = normalizeChatGptDownloadUrl(typeof item?.downloadUrl === "string" ? item.downloadUrl : item?.url);
|
|
263
|
+
const sandboxUrl = normalizeSandboxUrl(typeof item?.sandboxUrl === "string" ? item.sandboxUrl : item?.url);
|
|
264
|
+
if (!downloadUrl && !sandboxUrl) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
normalized.push({
|
|
268
|
+
url: downloadUrl ?? sandboxUrl ?? "",
|
|
269
|
+
downloadUrl,
|
|
270
|
+
sandboxUrl,
|
|
271
|
+
filename: typeof item?.filename === "string" ? item.filename : undefined,
|
|
272
|
+
label: typeof item?.label === "string" ? item.label : undefined,
|
|
273
|
+
mimeType: typeof item?.mimeType === "string" ? item.mimeType : undefined,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return dedupeFiles(normalized);
|
|
277
|
+
}
|
|
278
|
+
async function buildCookieHeader(Network, downloadUrl) {
|
|
279
|
+
const url = new URL(downloadUrl);
|
|
280
|
+
const response = await Network.getCookies({ urls: [`${url.origin}/`] });
|
|
281
|
+
return (response.cookies ?? [])
|
|
282
|
+
.filter((cookie) => cookie.name && typeof cookie.value === "string")
|
|
283
|
+
.map((cookie) => `${cookie.name}=${cookie.value}`)
|
|
284
|
+
.join("; ");
|
|
285
|
+
}
|
|
286
|
+
function filenameFromContentDisposition(value) {
|
|
287
|
+
const header = String(value ?? "");
|
|
288
|
+
const encoded = /filename\*=UTF-8''([^;]+)/i.exec(header)?.[1];
|
|
289
|
+
if (encoded) {
|
|
290
|
+
try {
|
|
291
|
+
return decodeURIComponent(encoded.trim().replace(/^"|"$/g, ""));
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return encoded.trim().replace(/^"|"$/g, "");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return /filename="?([^";]+)"?/i.exec(header)?.[1]?.trim();
|
|
298
|
+
}
|
|
299
|
+
function filenameFromUrl(value) {
|
|
300
|
+
const raw = String(value ?? "")
|
|
301
|
+
.split(/[?#]/)[0]
|
|
302
|
+
.replace(/\/+$/g, "");
|
|
303
|
+
if (!raw)
|
|
304
|
+
return undefined;
|
|
305
|
+
const part = raw.slice(raw.lastIndexOf("/") + 1);
|
|
306
|
+
if (!part)
|
|
307
|
+
return undefined;
|
|
308
|
+
try {
|
|
309
|
+
return decodeURIComponent(part);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return part;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function fallbackExtensionFromContentType(contentType) {
|
|
316
|
+
const value = String(contentType ?? "").toLowerCase();
|
|
317
|
+
if (value.includes("zip"))
|
|
318
|
+
return "zip";
|
|
319
|
+
if (value.includes("json"))
|
|
320
|
+
return "json";
|
|
321
|
+
if (value.includes("csv"))
|
|
322
|
+
return "csv";
|
|
323
|
+
if (value.includes("markdown"))
|
|
324
|
+
return "md";
|
|
325
|
+
if (value.includes("html"))
|
|
326
|
+
return "html";
|
|
327
|
+
if (value.includes("pdf"))
|
|
328
|
+
return "pdf";
|
|
329
|
+
if (value.startsWith("text/"))
|
|
330
|
+
return "txt";
|
|
331
|
+
return "bin";
|
|
332
|
+
}
|
|
333
|
+
function mimeTypeFromFilename(filename) {
|
|
334
|
+
const ext = path.extname(filename).toLowerCase();
|
|
335
|
+
if (ext === ".png")
|
|
336
|
+
return "image/png";
|
|
337
|
+
if (ext === ".jpg" || ext === ".jpeg")
|
|
338
|
+
return "image/jpeg";
|
|
339
|
+
if (ext === ".webp")
|
|
340
|
+
return "image/webp";
|
|
341
|
+
if (ext === ".gif")
|
|
342
|
+
return "image/gif";
|
|
343
|
+
if (ext === ".svg")
|
|
344
|
+
return "image/svg+xml";
|
|
345
|
+
if (ext === ".csv")
|
|
346
|
+
return "text/csv";
|
|
347
|
+
if (ext === ".json")
|
|
348
|
+
return "application/json";
|
|
349
|
+
if (ext === ".zip")
|
|
350
|
+
return "application/zip";
|
|
351
|
+
if (ext === ".md")
|
|
352
|
+
return "text/markdown";
|
|
353
|
+
if (ext === ".html")
|
|
354
|
+
return "text/html";
|
|
355
|
+
if (ext === ".pdf")
|
|
356
|
+
return "application/pdf";
|
|
357
|
+
if (ext === ".txt")
|
|
358
|
+
return "text/plain";
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
function resolveDownloadButtonLabels(files) {
|
|
362
|
+
const labels = new Set();
|
|
363
|
+
for (const file of files) {
|
|
364
|
+
for (const value of [
|
|
365
|
+
file.filename,
|
|
366
|
+
file.label,
|
|
367
|
+
filenameFromUrl(file.sandboxUrl),
|
|
368
|
+
filenameFromUrl(file.downloadUrl),
|
|
369
|
+
filenameFromUrl(file.url),
|
|
370
|
+
]) {
|
|
371
|
+
const normalized = String(value ?? "")
|
|
372
|
+
.trim()
|
|
373
|
+
.toLowerCase();
|
|
374
|
+
if (normalized) {
|
|
375
|
+
labels.add(normalized);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return [...labels];
|
|
380
|
+
}
|
|
381
|
+
function resolveDownloadedFilename(params) {
|
|
382
|
+
const filename = filenameFromContentDisposition(params.contentDisposition) ??
|
|
383
|
+
params.file.filename ??
|
|
384
|
+
filenameFromUrl(params.file.sandboxUrl) ??
|
|
385
|
+
filenameFromUrl(params.file.downloadUrl) ??
|
|
386
|
+
filenameFromUrl(params.file.url);
|
|
387
|
+
if (filename && path.extname(filename)) {
|
|
388
|
+
return filename;
|
|
389
|
+
}
|
|
390
|
+
const fallback = filename || `chatgpt-file-${params.index + 1}`;
|
|
391
|
+
return `${fallback}.${fallbackExtensionFromContentType(params.contentType)}`;
|
|
392
|
+
}
|
|
393
|
+
async function listCompletedDownloadFiles(dir, before) {
|
|
394
|
+
const entries = await fs.readdir(dir).catch(() => []);
|
|
395
|
+
const files = [];
|
|
396
|
+
for (const entry of entries) {
|
|
397
|
+
if (before.has(entry) || entry.endsWith(".crdownload")) {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const filePath = path.join(dir, entry);
|
|
401
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
402
|
+
if (stat?.isFile() && stat.size > 0) {
|
|
403
|
+
files.push(filePath);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return files;
|
|
407
|
+
}
|
|
408
|
+
async function waitForCompletedDownloadFiles(dir, before, expectedCount, timeoutMs = 10_000) {
|
|
409
|
+
const deadline = Date.now() + timeoutMs;
|
|
410
|
+
let latest = [];
|
|
411
|
+
let stableSignature = "";
|
|
412
|
+
let stableSince = 0;
|
|
413
|
+
while (Date.now() < deadline) {
|
|
414
|
+
latest = await listCompletedDownloadFiles(dir, before);
|
|
415
|
+
if (latest.length >= expectedCount) {
|
|
416
|
+
const signature = [...latest].sort().join("\n");
|
|
417
|
+
if (signature !== stableSignature) {
|
|
418
|
+
stableSignature = signature;
|
|
419
|
+
stableSince = Date.now();
|
|
420
|
+
}
|
|
421
|
+
else if (Date.now() - stableSince >= 500) {
|
|
422
|
+
return latest;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
426
|
+
}
|
|
427
|
+
return latest;
|
|
428
|
+
}
|
|
429
|
+
async function configureBrowserDownloadPath(params) {
|
|
430
|
+
if (params.Client?.send) {
|
|
431
|
+
try {
|
|
432
|
+
await params.Client.send("Browser.setDownloadBehavior", {
|
|
433
|
+
behavior: "allow",
|
|
434
|
+
downloadPath: params.downloadPath,
|
|
435
|
+
});
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
440
|
+
params.logger?.(`[browser] Browser.setDownloadBehavior unavailable: ${message}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const BrowserWithDownloads = params.Browser;
|
|
444
|
+
if (BrowserWithDownloads?.setDownloadBehavior) {
|
|
445
|
+
await BrowserWithDownloads.setDownloadBehavior({
|
|
446
|
+
behavior: "allow",
|
|
447
|
+
downloadPath: params.downloadPath,
|
|
448
|
+
});
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
const PageWithDownloads = params.Page;
|
|
452
|
+
if (PageWithDownloads?.setDownloadBehavior) {
|
|
453
|
+
await PageWithDownloads.setDownloadBehavior({
|
|
454
|
+
behavior: "allow",
|
|
455
|
+
downloadPath: params.downloadPath,
|
|
456
|
+
});
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabels = [], allowGenericDownloadLabels = true) {
|
|
462
|
+
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
463
|
+
? Math.floor(minTurnIndex)
|
|
464
|
+
: -1;
|
|
465
|
+
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
466
|
+
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
467
|
+
const expectedLabelsLiteral = JSON.stringify(expectedLabels);
|
|
468
|
+
const allowGenericDownloadLabelsLiteral = JSON.stringify(allowGenericDownloadLabels);
|
|
469
|
+
return `(() => {
|
|
470
|
+
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
471
|
+
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
472
|
+
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
473
|
+
const EXPECTED_LABELS = ${expectedLabelsLiteral};
|
|
474
|
+
const ALLOW_GENERIC_DOWNLOAD_LABELS = ${allowGenericDownloadLabelsLiteral};
|
|
475
|
+
const isAssistantTurn = (node) => {
|
|
476
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
477
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
478
|
+
if (turnAttr === 'assistant') return true;
|
|
479
|
+
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
480
|
+
if (role === 'assistant') return true;
|
|
481
|
+
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
482
|
+
if (testId.includes('assistant')) return true;
|
|
483
|
+
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
484
|
+
};
|
|
485
|
+
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
486
|
+
const selected = new Set();
|
|
487
|
+
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
488
|
+
const turn = turns[index];
|
|
489
|
+
if (!isAssistantTurn(turn)) continue;
|
|
490
|
+
if (MIN_TURN_INDEX >= 0 && index < MIN_TURN_INDEX) continue;
|
|
491
|
+
const messageRoot = turn.querySelector(ASSISTANT_SELECTOR) || turn;
|
|
492
|
+
const buttons = Array.from(messageRoot.querySelectorAll('button'));
|
|
493
|
+
const primary = buttons.filter((button) => {
|
|
494
|
+
const text = (button.textContent || '').trim().toLowerCase();
|
|
495
|
+
const expectedFile = EXPECTED_LABELS.some(
|
|
496
|
+
(label) => text === label || text.startsWith(label + ' ')
|
|
497
|
+
);
|
|
498
|
+
return String(button.className || '').includes('behavior-btn') &&
|
|
499
|
+
(expectedFile || (ALLOW_GENERIC_DOWNLOAD_LABELS && /^download\\b/.test(text)));
|
|
500
|
+
});
|
|
501
|
+
const fallback = primary.length > 0 ? [] : buttons.filter((button) => {
|
|
502
|
+
if (!ALLOW_GENERIC_DOWNLOAD_LABELS) return false;
|
|
503
|
+
const text = (button.textContent || '').trim().toLowerCase();
|
|
504
|
+
const aria = (button.getAttribute('aria-label') || '').trim().toLowerCase();
|
|
505
|
+
const testId = (button.getAttribute('data-testid') || '').trim().toLowerCase();
|
|
506
|
+
return text === 'download' || aria === 'download' || testId === 'download-files-turn-action-button';
|
|
507
|
+
});
|
|
508
|
+
[...primary, ...fallback].forEach((button) => selected.add(button));
|
|
509
|
+
}
|
|
510
|
+
const selectedButtons = Array.from(selected);
|
|
511
|
+
selectedButtons.forEach((button) => button.click());
|
|
512
|
+
return selectedButtons.map((button) => ({
|
|
513
|
+
text: (button.textContent || '').trim(),
|
|
514
|
+
ariaLabel: button.getAttribute('aria-label') || '',
|
|
515
|
+
testId: button.getAttribute('data-testid') || '',
|
|
516
|
+
}));
|
|
517
|
+
})()`;
|
|
518
|
+
}
|
|
519
|
+
export async function saveAssistantDownloadButtonArtifacts(params) {
|
|
520
|
+
if ((!params.sessionId && !params.downloadPath) ||
|
|
521
|
+
(!params.Client && !params.Browser && !params.Page)) {
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
const artifactsDir = params.downloadPath ?? resolveSessionArtifactsDir(params.sessionId);
|
|
525
|
+
await fs.mkdir(artifactsDir, { recursive: true });
|
|
526
|
+
const before = new Set(await fs.readdir(artifactsDir).catch(() => []));
|
|
527
|
+
const configured = await configureBrowserDownloadPath({
|
|
528
|
+
Browser: params.Browser,
|
|
529
|
+
Client: params.Client,
|
|
530
|
+
Page: params.Page,
|
|
531
|
+
logger: params.logger,
|
|
532
|
+
downloadPath: artifactsDir,
|
|
533
|
+
}).catch((error) => {
|
|
534
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
535
|
+
params.logger?.(`[browser] Failed to configure browser download path: ${message}`);
|
|
536
|
+
return false;
|
|
537
|
+
});
|
|
538
|
+
if (!configured) {
|
|
539
|
+
params.logger?.("[browser] Browser download path could not be configured; skipping button fallback.");
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
let clicked = [];
|
|
543
|
+
const expression = buildClickAssistantDownloadButtonsExpression(params.minTurnIndex, resolveDownloadButtonLabels(params.files ?? []), params.allowGenericDownloadLabels);
|
|
544
|
+
const deadline = Date.now() + DOWNLOAD_BUTTON_WAIT_MS;
|
|
545
|
+
while (Date.now() < deadline) {
|
|
546
|
+
const { result } = await params.Runtime.evaluate({
|
|
547
|
+
expression,
|
|
548
|
+
returnByValue: true,
|
|
549
|
+
});
|
|
550
|
+
clicked = Array.isArray(result?.value) ? result.value : [];
|
|
551
|
+
if (clicked.length > 0) {
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
555
|
+
}
|
|
556
|
+
if (clicked.length === 0) {
|
|
557
|
+
params.logger?.("[browser] No assistant download buttons found for button fallback.");
|
|
558
|
+
return [];
|
|
559
|
+
}
|
|
560
|
+
params.logger?.(`[browser] Clicked ${clicked.length} assistant download button(s).`);
|
|
561
|
+
const downloaded = await waitForCompletedDownloadFiles(artifactsDir, before, clicked.length);
|
|
562
|
+
return Promise.all(downloaded.map(async (filePath) => {
|
|
563
|
+
const filename = path.basename(filePath);
|
|
564
|
+
const stat = await fs.stat(filePath);
|
|
565
|
+
return {
|
|
566
|
+
kind: "file",
|
|
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
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
async function fetchDownloadWithNode(downloadUrl, getCookieHeader) {
|
|
579
|
+
let currentUrl = new URL(downloadUrl);
|
|
580
|
+
for (let redirects = 0; redirects <= DOWNLOAD_REDIRECT_LIMIT; redirects += 1) {
|
|
581
|
+
const headers = { "user-agent": "Mozilla/5.0" };
|
|
582
|
+
if (currentUrl.protocol === "https:" &&
|
|
583
|
+
!currentUrl.port &&
|
|
584
|
+
isAllowedChatGptHost(currentUrl.hostname) &&
|
|
585
|
+
isKnownChatGptFileDownloadUrl(currentUrl)) {
|
|
586
|
+
const cookieHeader = await getCookieHeader(currentUrl.href);
|
|
587
|
+
if (!cookieHeader) {
|
|
588
|
+
throw new Error("Missing ChatGPT cookies for file download.");
|
|
589
|
+
}
|
|
590
|
+
headers.cookie = cookieHeader;
|
|
591
|
+
}
|
|
592
|
+
const response = await fetch(currentUrl, {
|
|
593
|
+
headers,
|
|
594
|
+
redirect: "manual",
|
|
595
|
+
});
|
|
596
|
+
if (response.status >= 300 && response.status < 400) {
|
|
597
|
+
const location = response.headers.get("location");
|
|
598
|
+
if (!location) {
|
|
599
|
+
throw new Error(`download redirect missing location: ${response.status}`);
|
|
600
|
+
}
|
|
601
|
+
const redirectedUrl = new URL(location, currentUrl);
|
|
602
|
+
if (redirectedUrl.protocol !== "https:") {
|
|
603
|
+
throw new Error(`download redirect rejected: ${redirectedUrl.protocol}`);
|
|
604
|
+
}
|
|
605
|
+
currentUrl = redirectedUrl;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (!response.ok) {
|
|
609
|
+
throw new Error(`download failed: ${response.status} ${response.statusText}`);
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
buffer: Buffer.from(await response.arrayBuffer()),
|
|
613
|
+
contentDisposition: response.headers.get("content-disposition"),
|
|
614
|
+
contentType: response.headers.get("content-type"),
|
|
615
|
+
finalUrl: response.url,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
throw new Error(`download exceeded ${DOWNLOAD_REDIRECT_LIMIT} redirects`);
|
|
619
|
+
}
|
|
620
|
+
async function fetchDownloadWithBrowser(Runtime, downloadUrl) {
|
|
621
|
+
const expression = `(() => {
|
|
622
|
+
const downloadUrl = ${JSON.stringify(downloadUrl)};
|
|
623
|
+
const encodeBase64 = (bytes) => {
|
|
624
|
+
let binary = '';
|
|
625
|
+
const chunkSize = 0x8000;
|
|
626
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
627
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
|
|
628
|
+
}
|
|
629
|
+
return btoa(binary);
|
|
630
|
+
};
|
|
631
|
+
return fetch(downloadUrl, { credentials: 'include' }).then(async (response) => {
|
|
632
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
633
|
+
return {
|
|
634
|
+
ok: response.ok,
|
|
635
|
+
status: response.status,
|
|
636
|
+
statusText: response.statusText,
|
|
637
|
+
url: response.url,
|
|
638
|
+
contentDisposition: response.headers.get('content-disposition'),
|
|
639
|
+
contentType: response.headers.get('content-type'),
|
|
640
|
+
base64: encodeBase64(bytes),
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
})()`;
|
|
644
|
+
const evaluated = await Runtime.evaluate({
|
|
645
|
+
expression,
|
|
646
|
+
awaitPromise: true,
|
|
647
|
+
returnByValue: true,
|
|
648
|
+
});
|
|
649
|
+
const value = evaluated.result?.value;
|
|
650
|
+
if (!value) {
|
|
651
|
+
throw new Error("browser download returned no value");
|
|
652
|
+
}
|
|
653
|
+
if (!value.ok) {
|
|
654
|
+
throw new Error(`download failed: ${value.status ?? "?"} ${value.statusText ?? ""}`.trim());
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
buffer: Buffer.from(String(value.base64 ?? ""), "base64"),
|
|
658
|
+
contentDisposition: typeof value.contentDisposition === "string" ? value.contentDisposition : null,
|
|
659
|
+
contentType: typeof value.contentType === "string" ? value.contentType : null,
|
|
660
|
+
finalUrl: typeof value.url === "string" ? value.url : downloadUrl,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
export async function saveChatGptDownloadableFiles(params) {
|
|
664
|
+
const { Network, files, sessionId, logger } = params;
|
|
665
|
+
if (!files.length) {
|
|
666
|
+
return { saved: false, fileCount: 0, savedFiles: [], failedFiles: [], errors: [] };
|
|
667
|
+
}
|
|
668
|
+
const cookieHeaders = new Map();
|
|
669
|
+
const getCookieHeader = async (downloadUrl) => {
|
|
670
|
+
const origin = new URL(downloadUrl).origin;
|
|
671
|
+
const cached = cookieHeaders.get(origin);
|
|
672
|
+
if (cached !== undefined) {
|
|
673
|
+
return cached;
|
|
674
|
+
}
|
|
675
|
+
const cookieHeader = await buildCookieHeader(Network, downloadUrl);
|
|
676
|
+
cookieHeaders.set(origin, cookieHeader);
|
|
677
|
+
return cookieHeader;
|
|
678
|
+
};
|
|
679
|
+
const savedFiles = [];
|
|
680
|
+
const failedFiles = [];
|
|
681
|
+
const errors = [];
|
|
682
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
683
|
+
const file = files[index];
|
|
684
|
+
const explicitDownloadUrl = normalizeChatGptDownloadUrl(file.downloadUrl ?? file.url);
|
|
685
|
+
const sandboxDownloadUrl = downloadUrlFromSandboxUrl(file.sandboxUrl ?? file.url);
|
|
686
|
+
const downloadUrl = explicitDownloadUrl ?? sandboxDownloadUrl;
|
|
687
|
+
if (!downloadUrl) {
|
|
688
|
+
const source = file.sandboxUrl ?? file.filename ?? file.url;
|
|
689
|
+
errors.push(`${source}: no ChatGPT download URL found`);
|
|
690
|
+
failedFiles.push(file);
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const downloaded = params.Runtime && sandboxDownloadUrl && !explicitDownloadUrl
|
|
695
|
+
? await fetchDownloadWithBrowser(params.Runtime, downloadUrl)
|
|
696
|
+
: await fetchDownloadWithNode(downloadUrl, getCookieHeader);
|
|
697
|
+
const contentType = downloaded.contentType;
|
|
698
|
+
const filename = resolveDownloadedFilename({
|
|
699
|
+
file,
|
|
700
|
+
contentDisposition: downloaded.contentDisposition,
|
|
701
|
+
contentType,
|
|
702
|
+
index,
|
|
703
|
+
});
|
|
704
|
+
const artifact = await writeBinaryBrowserArtifact({
|
|
705
|
+
sessionId,
|
|
706
|
+
kind: "file",
|
|
707
|
+
filename,
|
|
708
|
+
contents: downloaded.buffer,
|
|
709
|
+
label: file.label || filename,
|
|
710
|
+
mimeType: contentType ?? file.mimeType,
|
|
711
|
+
sourceUrl: file.sandboxUrl ?? downloadUrl,
|
|
712
|
+
logger,
|
|
713
|
+
});
|
|
714
|
+
if (artifact) {
|
|
715
|
+
savedFiles.push({
|
|
716
|
+
...artifact,
|
|
717
|
+
kind: "file",
|
|
718
|
+
url: downloadUrl,
|
|
719
|
+
finalUrl: downloaded.finalUrl,
|
|
720
|
+
sandboxUrl: file.sandboxUrl,
|
|
721
|
+
filename,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
failedFiles.push(file);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
730
|
+
errors.push(`${file.filename ?? file.downloadUrl ?? file.url}: ${message}`);
|
|
731
|
+
failedFiles.push(file);
|
|
732
|
+
logger?.(`[browser] Failed to save downloadable file ${index + 1}/${files.length}: ${message}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
saved: savedFiles.length > 0,
|
|
737
|
+
fileCount: files.length,
|
|
738
|
+
savedFiles,
|
|
739
|
+
failedFiles,
|
|
740
|
+
errors,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
export async function collectChatGptFileArtifacts(params) {
|
|
744
|
+
const files = await readAssistantDownloadableFiles(params.Runtime, params.minTurnIndex ?? undefined).catch(() => []);
|
|
745
|
+
const textFiles = readTextDownloadableFiles(params.answerText);
|
|
746
|
+
if (textFiles.length > 0) {
|
|
747
|
+
params.logger?.(`[browser] Found ${textFiles.length} downloadable file link(s) in captured answer text.`);
|
|
748
|
+
}
|
|
749
|
+
const allFiles = dedupeFiles([...files, ...textFiles]);
|
|
750
|
+
if (allFiles.length === 0) {
|
|
751
|
+
return { files: [], savedFiles: [], fileCount: 0 };
|
|
752
|
+
}
|
|
753
|
+
params.logger?.(`[browser] Found ${allFiles.length} downloadable file candidate(s).`);
|
|
754
|
+
const saved = await saveChatGptDownloadableFiles({
|
|
755
|
+
Network: params.Network,
|
|
756
|
+
Runtime: params.Runtime,
|
|
757
|
+
files: allFiles,
|
|
758
|
+
sessionId: params.sessionId,
|
|
759
|
+
logger: params.logger,
|
|
760
|
+
});
|
|
761
|
+
const buttonSavedFiles = saved.failedFiles.length > 0
|
|
762
|
+
? await saveAssistantDownloadButtonArtifacts({
|
|
763
|
+
Browser: params.Browser,
|
|
764
|
+
Client: params.Client,
|
|
765
|
+
Page: params.Page,
|
|
766
|
+
Runtime: params.Runtime,
|
|
767
|
+
logger: params.logger,
|
|
768
|
+
files: saved.failedFiles,
|
|
769
|
+
allowGenericDownloadLabels: saved.savedFiles.length === 0,
|
|
770
|
+
minTurnIndex: params.minTurnIndex,
|
|
771
|
+
sessionId: params.sessionId,
|
|
772
|
+
})
|
|
773
|
+
: [];
|
|
774
|
+
const savedFiles = [...saved.savedFiles, ...buttonSavedFiles];
|
|
775
|
+
if (savedFiles.length === 0 && !saved.saved) {
|
|
776
|
+
const detail = saved.errors.length > 0 ? `\n${saved.errors.join("\n")}` : "";
|
|
777
|
+
params.logger?.(`[browser] Auto-save for downloadable files failed; returning metadata only.${detail}`);
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
params.logger?.(`[browser] Saved ${savedFiles.length} downloadable file artifact(s).`);
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
files: allFiles,
|
|
784
|
+
savedFiles,
|
|
785
|
+
fileCount: allFiles.length,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
export const __test__ = {
|
|
789
|
+
buildAssistantDownloadableFilesExpression,
|
|
790
|
+
buildClickAssistantDownloadButtonsExpression,
|
|
791
|
+
downloadUrlFromSandboxUrl,
|
|
792
|
+
normalizeChatGptDownloadUrl,
|
|
793
|
+
normalizeSandboxPath,
|
|
794
|
+
normalizeSandboxUrl,
|
|
795
|
+
readTextDownloadableFiles,
|
|
796
|
+
resolveDownloadButtonLabels,
|
|
797
|
+
};
|