@youdie006/prodex 0.21.0 → 0.21.2
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/chatgpt-browser.js +126 -12
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -1872,6 +1872,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
1872
1872
|
emitProgress("selecting", `attached ${uploaded.attached.join(", ")}`);
|
|
1873
1873
|
}
|
|
1874
1874
|
const toolLabels = (options.tools ?? []).map(resolveComposerToolLabel);
|
|
1875
|
+
const wantsDeepResearch = toolLabels.includes(DEEP_RESEARCH_TOOL_LABEL);
|
|
1875
1876
|
if (toolLabels.length > 0)
|
|
1876
1877
|
emitProgress("selecting", `tools=${toolLabels.join(", ")}`);
|
|
1877
1878
|
await insertComposerTextViaCdp(cdp, options.prompt, page, toolLabels);
|
|
@@ -1915,6 +1916,26 @@ export async function sendChatGptPrompt(options) {
|
|
|
1915
1916
|
}
|
|
1916
1917
|
}
|
|
1917
1918
|
dbgSend(`submit posted=${promptPosted} submitButtonFound=${submitButtonFound}`);
|
|
1919
|
+
// Deep research does not begin when the prompt posts: it shows a start
|
|
1920
|
+
// control with a countdown ring. Press it instead of trusting the timer -
|
|
1921
|
+
// a run left waiting sat with zero assistant messages for 30+ minutes.
|
|
1922
|
+
if (wantsDeepResearch) {
|
|
1923
|
+
const startDeadline = Date.now() + 60_000;
|
|
1924
|
+
let pressed = false;
|
|
1925
|
+
while (Date.now() < startDeadline) {
|
|
1926
|
+
const start = await cdp.evaluate(deepResearchStartButtonRectExpression());
|
|
1927
|
+
if (start.ok && start.x !== undefined && start.y !== undefined) {
|
|
1928
|
+
await dispatchMouseClickAt(cdp, start.x, start.y);
|
|
1929
|
+
pressed = true;
|
|
1930
|
+
emitProgress("selecting", "deep research started");
|
|
1931
|
+
break;
|
|
1932
|
+
}
|
|
1933
|
+
await sleep(1_000);
|
|
1934
|
+
}
|
|
1935
|
+
if (!pressed) {
|
|
1936
|
+
sendWarnings.push("deep_research_start_not_found: no start control appeared for the deep research run. If ChatGPT asked a clarifying question instead, answer it with a follow-up consult in the same thread.");
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1918
1939
|
}
|
|
1919
1940
|
finally {
|
|
1920
1941
|
cdp.close();
|
|
@@ -1977,13 +1998,38 @@ export async function sendChatGptPrompt(options) {
|
|
|
1977
1998
|
}
|
|
1978
1999
|
throw acceptanceTimeoutError({ timeoutMs, composerStillHasText, submitButtonFound });
|
|
1979
2000
|
}
|
|
2001
|
+
// Pin the conversation the prompt actually landed in. The browser is shared
|
|
2002
|
+
// (other agents, the user, tooling), and a tab that moves mid-wait made
|
|
2003
|
+
// prodex read a DIFFERENT conversation and save it as this consult's answer -
|
|
2004
|
+
// silently, with a receipt (caught live). Nothing about that is recoverable
|
|
2005
|
+
// after the fact, so the wait either stays on this thread or fails loudly.
|
|
2006
|
+
const pinnedThreadUrl = finalState?.url;
|
|
2007
|
+
let recoveredNavigations = 0;
|
|
1980
2008
|
const answerIsStable = createChatGptAnswerStabilityTracker();
|
|
1981
2009
|
while (Date.now() - started < timeoutMs) {
|
|
1982
2010
|
await sleep(1000);
|
|
1983
2011
|
try {
|
|
1984
2012
|
finalState = await evaluateOnPage(page, answerExpression());
|
|
2013
|
+
if (pinnedThreadUrl && finalState?.url && !chatGptUrlsReferToSameTarget(finalState.url, pinnedThreadUrl)) {
|
|
2014
|
+
if (recoveredNavigations >= 2) {
|
|
2015
|
+
throw new ChatGptBrowserBlockerError({
|
|
2016
|
+
code: "thread_navigated_away",
|
|
2017
|
+
message: "The browser tab was moved to a different ChatGPT conversation while this consult was waiting for its answer.",
|
|
2018
|
+
retryable: true,
|
|
2019
|
+
next_step: `Keep the dedicated browser on the consult thread, then fetch the answer with \`prodex pro browser recover --target-url ${pinnedThreadUrl}\`.`,
|
|
2020
|
+
thread: pinnedThreadUrl
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
recoveredNavigations += 1;
|
|
2024
|
+
sendWarnings.push(`thread_navigated_away_recovered: something moved the tab to another conversation mid-wait; prodex navigated back to ${pinnedThreadUrl}.`);
|
|
2025
|
+
await evaluateOnPage(page, `location.assign(${JSON.stringify(pinnedThreadUrl)})`);
|
|
2026
|
+
await sleep(3_000);
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
1985
2029
|
}
|
|
1986
|
-
catch {
|
|
2030
|
+
catch (error) {
|
|
2031
|
+
if (error instanceof ChatGptBrowserBlockerError)
|
|
2032
|
+
throw error;
|
|
1987
2033
|
// Transient CDP failure while the answer is streaming: retry. A throw here
|
|
1988
2034
|
// would discard an already-streamed partial answer and skip the salvage
|
|
1989
2035
|
// path below, so keep the last good state and poll again until timeout.
|
|
@@ -2564,10 +2610,31 @@ export async function attachFilesToComposer(cdp, absolutePaths, options = {}) {
|
|
|
2564
2610
|
// check has to ignore the token or it reads as leftover contamination.
|
|
2565
2611
|
// ---------------------------------------------------------------------------
|
|
2566
2612
|
const COMPOSER_TOOL_ALIASES = [
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2613
|
+
// menuText carries every string the menu row is known to render. ChatGPT
|
|
2614
|
+
// dropped the visible "Deep research" title at one point and left only its
|
|
2615
|
+
// description, which made a title-only lookup fail (measured live), so each
|
|
2616
|
+
// tool is found by title OR description.
|
|
2617
|
+
{
|
|
2618
|
+
label: "Deep research",
|
|
2619
|
+
aliases: ["deep research", "deep-research", "deepresearch", "deep", "research"],
|
|
2620
|
+
menuText: ["Deep research", "Get a detailed report"]
|
|
2621
|
+
},
|
|
2622
|
+
{
|
|
2623
|
+
label: "Web search",
|
|
2624
|
+
aliases: ["web search", "web-search", "websearch", "search", "web"],
|
|
2625
|
+
menuText: ["Web search", "Find real-time news and info"]
|
|
2626
|
+
},
|
|
2627
|
+
{
|
|
2628
|
+
label: "Create image",
|
|
2629
|
+
aliases: ["create image", "create-image", "image", "img"],
|
|
2630
|
+
menuText: ["Create image", "Visualize anything"]
|
|
2631
|
+
}
|
|
2570
2632
|
];
|
|
2633
|
+
/** Every string the tools menu may render for this tool. */
|
|
2634
|
+
export function composerToolMenuTexts(label) {
|
|
2635
|
+
const known = COMPOSER_TOOL_ALIASES.find((tool) => tool.label === label);
|
|
2636
|
+
return known ? [...known.menuText] : [label];
|
|
2637
|
+
}
|
|
2571
2638
|
/**
|
|
2572
2639
|
* Map what a caller typed to the label ChatGPT renders. An unknown value is
|
|
2573
2640
|
* passed through unchanged, so a tool ChatGPT adds tomorrow is reachable by
|
|
@@ -2594,11 +2661,11 @@ export function composerToolsButtonRectExpression() {
|
|
|
2594
2661
|
}
|
|
2595
2662
|
/** Click point for a tools-menu entry, matched by its visible label. */
|
|
2596
2663
|
export function composerToolEntryRectExpression(label) {
|
|
2597
|
-
const
|
|
2664
|
+
const candidatesJson = JSON.stringify(composerToolMenuTexts(label).map((text) => text.toLowerCase()));
|
|
2598
2665
|
return `(() => {${CLICK_POINT_SNIPPET}
|
|
2599
|
-
const
|
|
2666
|
+
const candidates = ${candidatesJson};
|
|
2600
2667
|
const leaves = [...document.querySelectorAll("div,span,button,a")].filter((el) => el.children.length === 0);
|
|
2601
|
-
const leaf = leaves.find((el) => (el.textContent || "").trim().toLowerCase()
|
|
2668
|
+
const leaf = leaves.find((el) => candidates.includes((el.textContent || "").trim().toLowerCase()));
|
|
2602
2669
|
if (!leaf) {
|
|
2603
2670
|
const available = [...new Set(leaves.map((el) => (el.textContent || "").trim()).filter((t) => t.length > 1 && t.length < 30))].slice(0, 20);
|
|
2604
2671
|
return { ok: false, reason: "tool not found in the composer tools menu", available };
|
|
@@ -2611,8 +2678,12 @@ export function composerToolEntryRectExpression(label) {
|
|
|
2611
2678
|
export function activeComposerToolsExpression(labels) {
|
|
2612
2679
|
const labelsJson = JSON.stringify(labels);
|
|
2613
2680
|
return `(() => {
|
|
2681
|
+
// The token normally lands inside the editor, but some builds render it as
|
|
2682
|
+
// a pill in the surrounding composer form - check both before deciding a
|
|
2683
|
+
// selection did not take.
|
|
2614
2684
|
const el = document.querySelector('#prompt-textarea,[contenteditable="true"]');
|
|
2615
|
-
const
|
|
2685
|
+
const form = el ? (el.closest("form") || el.parentElement) : null;
|
|
2686
|
+
const text = (el ? el.innerText || "" : "") + String.fromCharCode(10) + (form ? form.innerText || "" : "");
|
|
2616
2687
|
return { ok: true, active: ${labelsJson}.filter((label) => text.includes(label)) };
|
|
2617
2688
|
})()`;
|
|
2618
2689
|
}
|
|
@@ -2649,8 +2720,9 @@ export function composerTextStateExpression(expectedText, toolLabels = []) {
|
|
|
2649
2720
|
export async function enableComposerTools(cdp, labels) {
|
|
2650
2721
|
const enabled = [];
|
|
2651
2722
|
for (const label of labels) {
|
|
2652
|
-
const
|
|
2653
|
-
|
|
2723
|
+
const activeProbe = composerToolMenuTexts(label);
|
|
2724
|
+
const already = await cdp.evaluate(activeComposerToolsExpression(activeProbe));
|
|
2725
|
+
if ((already?.active ?? []).length > 0) {
|
|
2654
2726
|
enabled.push(label);
|
|
2655
2727
|
continue;
|
|
2656
2728
|
}
|
|
@@ -2678,12 +2750,33 @@ export async function enableComposerTools(cdp, labels) {
|
|
|
2678
2750
|
const activeDeadline = Date.now() + 8_000;
|
|
2679
2751
|
let active = false;
|
|
2680
2752
|
for (;;) {
|
|
2681
|
-
const state = await cdp.evaluate(activeComposerToolsExpression(
|
|
2682
|
-
active = (state?.active ?? []).
|
|
2753
|
+
const state = await cdp.evaluate(activeComposerToolsExpression(activeProbe));
|
|
2754
|
+
active = (state?.active ?? []).length > 0;
|
|
2683
2755
|
if (active || Date.now() >= activeDeadline)
|
|
2684
2756
|
break;
|
|
2685
2757
|
await sleep(250);
|
|
2686
2758
|
}
|
|
2759
|
+
if (!active) {
|
|
2760
|
+
// One retry: the menu can close on a click that lands as the popover is
|
|
2761
|
+
// still settling, which looks identical to a refused selection.
|
|
2762
|
+
const retryButton = await cdp.evaluate(composerToolsButtonRectExpression());
|
|
2763
|
+
if (retryButton.ok && retryButton.x !== undefined && retryButton.y !== undefined) {
|
|
2764
|
+
await dispatchMouseClickAt(cdp, retryButton.x, retryButton.y);
|
|
2765
|
+
await sleep(1_000);
|
|
2766
|
+
const retryEntry = await cdp.evaluate(composerToolEntryRectExpression(label));
|
|
2767
|
+
if (retryEntry.ok && retryEntry.x !== undefined && retryEntry.y !== undefined) {
|
|
2768
|
+
await dispatchMouseClickAt(cdp, retryEntry.x, retryEntry.y);
|
|
2769
|
+
const retryDeadline = Date.now() + 8_000;
|
|
2770
|
+
for (;;) {
|
|
2771
|
+
const state = await cdp.evaluate(activeComposerToolsExpression(activeProbe));
|
|
2772
|
+
active = (state?.active ?? []).length > 0;
|
|
2773
|
+
if (active || Date.now() >= retryDeadline)
|
|
2774
|
+
break;
|
|
2775
|
+
await sleep(250);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2687
2780
|
if (!active)
|
|
2688
2781
|
throw new Error(`Selected "${label}" but the composer never showed it as active.`);
|
|
2689
2782
|
enabled.push(label);
|
|
@@ -2778,6 +2871,27 @@ export function chunkComposerText(text, size = COMPOSER_INSERT_CHUNK_CHARS) {
|
|
|
2778
2871
|
chunks.push(current);
|
|
2779
2872
|
return chunks;
|
|
2780
2873
|
}
|
|
2874
|
+
/**
|
|
2875
|
+
* The start control a deep research run shows after the prompt posts. It has a
|
|
2876
|
+
* countdown ring and starts on its own eventually, but prodex must not depend
|
|
2877
|
+
* on that: a run left waiting produced zero assistant messages for 30+ minutes
|
|
2878
|
+
* (measured live), because nothing pressed it.
|
|
2879
|
+
*/
|
|
2880
|
+
export function deepResearchStartButtonRectExpression() {
|
|
2881
|
+
return `(() => {${CLICK_POINT_SNIPPET}
|
|
2882
|
+
const buttons = [...document.querySelectorAll('button,[role="button"]')];
|
|
2883
|
+
const target = buttons.find((b) => {
|
|
2884
|
+
const text = ((b.innerText || "") + " " + (b.getAttribute("aria-label") || "")).trim();
|
|
2885
|
+
// "Start dictation" and "Start Voice" live in the same composer and
|
|
2886
|
+
// would otherwise swallow this click - pressing the microphone instead
|
|
2887
|
+
// of starting the research (measured live).
|
|
2888
|
+
if (/dictation|voice|mic|음성|받아쓰기/i.test(text)) return false;
|
|
2889
|
+
return /^(start|시작)\\s*$|start research|시작하기|리서치 시작|조사 시작/i.test(text);
|
|
2890
|
+
});
|
|
2891
|
+
if (!target) return { ok: false, reason: "no deep research start button" };
|
|
2892
|
+
return clickPoint(target);
|
|
2893
|
+
})()`;
|
|
2894
|
+
}
|
|
2781
2895
|
export function submitExpression() {
|
|
2782
2896
|
return `(() => {
|
|
2783
2897
|
${composerExpressionHelpers()}
|