@steipete/oracle 0.14.0 → 0.15.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 +2 -2
- package/dist/bin/oracle-cli.js +10 -8
- package/dist/docs-site/browser-mode.html +3 -2
- package/dist/docs-site/cli-reference.html +1 -1
- package/dist/docs-site/mcp.html +1 -1
- package/dist/src/browser/actions/assistantResponse.js +2 -1
- package/dist/src/browser/actions/deepResearch.js +132 -61
- package/dist/src/browser/actions/modelSelection.js +388 -30
- package/dist/src/browser/actions/thinkingTime.js +303 -65
- package/dist/src/browser/artifacts.js +2 -8
- package/dist/src/browser/chatgptFiles.js +198 -49
- package/dist/src/browser/chatgptImages.js +126 -24
- package/dist/src/browser/chromeLifecycle.js +35 -4
- package/dist/src/browser/deepResearchResult.js +23 -0
- package/dist/src/browser/index.js +145 -19
- package/dist/src/browser/profileCopy.js +93 -0
- package/dist/src/browser/projectSourcesRunner.js +2 -1
- package/dist/src/browser/prompt.js +151 -22
- package/dist/src/cli/browserConfig.js +19 -2
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/options.js +8 -0
- package/dist/src/cli/sessionRunner.js +13 -7
- package/dist/src/mcp/tools/chatgptImage.js +8 -3
- package/dist/src/mcp/tools/consult.js +9 -8
- package/dist/src/mcp/types.js +11 -2
- package/dist/src/oracle/thinkingTime.js +40 -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 +6 -6
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -458,7 +458,7 @@ async function configureBrowserDownloadPath(params) {
|
|
|
458
458
|
}
|
|
459
459
|
return false;
|
|
460
460
|
}
|
|
461
|
-
function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabels = [], allowGenericDownloadLabels = true) {
|
|
461
|
+
function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabels = [], allowGenericDownloadLabels = true, options = {}) {
|
|
462
462
|
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
463
463
|
? Math.floor(minTurnIndex)
|
|
464
464
|
: -1;
|
|
@@ -466,12 +466,22 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
466
466
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
467
467
|
const expectedLabelsLiteral = JSON.stringify(expectedLabels);
|
|
468
468
|
const allowGenericDownloadLabelsLiteral = JSON.stringify(allowGenericDownloadLabels);
|
|
469
|
+
const markClickedLiteral = JSON.stringify(options.markClicked === true);
|
|
470
|
+
const maxClicksLiteral = typeof options.maxClicks === "number" &&
|
|
471
|
+
Number.isFinite(options.maxClicks) &&
|
|
472
|
+
options.maxClicks > 0
|
|
473
|
+
? Math.floor(options.maxClicks)
|
|
474
|
+
: 0;
|
|
469
475
|
return `(() => {
|
|
470
476
|
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
471
477
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
472
478
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
473
479
|
const EXPECTED_LABELS = ${expectedLabelsLiteral};
|
|
474
480
|
const ALLOW_GENERIC_DOWNLOAD_LABELS = ${allowGenericDownloadLabelsLiteral};
|
|
481
|
+
const MARK_CLICKED = ${markClickedLiteral};
|
|
482
|
+
const MAX_CLICKS = ${maxClicksLiteral};
|
|
483
|
+
const HAS_EXPECTED_LABELS = EXPECTED_LABELS.length > 0;
|
|
484
|
+
const CLICKED_ATTRIBUTE = 'data-oracle-download-clicked';
|
|
475
485
|
const isAssistantTurn = (node) => {
|
|
476
486
|
if (!(node instanceof HTMLElement)) return false;
|
|
477
487
|
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
@@ -482,33 +492,67 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
482
492
|
if (testId.includes('assistant')) return true;
|
|
483
493
|
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
484
494
|
};
|
|
495
|
+
const expectedFileButton = (button) => {
|
|
496
|
+
const text = (button.textContent || '').trim().toLowerCase();
|
|
497
|
+
return EXPECTED_LABELS.some((label) => {
|
|
498
|
+
const downloadLabel = 'download ' + label;
|
|
499
|
+
return text === label ||
|
|
500
|
+
text.startsWith(label + ' ') ||
|
|
501
|
+
text === downloadLabel ||
|
|
502
|
+
text.startsWith(downloadLabel + ' ');
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
const genericBehaviorButton = (button) => {
|
|
506
|
+
const text = (button.textContent || '').trim().toLowerCase();
|
|
507
|
+
return ALLOW_GENERIC_DOWNLOAD_LABELS && /^download\\b/.test(text);
|
|
508
|
+
};
|
|
509
|
+
const genericFallbackButton = (button) => {
|
|
510
|
+
if (!ALLOW_GENERIC_DOWNLOAD_LABELS) return false;
|
|
511
|
+
const text = (button.textContent || '').trim().toLowerCase();
|
|
512
|
+
const aria = (button.getAttribute('aria-label') || '').trim().toLowerCase();
|
|
513
|
+
const testId = (button.getAttribute('data-testid') || '').trim().toLowerCase();
|
|
514
|
+
return text === 'download' || aria === 'download' || testId === 'download-files-turn-action-button';
|
|
515
|
+
};
|
|
485
516
|
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
486
|
-
const
|
|
517
|
+
const expectedMatches = new Set();
|
|
518
|
+
const genericBehaviorMatches = new Set();
|
|
519
|
+
const genericFallbackMatches = new Set();
|
|
520
|
+
const genericAllMatches = new Set();
|
|
487
521
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
488
522
|
const turn = turns[index];
|
|
489
523
|
if (!isAssistantTurn(turn)) continue;
|
|
490
524
|
if (MIN_TURN_INDEX >= 0 && index < MIN_TURN_INDEX) continue;
|
|
491
525
|
const messageRoot = turn.querySelector(ASSISTANT_SELECTOR) || turn;
|
|
492
|
-
const buttons = Array.from(messageRoot.querySelectorAll('button'))
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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';
|
|
526
|
+
const buttons = Array.from(messageRoot.querySelectorAll('button'))
|
|
527
|
+
.filter((button) => !(MARK_CLICKED && button.getAttribute(CLICKED_ATTRIBUTE) === 'true'));
|
|
528
|
+
const behaviorButtons = buttons.filter((button) =>
|
|
529
|
+
String(button.className || '').includes('behavior-btn')
|
|
530
|
+
);
|
|
531
|
+
behaviorButtons.filter(expectedFileButton).forEach((button) => expectedMatches.add(button));
|
|
532
|
+
const genericBehavior = behaviorButtons.filter(genericBehaviorButton);
|
|
533
|
+
genericBehavior.forEach((button) => {
|
|
534
|
+
genericBehaviorMatches.add(button);
|
|
535
|
+
genericAllMatches.add(button);
|
|
507
536
|
});
|
|
508
|
-
|
|
537
|
+
if (genericBehavior.length === 0) {
|
|
538
|
+
buttons.filter(genericFallbackButton).forEach((button) => {
|
|
539
|
+
genericFallbackMatches.add(button);
|
|
540
|
+
genericAllMatches.add(button);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
509
543
|
}
|
|
510
|
-
const
|
|
511
|
-
|
|
544
|
+
const selected = expectedMatches.size > 0
|
|
545
|
+
? expectedMatches
|
|
546
|
+
: HAS_EXPECTED_LABELS
|
|
547
|
+
? genericBehaviorMatches.size > 0
|
|
548
|
+
? genericBehaviorMatches
|
|
549
|
+
: genericFallbackMatches
|
|
550
|
+
: genericAllMatches;
|
|
551
|
+
const selectedButtons = Array.from(selected).slice(0, MAX_CLICKS > 0 ? MAX_CLICKS : undefined);
|
|
552
|
+
selectedButtons.forEach((button) => {
|
|
553
|
+
if (MARK_CLICKED) button.setAttribute(CLICKED_ATTRIBUTE, 'true');
|
|
554
|
+
button.click();
|
|
555
|
+
});
|
|
512
556
|
return selectedButtons.map((button) => ({
|
|
513
557
|
text: (button.textContent || '').trim(),
|
|
514
558
|
ariaLabel: button.getAttribute('aria-label') || '',
|
|
@@ -516,6 +560,80 @@ function buildClickAssistantDownloadButtonsExpression(minTurnIndex, expectedLabe
|
|
|
516
560
|
}));
|
|
517
561
|
})()`;
|
|
518
562
|
}
|
|
563
|
+
function describeDownloadableFile(file) {
|
|
564
|
+
return (file.filename ??
|
|
565
|
+
file.label ??
|
|
566
|
+
filenameFromUrl(file.sandboxUrl) ??
|
|
567
|
+
filenameFromUrl(file.downloadUrl) ??
|
|
568
|
+
filenameFromUrl(file.url) ??
|
|
569
|
+
file.sandboxUrl ??
|
|
570
|
+
file.downloadUrl ??
|
|
571
|
+
file.url);
|
|
572
|
+
}
|
|
573
|
+
function expectedDownloadedFilename(file) {
|
|
574
|
+
const filename = file.filename ??
|
|
575
|
+
filenameFromUrl(file.sandboxUrl) ??
|
|
576
|
+
filenameFromUrl(file.downloadUrl) ??
|
|
577
|
+
filenameFromUrl(file.url);
|
|
578
|
+
const basename = path.basename(String(filename ?? "").trim());
|
|
579
|
+
return basename && basename !== "." ? basename : undefined;
|
|
580
|
+
}
|
|
581
|
+
async function moveDownloadedFileToExpectedName(filePath, file) {
|
|
582
|
+
const filename = expectedDownloadedFilename(file);
|
|
583
|
+
if (!filename) {
|
|
584
|
+
return filePath;
|
|
585
|
+
}
|
|
586
|
+
const targetPath = path.join(path.dirname(filePath), filename);
|
|
587
|
+
if (path.resolve(targetPath) === path.resolve(filePath)) {
|
|
588
|
+
return filePath;
|
|
589
|
+
}
|
|
590
|
+
const expected = path.parse(filename);
|
|
591
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
592
|
+
const duplicatePattern = new RegExp(`^${escapeRegExp(expected.name)} ?\\(\\d+\\)${escapeRegExp(expected.ext)}$`);
|
|
593
|
+
if (!duplicatePattern.test(path.basename(filePath))) {
|
|
594
|
+
return filePath;
|
|
595
|
+
}
|
|
596
|
+
const targetExists = await fs
|
|
597
|
+
.stat(targetPath)
|
|
598
|
+
.then((stat) => stat.isFile())
|
|
599
|
+
.catch(() => false);
|
|
600
|
+
if (targetExists) {
|
|
601
|
+
return filePath;
|
|
602
|
+
}
|
|
603
|
+
await fs.rename(filePath, targetPath);
|
|
604
|
+
return targetPath;
|
|
605
|
+
}
|
|
606
|
+
async function clickAssistantDownloadButtons(params) {
|
|
607
|
+
const expression = buildClickAssistantDownloadButtonsExpression(params.minTurnIndex, params.expectedLabels ?? [], params.allowGenericDownloadLabels, { markClicked: params.markClicked, maxClicks: params.maxClicks });
|
|
608
|
+
const deadline = Date.now() + (params.timeoutMs ?? DOWNLOAD_BUTTON_WAIT_MS);
|
|
609
|
+
while (Date.now() < deadline) {
|
|
610
|
+
const { result } = await params.Runtime.evaluate({
|
|
611
|
+
expression,
|
|
612
|
+
returnByValue: true,
|
|
613
|
+
});
|
|
614
|
+
const clicked = Array.isArray(result?.value) ? result.value : [];
|
|
615
|
+
if (clicked.length > 0) {
|
|
616
|
+
return clicked;
|
|
617
|
+
}
|
|
618
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
619
|
+
}
|
|
620
|
+
return [];
|
|
621
|
+
}
|
|
622
|
+
async function savedBrowserFileFromPath(filePath) {
|
|
623
|
+
const filename = path.basename(filePath);
|
|
624
|
+
const stat = await fs.stat(filePath);
|
|
625
|
+
return {
|
|
626
|
+
kind: "file",
|
|
627
|
+
path: filePath,
|
|
628
|
+
label: filename,
|
|
629
|
+
mimeType: mimeTypeFromFilename(filename),
|
|
630
|
+
sizeBytes: stat.size,
|
|
631
|
+
sourceUrl: "browser-download",
|
|
632
|
+
url: "browser-download",
|
|
633
|
+
finalUrl: "browser-download",
|
|
634
|
+
filename,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
519
637
|
export async function saveAssistantDownloadButtonArtifacts(params) {
|
|
520
638
|
if ((!params.sessionId && !params.downloadPath) ||
|
|
521
639
|
(!params.Client && !params.Browser && !params.Page)) {
|
|
@@ -539,41 +657,72 @@ export async function saveAssistantDownloadButtonArtifacts(params) {
|
|
|
539
657
|
params.logger?.("[browser] Browser download path could not be configured; skipping button fallback.");
|
|
540
658
|
return [];
|
|
541
659
|
}
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
660
|
+
const buttonWaitMs = params.buttonWaitMs ?? DOWNLOAD_BUTTON_WAIT_MS;
|
|
661
|
+
const downloadWaitMs = params.downloadWaitMs ?? DOWNLOAD_BUTTON_WAIT_MS;
|
|
662
|
+
const expectedFiles = params.files ?? [];
|
|
663
|
+
if (expectedFiles.length === 0) {
|
|
664
|
+
const clicked = await clickAssistantDownloadButtons({
|
|
665
|
+
Runtime: params.Runtime,
|
|
666
|
+
minTurnIndex: params.minTurnIndex,
|
|
667
|
+
expectedLabels: [],
|
|
668
|
+
allowGenericDownloadLabels: params.allowGenericDownloadLabels,
|
|
669
|
+
timeoutMs: buttonWaitMs,
|
|
549
670
|
});
|
|
550
|
-
clicked
|
|
551
|
-
|
|
671
|
+
if (clicked.length === 0) {
|
|
672
|
+
params.logger?.("[browser] No assistant download buttons found for button fallback.");
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
params.logger?.(`[browser] Clicked ${clicked.length} assistant download button(s).`);
|
|
676
|
+
const downloaded = await waitForCompletedDownloadFiles(artifactsDir, before, clicked.length, downloadWaitMs);
|
|
677
|
+
return Promise.all(downloaded.map(savedBrowserFileFromPath));
|
|
678
|
+
}
|
|
679
|
+
let clickedCount = 0;
|
|
680
|
+
let knownEntries = before;
|
|
681
|
+
const downloadedPaths = [];
|
|
682
|
+
const missingFiles = [];
|
|
683
|
+
const unattemptedFiles = [];
|
|
684
|
+
for (const [fileIndex, file] of expectedFiles.entries()) {
|
|
685
|
+
const expectedLabels = resolveDownloadButtonLabels([file]);
|
|
686
|
+
const clicked = await clickAssistantDownloadButtons({
|
|
687
|
+
Runtime: params.Runtime,
|
|
688
|
+
minTurnIndex: params.minTurnIndex,
|
|
689
|
+
expectedLabels,
|
|
690
|
+
allowGenericDownloadLabels: params.allowGenericDownloadLabels === true,
|
|
691
|
+
markClicked: true,
|
|
692
|
+
maxClicks: 1,
|
|
693
|
+
timeoutMs: buttonWaitMs,
|
|
694
|
+
});
|
|
695
|
+
const displayName = describeDownloadableFile(file);
|
|
696
|
+
if (clicked.length === 0) {
|
|
697
|
+
missingFiles.push(displayName);
|
|
698
|
+
knownEntries = new Set(await fs.readdir(artifactsDir).catch(() => []));
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
clickedCount += clicked.length;
|
|
702
|
+
const downloaded = await waitForCompletedDownloadFiles(artifactsDir, knownEntries, 1, downloadWaitMs);
|
|
703
|
+
if (downloaded.length === 0) {
|
|
704
|
+
missingFiles.push(displayName);
|
|
705
|
+
unattemptedFiles.push(...expectedFiles.slice(fileIndex + 1).map(describeDownloadableFile));
|
|
706
|
+
missingFiles.push(...unattemptedFiles);
|
|
707
|
+
params.logger?.(`[browser] Download timed out for ${displayName}${unattemptedFiles.length > 0
|
|
708
|
+
? `; skipped remaining expected file(s) to avoid misassigning a late completion: ${unattemptedFiles.join(", ")}`
|
|
709
|
+
: ""}`);
|
|
552
710
|
break;
|
|
553
711
|
}
|
|
554
|
-
await
|
|
712
|
+
const normalizedDownloads = await Promise.all(downloaded.map((filePath, index) => index === 0 ? moveDownloadedFileToExpectedName(filePath, file) : filePath));
|
|
713
|
+
downloadedPaths.push(...normalizedDownloads);
|
|
714
|
+
knownEntries = new Set(await fs.readdir(artifactsDir).catch(() => []));
|
|
555
715
|
}
|
|
556
|
-
if (
|
|
716
|
+
if (clickedCount === 0) {
|
|
557
717
|
params.logger?.("[browser] No assistant download buttons found for button fallback.");
|
|
558
|
-
return [];
|
|
559
718
|
}
|
|
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
|
-
}));
|
|
719
|
+
else {
|
|
720
|
+
params.logger?.(`[browser] Clicked ${clickedCount} assistant download button(s).`);
|
|
721
|
+
}
|
|
722
|
+
if (missingFiles.length > 0) {
|
|
723
|
+
params.logger?.(`[browser] Download button fallback did not save expected file(s): ${missingFiles.join(", ")}`);
|
|
724
|
+
}
|
|
725
|
+
return Promise.all([...new Set(downloadedPaths)].map(savedBrowserFileFromPath));
|
|
577
726
|
}
|
|
578
727
|
async function fetchDownloadWithNode(downloadUrl, getCookieHeader) {
|
|
579
728
|
let currentUrl = new URL(downloadUrl);
|
|
@@ -9,6 +9,33 @@ import { resolveSessionArtifactsDir } from "./artifacts.js";
|
|
|
9
9
|
import { saveAssistantDownloadButtonArtifacts } from "./chatgptFiles.js";
|
|
10
10
|
const GENERATED_IMAGE_WAIT_MIN_MS = 15_000;
|
|
11
11
|
const GENERATED_IMAGE_WAIT_MAX_MS = 15 * 60_000;
|
|
12
|
+
const CHATGPT_GENERATED_IMAGE_BASE_URL = "https://chatgpt.com/";
|
|
13
|
+
function isAllowedChatGptHost(hostname) {
|
|
14
|
+
const value = hostname.toLowerCase();
|
|
15
|
+
return value === "chatgpt.com" || value === "chat.openai.com";
|
|
16
|
+
}
|
|
17
|
+
function normalizeGeneratedImageUrl(value) {
|
|
18
|
+
const raw = String(value ?? "").trim();
|
|
19
|
+
if (!raw)
|
|
20
|
+
return undefined;
|
|
21
|
+
let url;
|
|
22
|
+
try {
|
|
23
|
+
url = new URL(raw, CHATGPT_GENERATED_IMAGE_BASE_URL);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (url.protocol !== "https:" || url.port || !isAllowedChatGptHost(url.hostname)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (url.pathname !== "/backend-api/estuary/content") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (!(url.searchParams.get("id") ?? "").startsWith("file_")) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return url.href;
|
|
38
|
+
}
|
|
12
39
|
function extractFileId(url) {
|
|
13
40
|
try {
|
|
14
41
|
return new URL(url).searchParams.get("id") ?? undefined;
|
|
@@ -41,8 +68,12 @@ function buildAssistantImageExpression(minTurnIndex) {
|
|
|
41
68
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
42
69
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
43
70
|
const isGeneratedImage = (img) => {
|
|
44
|
-
const url = img?.src || '';
|
|
45
|
-
|
|
71
|
+
const url = new URL(img?.src || '', location.origin || 'https://chatgpt.com');
|
|
72
|
+
const host = url.hostname.toLowerCase();
|
|
73
|
+
if (url.protocol !== 'https:' || url.port) return false;
|
|
74
|
+
if (host !== 'chatgpt.com' && host !== 'chat.openai.com') return false;
|
|
75
|
+
if (url.pathname !== '/backend-api/estuary/content') return false;
|
|
76
|
+
if (!String(url.searchParams.get('id') || '').startsWith('file_')) return false;
|
|
46
77
|
const alt = String(img.alt || '').toLowerCase();
|
|
47
78
|
if (alt.includes('generated image')) return true;
|
|
48
79
|
let node = img;
|
|
@@ -104,13 +135,16 @@ export async function readAssistantGeneratedImages(Runtime, minTurnIndex) {
|
|
|
104
135
|
});
|
|
105
136
|
const raw = Array.isArray(result?.value) ? result.value : [];
|
|
106
137
|
const normalized = raw
|
|
107
|
-
.map((item) =>
|
|
108
|
-
url
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
.map((item) => {
|
|
139
|
+
const url = normalizeGeneratedImageUrl(typeof item?.url === "string" ? item.url : "");
|
|
140
|
+
return {
|
|
141
|
+
url: url ?? "",
|
|
142
|
+
alt: typeof item?.alt === "string" ? item.alt : undefined,
|
|
143
|
+
width: typeof item?.width === "number" ? item.width : undefined,
|
|
144
|
+
height: typeof item?.height === "number" ? item.height : undefined,
|
|
145
|
+
fileId: url ? extractFileId(url) : undefined,
|
|
146
|
+
};
|
|
147
|
+
})
|
|
114
148
|
.filter((item) => item.url.length > 0);
|
|
115
149
|
return dedupeImages(normalized);
|
|
116
150
|
}
|
|
@@ -209,8 +243,51 @@ async function buildCookieHeader(Network) {
|
|
|
209
243
|
.map((cookie) => `${cookie.name}=${cookie.value}`)
|
|
210
244
|
.join("; ");
|
|
211
245
|
}
|
|
246
|
+
async function fetchGeneratedImageInBrowserContext(Runtime, url) {
|
|
247
|
+
const expression = `
|
|
248
|
+
(async () => {
|
|
249
|
+
const url = ${JSON.stringify(url)};
|
|
250
|
+
const response = await fetch(url, { credentials: 'include', redirect: 'follow' });
|
|
251
|
+
const contentType = response.headers.get('content-type') || '';
|
|
252
|
+
const buffer = await response.arrayBuffer();
|
|
253
|
+
const bytes = new Uint8Array(buffer);
|
|
254
|
+
let binary = '';
|
|
255
|
+
for (let index = 0; index < bytes.length; index += 0x8000) {
|
|
256
|
+
binary += String.fromCharCode(...bytes.slice(index, index + 0x8000));
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
ok: response.ok,
|
|
260
|
+
status: response.status,
|
|
261
|
+
statusText: response.statusText,
|
|
262
|
+
contentType,
|
|
263
|
+
finalUrl: response.url,
|
|
264
|
+
b64: btoa(binary),
|
|
265
|
+
};
|
|
266
|
+
})()
|
|
267
|
+
`;
|
|
268
|
+
const { result, exceptionDetails } = await Runtime.evaluate({
|
|
269
|
+
expression,
|
|
270
|
+
awaitPromise: true,
|
|
271
|
+
returnByValue: true,
|
|
272
|
+
timeout: 120_000,
|
|
273
|
+
});
|
|
274
|
+
if (exceptionDetails) {
|
|
275
|
+
throw new Error("browser-context fetch threw an exception");
|
|
276
|
+
}
|
|
277
|
+
const value = result?.value;
|
|
278
|
+
if (!value?.ok || typeof value.b64 !== "string") {
|
|
279
|
+
const status = typeof value?.status === "number" ? value.status : "unknown";
|
|
280
|
+
const statusText = typeof value?.statusText === "string" ? value.statusText : "";
|
|
281
|
+
throw new Error(`browser-context fetch failed: ${status} ${statusText}`.trim());
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
buffer: Buffer.from(value.b64, "base64"),
|
|
285
|
+
contentType: typeof value.contentType === "string" ? value.contentType : null,
|
|
286
|
+
finalUrl: typeof value.finalUrl === "string" && value.finalUrl ? value.finalUrl : url,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
212
289
|
export async function saveChatGptGeneratedImages(params) {
|
|
213
|
-
const { Network, images, outputPath, logger } = params;
|
|
290
|
+
const { Network, Runtime, images, outputPath, logger } = params;
|
|
214
291
|
if (!images.length)
|
|
215
292
|
return { saved: false, imageCount: 0, savedImages: [], errors: [] };
|
|
216
293
|
const cookieHeader = await buildCookieHeader(Network);
|
|
@@ -228,20 +305,41 @@ export async function saveChatGptGeneratedImages(params) {
|
|
|
228
305
|
for (let index = 0; index < images.length; index += 1) {
|
|
229
306
|
const image = images[index];
|
|
230
307
|
try {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
308
|
+
const imageUrl = normalizeGeneratedImageUrl(image.url);
|
|
309
|
+
if (!imageUrl) {
|
|
310
|
+
throw new Error("rejected non-ChatGPT generated image URL");
|
|
311
|
+
}
|
|
312
|
+
let contentType = null;
|
|
313
|
+
let finalUrl = imageUrl;
|
|
314
|
+
let buffer;
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetch(imageUrl, {
|
|
317
|
+
headers: {
|
|
318
|
+
cookie: cookieHeader,
|
|
319
|
+
"user-agent": "Mozilla/5.0",
|
|
320
|
+
},
|
|
321
|
+
redirect: "follow",
|
|
322
|
+
});
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
throw new Error(`download failed: ${response.status} ${response.statusText}`);
|
|
325
|
+
}
|
|
326
|
+
contentType = response.headers.get("content-type");
|
|
327
|
+
finalUrl = response.url;
|
|
328
|
+
buffer = Buffer.from(await response.arrayBuffer());
|
|
329
|
+
}
|
|
330
|
+
catch (downloadError) {
|
|
331
|
+
if (!Runtime) {
|
|
332
|
+
throw downloadError;
|
|
333
|
+
}
|
|
334
|
+
const message = downloadError instanceof Error ? downloadError.message : String(downloadError);
|
|
335
|
+
logger?.(`[browser] ChatGPT generated image download failed via Node fetch; retrying in browser context (${image.fileId ?? imageUrl}: ${message}).`);
|
|
336
|
+
const browserFetch = await fetchGeneratedImageInBrowserContext(Runtime, imageUrl);
|
|
337
|
+
contentType = browserFetch.contentType;
|
|
338
|
+
finalUrl = browserFetch.finalUrl;
|
|
339
|
+
buffer = browserFetch.buffer;
|
|
240
340
|
}
|
|
241
|
-
const contentType = response.headers.get("content-type");
|
|
242
341
|
const extension = contentTypeToExtension(contentType);
|
|
243
342
|
const targetPath = resolveSiblingImagePath(path.resolve(outputPath), index, extension);
|
|
244
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
245
343
|
await fs.writeFile(targetPath, buffer);
|
|
246
344
|
savedImages.push({
|
|
247
345
|
kind: "image",
|
|
@@ -249,9 +347,9 @@ export async function saveChatGptGeneratedImages(params) {
|
|
|
249
347
|
label: index === 0 ? "Generated image" : `Generated image ${index + 1}`,
|
|
250
348
|
mimeType: contentType ?? undefined,
|
|
251
349
|
sizeBytes: buffer.length,
|
|
252
|
-
sourceUrl:
|
|
253
|
-
url:
|
|
254
|
-
finalUrl
|
|
350
|
+
sourceUrl: imageUrl,
|
|
351
|
+
url: imageUrl,
|
|
352
|
+
finalUrl,
|
|
255
353
|
alt: image.alt,
|
|
256
354
|
width: image.width,
|
|
257
355
|
height: image.height,
|
|
@@ -336,6 +434,7 @@ export async function collectGeneratedImageArtifacts(params) {
|
|
|
336
434
|
let generatedImages = await readAssistantGeneratedImagesWithFallback(params.Runtime, params.minTurnIndex ?? undefined);
|
|
337
435
|
let latestAnswerText = params.answerText;
|
|
338
436
|
if (explicitTargetPath && generatedImages.length === 0) {
|
|
437
|
+
await params.checkBlockingUiWarning?.();
|
|
339
438
|
const targetPath = path.resolve(explicitTargetPath);
|
|
340
439
|
const buttonImages = await saveGeneratedImageButtonArtifacts({
|
|
341
440
|
Browser: params.Browser,
|
|
@@ -352,6 +451,7 @@ export async function collectGeneratedImageArtifacts(params) {
|
|
|
352
451
|
const deadline = Date.now() + resolveGeneratedImageWaitTimeoutMs(params.waitTimeoutMs);
|
|
353
452
|
while (Date.now() < deadline) {
|
|
354
453
|
await delay(1500);
|
|
454
|
+
await params.checkBlockingUiWarning?.();
|
|
355
455
|
generatedImages = await readAssistantGeneratedImagesWithFallback(params.Runtime, params.minTurnIndex ?? undefined);
|
|
356
456
|
if (generatedImages.length > 0) {
|
|
357
457
|
break;
|
|
@@ -363,6 +463,7 @@ export async function collectGeneratedImageArtifacts(params) {
|
|
|
363
463
|
}
|
|
364
464
|
}
|
|
365
465
|
if (generatedImages.length === 0) {
|
|
466
|
+
await params.checkBlockingUiWarning?.();
|
|
366
467
|
const delayedButtonImages = await saveGeneratedImageButtonArtifacts({
|
|
367
468
|
Browser: params.Browser,
|
|
368
469
|
Client: params.Client,
|
|
@@ -396,6 +497,7 @@ export async function collectGeneratedImageArtifacts(params) {
|
|
|
396
497
|
}
|
|
397
498
|
const saved = await saveChatGptGeneratedImages({
|
|
398
499
|
Network: params.Network,
|
|
500
|
+
Runtime: params.Runtime,
|
|
399
501
|
images: generatedImages,
|
|
400
502
|
outputPath: targetPath,
|
|
401
503
|
logger: params.logger,
|
|
@@ -15,20 +15,31 @@ export async function launchChrome(config, userDataDir, logger) {
|
|
|
15
15
|
const debugPort = config.debugPort ?? parseDebugPortEnv();
|
|
16
16
|
const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress);
|
|
17
17
|
const usePatchedLauncher = Boolean(connectHost && connectHost !== "127.0.0.1");
|
|
18
|
+
// copy-profile reuses a copied signed-in profile whose cookies are
|
|
19
|
+
// Keychain-encrypted, so it must launch with the real Keychain (not mocked):
|
|
20
|
+
// strip the keychain-mocking flags from both chrome-launcher's defaults and
|
|
21
|
+
// Oracle's set, and ignore the defaults so they aren't re-added.
|
|
22
|
+
const usingCopiedProfile = Boolean(config.copyProfileSource);
|
|
23
|
+
if (usingCopiedProfile && config.chromeProfile) {
|
|
24
|
+
chromeFlags.push(`--profile-directory=${config.chromeProfile}`);
|
|
25
|
+
}
|
|
26
|
+
const launchOptions = resolveChromeLaunchOptions(chromeFlags, usingCopiedProfile);
|
|
18
27
|
const launcher = usePatchedLauncher
|
|
19
28
|
? await launchWithCustomHost({
|
|
20
|
-
chromeFlags,
|
|
29
|
+
chromeFlags: launchOptions.chromeFlags,
|
|
21
30
|
chromePath: config.chromePath ?? undefined,
|
|
22
31
|
userDataDir,
|
|
23
32
|
host: connectHost ?? "127.0.0.1",
|
|
24
33
|
requestedPort: debugPort ?? undefined,
|
|
34
|
+
ignoreDefaultFlags: launchOptions.ignoreDefaultFlags,
|
|
25
35
|
})
|
|
26
36
|
: await launch({
|
|
27
37
|
chromePath: config.chromePath ?? undefined,
|
|
28
|
-
chromeFlags,
|
|
38
|
+
chromeFlags: launchOptions.chromeFlags,
|
|
29
39
|
userDataDir,
|
|
30
40
|
handleSIGINT: false,
|
|
31
41
|
port: debugPort ?? undefined,
|
|
42
|
+
ignoreDefaultFlags: launchOptions.ignoreDefaultFlags,
|
|
32
43
|
});
|
|
33
44
|
const pidLabel = typeof launcher.pid === "number" ? ` (pid ${launcher.pid})` : "";
|
|
34
45
|
const hostLabel = connectHost ? ` on ${connectHost}` : "";
|
|
@@ -44,10 +55,14 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
|
|
|
44
55
|
}
|
|
45
56
|
handling = true;
|
|
46
57
|
const inFlight = opts?.isInFlight?.() ?? false;
|
|
47
|
-
const
|
|
58
|
+
const forceCleanup = opts?.forceProfileCleanup ?? false;
|
|
59
|
+
const leaveRunning = (keepBrowser || inFlight) && !forceCleanup;
|
|
48
60
|
if (leaveRunning) {
|
|
49
61
|
logger(`Received ${signal}; leaving Chrome running${inFlight ? " (assistant response pending)" : ""}`);
|
|
50
62
|
}
|
|
63
|
+
else if (forceCleanup && (keepBrowser || inFlight)) {
|
|
64
|
+
logger(`Received ${signal}; terminating Chrome and removing the copied profile (copy-profile is not retained)`);
|
|
65
|
+
}
|
|
51
66
|
else {
|
|
52
67
|
logger(`Received ${signal}; terminating Chrome process`);
|
|
53
68
|
}
|
|
@@ -343,6 +358,9 @@ function createSessionBoundChromeClient(browser, sessionId) {
|
|
|
343
358
|
// Raw `send` here is the browser-level send (not session-bound), so callers
|
|
344
359
|
// that issue Target.* via `send` must pass this page session id explicitly to
|
|
345
360
|
// stay scoped to this tab (e.g. Deep Research OOPIF auto-attach).
|
|
361
|
+
// chrome-remote-interface defines `send` on the client prototype, so object
|
|
362
|
+
// spread does not preserve it. Bind it explicitly for raw session commands.
|
|
363
|
+
send: typeof browser.send === "function" ? browser.send.bind(browser) : undefined,
|
|
346
364
|
oraclePageSessionId: sessionId,
|
|
347
365
|
Network: bindDomain("Network"),
|
|
348
366
|
Page: bindDomain("Page"),
|
|
@@ -472,6 +490,18 @@ function buildChromeFlags(headless, debugBindAddress) {
|
|
|
472
490
|
}
|
|
473
491
|
return flags;
|
|
474
492
|
}
|
|
493
|
+
function resolveChromeLaunchOptions(chromeFlags, usingCopiedProfile) {
|
|
494
|
+
if (!usingCopiedProfile) {
|
|
495
|
+
return { chromeFlags, ignoreDefaultFlags: false };
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
chromeFlags: [...Launcher.defaultFlags(), ...chromeFlags].filter((flag) => flag !== "--use-mock-keychain" && flag !== "--password-store=basic"),
|
|
499
|
+
ignoreDefaultFlags: true,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
export function resolveChromeLaunchOptionsForTest(chromeFlags, usingCopiedProfile) {
|
|
503
|
+
return resolveChromeLaunchOptions(chromeFlags, usingCopiedProfile);
|
|
504
|
+
}
|
|
475
505
|
function parseDebugPortEnv() {
|
|
476
506
|
const raw = process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT;
|
|
477
507
|
if (!raw)
|
|
@@ -514,13 +544,14 @@ function isWsl() {
|
|
|
514
544
|
const release = os.release();
|
|
515
545
|
return release.toLowerCase().includes("microsoft");
|
|
516
546
|
}
|
|
517
|
-
async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, }) {
|
|
547
|
+
async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, ignoreDefaultFlags, }) {
|
|
518
548
|
const launcher = new Launcher({
|
|
519
549
|
chromePath: chromePath ?? undefined,
|
|
520
550
|
chromeFlags,
|
|
521
551
|
userDataDir,
|
|
522
552
|
handleSIGINT: false,
|
|
523
553
|
port: requestedPort ?? undefined,
|
|
554
|
+
ignoreDefaultFlags,
|
|
524
555
|
});
|
|
525
556
|
if (host) {
|
|
526
557
|
const patched = launcher;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function isDeepResearchIncompleteText(text) {
|
|
2
|
+
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3
|
+
const lines = text
|
|
4
|
+
.split(/\n+/)
|
|
5
|
+
.map((line) => line.trim())
|
|
6
|
+
.filter(Boolean);
|
|
7
|
+
const tailIsPlanningPanel = text.length <= 1_500 &&
|
|
8
|
+
lines.length >= 4 &&
|
|
9
|
+
lines.length <= 20 &&
|
|
10
|
+
/^update$/i.test(lines[1] ?? "") &&
|
|
11
|
+
/^stop research$/i.test(lines.at(-1) ?? "") &&
|
|
12
|
+
/^determining steps for creating a report(?:\.\.\.)?$/i.test(lines.at(-2) ?? "");
|
|
13
|
+
return (normalized === "called tool" ||
|
|
14
|
+
normalized === "used tool" ||
|
|
15
|
+
normalized === "użyto narzędzia" ||
|
|
16
|
+
normalized === "narzędzie wywołane" ||
|
|
17
|
+
normalized === "planning" ||
|
|
18
|
+
normalized === "researching" ||
|
|
19
|
+
normalized === "searching the web" ||
|
|
20
|
+
(text.trimStart().startsWith("<system-reminder>") &&
|
|
21
|
+
/<system-reminder>[\s\S]*#\s*plan mode\b/i.test(text)) ||
|
|
22
|
+
tailIsPlanningPanel);
|
|
23
|
+
}
|