@steipete/oracle 0.19.0 → 0.20.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 +72 -29
- package/dist/src/browser/actions/deepResearch.js +168 -44
- package/dist/src/browser/actions/modelSelection.js +78 -1
- package/dist/src/browser/actions/promptComposer.js +3 -0
- package/dist/src/browser/actions/thinkingTime.js +49 -6
- package/dist/src/browser/actions/webSearch.js +96 -0
- package/dist/src/browser/chromeLifecycle.js +28 -16
- package/dist/src/browser/config.js +1 -1
- package/dist/src/browser/index.js +144 -49
- package/dist/src/browser/liveTabs.js +11 -4
- package/dist/src/browser/profileState.js +6 -1
- package/dist/src/browser/promptFingerprint.js +54 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattach.js +178 -109
- package/dist/src/browser/recoveryTarget.js +156 -0
- package/dist/src/browser/sessionRunner.js +33 -8
- package/dist/src/browser/tabLeaseRegistry.js +20 -0
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/cli/browserConfig.js +33 -3
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +38 -3
- package/dist/src/cli/detach.js +10 -1
- package/dist/src/cli/detachedSession.js +36 -0
- package/dist/src/cli/options.js +15 -0
- package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
- package/dist/src/cli/runOptions.js +19 -4
- package/dist/src/cli/sessionDisplay.js +15 -8
- package/dist/src/cli/sessionRunner.js +153 -41
- package/dist/src/mcp/tools/consult.js +2 -2
- package/dist/src/mcp/types.js +1 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +3 -0
- package/dist/src/remote/server.js +2 -0
- package/dist/src/sessionManager.js +8 -1
- 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 +10 -10
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -13,9 +13,10 @@ import { CHATGPT_URL } from "../src/browser/constants.js";
|
|
|
13
13
|
import { applyHelpStyling } from "../src/cli/help.js";
|
|
14
14
|
import { collectPaths, collectModelList, collectTextValues, parseFloatOption, parseIntOption, parseSearchOption, parseThinkingTimeOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from "../src/cli/options.js";
|
|
15
15
|
import { copyToClipboard } from "../src/cli/clipboard.js";
|
|
16
|
+
import { isGpt6ProAlias } from "../src/cli/browserConfig.js";
|
|
16
17
|
import { buildMarkdownBundle } from "../src/cli/markdownBundle.js";
|
|
17
|
-
import { shouldDetachSession, stopDetachedWorker } from "../src/cli/detach.js";
|
|
18
|
-
import { launchDetachedSession } from "../src/cli/detachedSession.js";
|
|
18
|
+
import { detachedCancellationExitCode, shouldDetachSession, shouldExitAfterTopLevelSigint, stopDetachedWorker, } from "../src/cli/detach.js";
|
|
19
|
+
import { clearDetachedSessionCancellation, detachedSessionCancellationPath, launchDetachedSession, requestDetachedSessionCancellation, waitForDetachedSessionCancellation, } from "../src/cli/detachedSession.js";
|
|
19
20
|
import { applyHiddenAliases } from "../src/cli/hiddenAliases.js";
|
|
20
21
|
import { isMediaFile } from "../src/browser/prompt.js";
|
|
21
22
|
import { formatCompactNumber } from "../src/cli/format.js";
|
|
@@ -36,6 +37,7 @@ import { isAzureOpenAICandidateModel, validateProviderRouting, } from "../src/or
|
|
|
36
37
|
import { buildSessionLifecycle, formatSessionLifecycleBlock } from "../src/cli/sessionLifecycle.js";
|
|
37
38
|
import { buildDetachedPerfTraceEnv, createPerfTrace, isTraceValueFlag, } from "../src/cli/perfTrace.js";
|
|
38
39
|
import { resolveBrowserFollowupReference } from "../src/cli/followup.js";
|
|
40
|
+
import { BrowserRunCancelledError } from "../src/oracle/errors.js";
|
|
39
41
|
const VERSION = getCliVersion();
|
|
40
42
|
const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
|
|
41
43
|
const LEGACY_FLAG_ALIASES = new Map([
|
|
@@ -225,15 +227,8 @@ program
|
|
|
225
227
|
.addOption(new Option("--models <models>", 'Comma-separated API model list to query in parallel (e.g., "gpt-5.5-pro,gemini-3-pro").')
|
|
226
228
|
.argParser(collectModelList)
|
|
227
229
|
.default([]))
|
|
228
|
-
.addOption(new Option("--reasoning-effort <effort>", "Reasoning effort for GPT-5.6 API models.").choices([
|
|
229
|
-
"
|
|
230
|
-
"low",
|
|
231
|
-
"medium",
|
|
232
|
-
"high",
|
|
233
|
-
"xhigh",
|
|
234
|
-
"max",
|
|
235
|
-
]))
|
|
236
|
-
.addOption(new Option("--reasoning-mode <mode>", 'Responses API reasoning execution mode for GPT-5.6 models ("standard" or "pro").').choices(["standard", "pro"]))
|
|
230
|
+
.addOption(new Option("--reasoning-effort <effort>", "Reasoning effort for GPT-6 Astra and GPT-5.6 API models (Astra requires low or higher).").choices(["none", "low", "medium", "high", "xhigh", "max"]))
|
|
231
|
+
.addOption(new Option("--reasoning-mode <mode>", 'Responses API reasoning execution mode for GPT-6 Astra and GPT-5.6 models ("standard" or "pro").').choices(["standard", "pro"]))
|
|
237
232
|
.addOption(new Option("-e, --engine <mode>", "Execution engine (api | browser). Browser engine: GPT models automate ChatGPT; Gemini models use a cookie-based client for gemini.google.com. If omitted, oracle picks api when OPENAI_API_KEY is set, otherwise browser.").choices(["api", "browser"]))
|
|
238
233
|
.addOption(new Option("--mode <mode>", "Alias for --engine (api | browser).")
|
|
239
234
|
.choices(["api", "browser"])
|
|
@@ -339,7 +334,7 @@ program
|
|
|
339
334
|
.addOption(new Option("--browser-thinking-time <level>", "Thinking time intensity for Thinking/Pro models: light, standard, extended, extra-high (Extra High), pro (Pro tier of the active model), heavy, or ChatGPT UI aliases.")
|
|
340
335
|
.argParser(parseThinkingTimeOption)
|
|
341
336
|
.hideHelp())
|
|
342
|
-
.addOption(new Option("--browser-research <mode>", "Browser research mode: deep activates
|
|
337
|
+
.addOption(new Option("--browser-research <mode>", "Browser research mode: search activates Web Search; deep activates Deep Research.").choices(["off", "search", "deep"]))
|
|
343
338
|
.addOption(new Option("--browser-archive <mode>", "Archive completed ChatGPT browser conversations after local artifacts are saved (auto archives successful non-project one-shots only).").choices(["auto", "always", "never"]))
|
|
344
339
|
.addOption(new Option("--browser-follow-up <prompt>", "Submit an additional prompt in the same ChatGPT browser conversation after the initial answer; repeat for multi-turn consults.")
|
|
345
340
|
.argParser(collectTextValues)
|
|
@@ -1064,9 +1059,13 @@ async function runRootCommand(options) {
|
|
|
1064
1059
|
options.baseUrl = userConfig.apiBaseUrl;
|
|
1065
1060
|
}
|
|
1066
1061
|
const providerMode = resolveApiProviderMode(options);
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1062
|
+
// Engine discovery must not apply API-only validation to browser aliases.
|
|
1063
|
+
const engineModelInputs = multiModelProvided
|
|
1064
|
+
? options.models
|
|
1065
|
+
: [normalizeModelOption(options.model) || DEFAULT_MODEL];
|
|
1066
|
+
const engineModels = Array.from(new Set(engineModelInputs.map((entry) => isGpt6ProAlias(entry) && !options.route && !options.preflight
|
|
1067
|
+
? "gpt-6-pro"
|
|
1068
|
+
: resolveApiModel(entry))));
|
|
1070
1069
|
if (options.route || options.preflight) {
|
|
1071
1070
|
const routeAzureEndpoint = firstNonEmpty(options.azureEndpoint, process.env.AZURE_OPENAI_ENDPOINT, userConfig.azure?.endpoint);
|
|
1072
1071
|
const configuredAzureForRoute = routeAzureEndpoint
|
|
@@ -1675,18 +1674,28 @@ async function waitForDetachedStartGate() {
|
|
|
1675
1674
|
}
|
|
1676
1675
|
}
|
|
1677
1676
|
async function attachToDetachedSession(sessionId, workerPid) {
|
|
1677
|
+
const cancellationMarker = sessionStore
|
|
1678
|
+
.getPaths(sessionId)
|
|
1679
|
+
.then((paths) => detachedSessionCancellationPath(paths.dir, workerPid));
|
|
1678
1680
|
let cancelled = false;
|
|
1681
|
+
let cancellationRequest;
|
|
1679
1682
|
const cancelWorker = () => {
|
|
1680
1683
|
cancelled = true;
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
catch (error) {
|
|
1684
|
+
cancellationRequest ??= cancellationMarker
|
|
1685
|
+
.then((markerPath) => requestDetachedSessionCancellation(markerPath))
|
|
1686
|
+
.catch((error) => {
|
|
1685
1687
|
const message = error instanceof Error ? error.message : String(error);
|
|
1686
|
-
console.error(chalk.red(`Unable to
|
|
1687
|
-
|
|
1688
|
+
console.error(chalk.red(`Unable to request cancellation from worker ${workerPid}: ${message}`));
|
|
1689
|
+
try {
|
|
1690
|
+
stopDetachedWorker(workerPid);
|
|
1691
|
+
}
|
|
1692
|
+
catch (stopError) {
|
|
1693
|
+
const stopMessage = stopError instanceof Error ? stopError.message : String(stopError);
|
|
1694
|
+
console.error(chalk.red(`Unable to stop detached worker ${workerPid}: ${stopMessage}`));
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1688
1697
|
};
|
|
1689
|
-
process.
|
|
1698
|
+
process.on("SIGINT", cancelWorker);
|
|
1690
1699
|
try {
|
|
1691
1700
|
const { attachSession } = await import("../src/cli/sessionDisplay.js");
|
|
1692
1701
|
await attachSession(sessionId, {
|
|
@@ -1697,9 +1706,15 @@ async function attachToDetachedSession(sessionId, workerPid) {
|
|
|
1697
1706
|
}
|
|
1698
1707
|
finally {
|
|
1699
1708
|
process.off("SIGINT", cancelWorker);
|
|
1700
|
-
|
|
1701
|
-
|
|
1709
|
+
await cancellationRequest;
|
|
1710
|
+
const finalStatus = (await sessionStore.readSession(sessionId).catch(() => null))?.status;
|
|
1711
|
+
if (cancellationRequest &&
|
|
1712
|
+
finalStatus &&
|
|
1713
|
+
["completed", "partial", "cancelled", "error"].includes(finalStatus)) {
|
|
1714
|
+
// The parent may publish its request after the worker's final cleanup.
|
|
1715
|
+
await clearDetachedSessionCancellation(await cancellationMarker);
|
|
1702
1716
|
}
|
|
1717
|
+
process.exitCode = detachedCancellationExitCode(cancelled, finalStatus, process.exitCode);
|
|
1703
1718
|
}
|
|
1704
1719
|
}
|
|
1705
1720
|
async function restartSession(sessionId, options) {
|
|
@@ -1881,6 +1896,10 @@ async function restartSession(sessionId, options) {
|
|
|
1881
1896
|
async function executeSession(sessionId) {
|
|
1882
1897
|
let metadata = null;
|
|
1883
1898
|
let writer = null;
|
|
1899
|
+
const cancellation = new AbortController();
|
|
1900
|
+
const stopCancellationMonitor = new AbortController();
|
|
1901
|
+
let cancellationMarker;
|
|
1902
|
+
let cancellationMonitor;
|
|
1884
1903
|
try {
|
|
1885
1904
|
metadata = await sessionStore.readSession(sessionId);
|
|
1886
1905
|
if (!metadata) {
|
|
@@ -1894,6 +1913,18 @@ async function executeSession(sessionId) {
|
|
|
1894
1913
|
const sessionMode = getSessionMode(metadata);
|
|
1895
1914
|
const browserConfig = getBrowserConfigFromMetadata(metadata);
|
|
1896
1915
|
writer = sessionStore.createLogWriter(sessionId);
|
|
1916
|
+
if (sessionMode === "browser") {
|
|
1917
|
+
const paths = await sessionStore.getPaths(sessionId);
|
|
1918
|
+
cancellationMarker = detachedSessionCancellationPath(paths.dir, process.pid);
|
|
1919
|
+
cancellationMonitor = waitForDetachedSessionCancellation({
|
|
1920
|
+
markerPath: cancellationMarker,
|
|
1921
|
+
signal: stopCancellationMonitor.signal,
|
|
1922
|
+
}).then((requested) => {
|
|
1923
|
+
if (requested) {
|
|
1924
|
+
cancellation.abort(new BrowserRunCancelledError("Browser run cancelled by the user."));
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1897
1928
|
const userConfig = (await loadUserConfig()).config;
|
|
1898
1929
|
const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
|
|
1899
1930
|
const { performSessionRun } = await import("../src/cli/sessionRunner.js");
|
|
@@ -1907,18 +1938,22 @@ async function executeSession(sessionId) {
|
|
|
1907
1938
|
write: writer.writeChunk,
|
|
1908
1939
|
version: VERSION,
|
|
1909
1940
|
notifications,
|
|
1941
|
+
signal: sessionMode === "browser" ? cancellation.signal : undefined,
|
|
1910
1942
|
});
|
|
1911
1943
|
}
|
|
1912
1944
|
catch (error) {
|
|
1913
|
-
|
|
1945
|
+
const cancelled = error instanceof BrowserRunCancelledError;
|
|
1946
|
+
process.exitCode = cancelled ? 130 : 1;
|
|
1914
1947
|
const message = error instanceof Error ? error.message : String(error);
|
|
1915
1948
|
if (!metadata) {
|
|
1916
|
-
|
|
1949
|
+
if (!cancelled)
|
|
1950
|
+
console.error(chalk.red(message));
|
|
1917
1951
|
return;
|
|
1918
1952
|
}
|
|
1919
|
-
|
|
1953
|
+
if (!cancelled)
|
|
1954
|
+
writer?.logLine(`ERROR: Detached session worker failed: ${message}`);
|
|
1920
1955
|
const latest = await sessionStore.readSession(sessionId).catch(() => null);
|
|
1921
|
-
if (latest && !["completed", "partial", "error"].includes(latest.status)) {
|
|
1956
|
+
if (latest && !["completed", "partial", "error", "cancelled"].includes(latest.status)) {
|
|
1922
1957
|
await sessionStore.updateSession(sessionId, {
|
|
1923
1958
|
status: "error",
|
|
1924
1959
|
completedAt: new Date().toISOString(),
|
|
@@ -1932,6 +1967,14 @@ async function executeSession(sessionId) {
|
|
|
1932
1967
|
}
|
|
1933
1968
|
}
|
|
1934
1969
|
finally {
|
|
1970
|
+
stopCancellationMonitor.abort();
|
|
1971
|
+
await cancellationMonitor?.catch((error) => {
|
|
1972
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1973
|
+
writer?.logLine(`ERROR: Detached cancellation monitor failed: ${message}`);
|
|
1974
|
+
});
|
|
1975
|
+
if (cancellationMarker) {
|
|
1976
|
+
await clearDetachedSessionCancellation(cancellationMarker).catch(() => undefined);
|
|
1977
|
+
}
|
|
1935
1978
|
writer?.stream.end();
|
|
1936
1979
|
}
|
|
1937
1980
|
}
|
|
@@ -2050,7 +2093,7 @@ async function main() {
|
|
|
2050
2093
|
console.log(chalk.yellow("\nCancelled."));
|
|
2051
2094
|
process.exitCode = 130;
|
|
2052
2095
|
// Browser/serve modes install their own SIGINT cleanup after this top-level handler.
|
|
2053
|
-
if (process.listenerCount("SIGINT")
|
|
2096
|
+
if (shouldExitAfterTopLevelSigint(process.listenerCount("SIGINT"))) {
|
|
2054
2097
|
process.exit(130);
|
|
2055
2098
|
}
|
|
2056
2099
|
};
|
|
@@ -79,65 +79,131 @@ async function waitForDeepResearchPill(Runtime, timeoutMs = 5000) {
|
|
|
79
79
|
* After prompt submission, waits for the research plan to appear and
|
|
80
80
|
* auto-confirm (~60s countdown + 10s safety margin).
|
|
81
81
|
*/
|
|
82
|
-
export async function waitForResearchPlanAutoConfirm(Runtime, logger, autoConfirmWaitMs = DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS) {
|
|
82
|
+
export async function waitForResearchPlanAutoConfirm(Runtime, logger, autoConfirmWaitMs = DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS, options) {
|
|
83
|
+
const ignoredTargetKeys = new Set(options?.ignoredTargetKeys ?? []);
|
|
84
|
+
const minTurnIndex = typeof options?.minTurnIndex === "number" && Number.isFinite(options.minTurnIndex)
|
|
85
|
+
? Math.floor(options.minTurnIndex)
|
|
86
|
+
: -1;
|
|
87
|
+
let capturedPlan = null;
|
|
88
|
+
let loggedPlan = false;
|
|
89
|
+
const targetOwnerMinTurnIndex = minTurnIndex >= 0 && options?.targetBaselineCaptured !== true ? minTurnIndex : -1;
|
|
90
|
+
const readPlanStatus = async () => {
|
|
91
|
+
const targetRead = options?.client
|
|
92
|
+
? ((await readDeepResearchTargetResult(options.client, ignoredTargetKeys, targetOwnerMinTurnIndex).catch(() => null))?.read ?? null)
|
|
93
|
+
: null;
|
|
94
|
+
if (targetRead?.planTitle || targetRead?.researchStarted) {
|
|
95
|
+
return targetRead;
|
|
96
|
+
}
|
|
97
|
+
const inPageRead = options?.Page
|
|
98
|
+
? await readDeepResearchFrameResult(Runtime, options.Page, options.client, minTurnIndex).catch(() => null)
|
|
99
|
+
: null;
|
|
100
|
+
return inPageRead?.read ?? targetRead;
|
|
101
|
+
};
|
|
102
|
+
const capturePlan = async (status) => {
|
|
103
|
+
if (!status.planTitle || !status.planSteps || status.planSteps.length === 0) {
|
|
104
|
+
return capturedPlan;
|
|
105
|
+
}
|
|
106
|
+
const next = {
|
|
107
|
+
title: status.planTitle,
|
|
108
|
+
steps: status.planSteps,
|
|
109
|
+
phase: status.researchStarted ? "researching" : "planning",
|
|
110
|
+
...(status.planActionText ? { actionText: status.planActionText } : {}),
|
|
111
|
+
capturedAt: capturedPlan?.capturedAt ?? new Date().toISOString(),
|
|
112
|
+
};
|
|
113
|
+
const changed = !capturedPlan ||
|
|
114
|
+
capturedPlan.phase !== next.phase ||
|
|
115
|
+
capturedPlan.title !== next.title ||
|
|
116
|
+
capturedPlan.steps.join("\n") !== next.steps.join("\n") ||
|
|
117
|
+
capturedPlan.actionText !== next.actionText;
|
|
118
|
+
capturedPlan = next;
|
|
119
|
+
if (!loggedPlan) {
|
|
120
|
+
logger(`[browser] Deep Research plan detected:\n${[next.title, ...next.steps.map((step, index) => `${index + 1}. ${step}`)].join("\n")}`);
|
|
121
|
+
loggedPlan = true;
|
|
122
|
+
}
|
|
123
|
+
if (changed) {
|
|
124
|
+
await options?.onPlan?.(next);
|
|
125
|
+
}
|
|
126
|
+
return next;
|
|
127
|
+
};
|
|
128
|
+
const reportResearchStarted = async () => {
|
|
129
|
+
if (capturedPlan?.phase === "planning") {
|
|
130
|
+
capturedPlan = { ...capturedPlan, phase: "researching" };
|
|
131
|
+
await options?.onPlan?.(capturedPlan);
|
|
132
|
+
}
|
|
133
|
+
logger("[browser] Deep Research execution started; plan countdown is complete.");
|
|
134
|
+
return capturedPlan;
|
|
135
|
+
};
|
|
83
136
|
// Phase A: Detect research plan appearance (up to 60s)
|
|
84
137
|
const planDeadline = Date.now() + 60_000;
|
|
85
138
|
let planDetected = false;
|
|
86
139
|
while (Date.now() < planDeadline) {
|
|
140
|
+
const frameStatus = await readPlanStatus();
|
|
141
|
+
if (frameStatus) {
|
|
142
|
+
await capturePlan(frameStatus);
|
|
143
|
+
if (frameStatus.researchStarted) {
|
|
144
|
+
return reportResearchStarted();
|
|
145
|
+
}
|
|
146
|
+
if (capturedPlan) {
|
|
147
|
+
planDetected = true;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Legacy/inline fallback. Do not treat an arbitrary large iframe as a plan:
|
|
152
|
+
// ChatGPT projects, attachments, and other tools also render large iframes.
|
|
87
153
|
const { result } = await Runtime.evaluate({
|
|
88
154
|
expression: `(() => {
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const rect = f.getBoundingClientRect();
|
|
92
|
-
return rect.width > 200 && rect.height > 200;
|
|
93
|
-
});
|
|
94
|
-
const assistantText = (document.querySelector('[data-message-author-role="assistant"]')?.textContent || '').toLowerCase();
|
|
155
|
+
const turns = Array.from(document.querySelectorAll('[data-message-author-role="assistant"]'));
|
|
156
|
+
const assistantText = String(turns.at(-1)?.textContent || '').toLowerCase();
|
|
95
157
|
const hasResearchText = assistantText.includes('researching') ||
|
|
96
158
|
assistantText.includes('research plan') ||
|
|
97
|
-
assistantText.includes('
|
|
98
|
-
assistantText.includes('
|
|
99
|
-
return {
|
|
159
|
+
assistantText.includes('正在研究') ||
|
|
160
|
+
assistantText.includes('研究计划');
|
|
161
|
+
return { hasResearchText };
|
|
100
162
|
})()`,
|
|
101
163
|
returnByValue: true,
|
|
102
164
|
});
|
|
103
165
|
const val = result?.value;
|
|
104
|
-
if (val?.
|
|
166
|
+
if (val?.hasResearchText) {
|
|
105
167
|
planDetected = true;
|
|
106
|
-
logger("Research
|
|
168
|
+
logger("[browser] Deep Research activity detected; waiting for plan auto-confirm...");
|
|
107
169
|
break;
|
|
108
170
|
}
|
|
109
171
|
await delay(2_000);
|
|
110
172
|
}
|
|
111
173
|
if (!planDetected) {
|
|
112
174
|
logger("Warning: Research plan not detected within 60s; continuing (may have auto-confirmed already)");
|
|
113
|
-
return;
|
|
175
|
+
return capturedPlan;
|
|
114
176
|
}
|
|
115
|
-
// Phase B: Wait for
|
|
177
|
+
// Phase B: Wait for the OOPIF's real execution state instead of sleeping for
|
|
178
|
+
// the full countdown. The main page cannot see this sandboxed iframe's text.
|
|
116
179
|
const confirmStart = Date.now();
|
|
117
180
|
while (Date.now() - confirmStart < autoConfirmWaitMs) {
|
|
181
|
+
const frameStatus = await readPlanStatus();
|
|
182
|
+
if (frameStatus) {
|
|
183
|
+
await capturePlan(frameStatus);
|
|
184
|
+
if (frameStatus.researchStarted) {
|
|
185
|
+
return reportResearchStarted();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
118
188
|
const { result } = await Runtime.evaluate({
|
|
119
189
|
expression: `(() => {
|
|
120
|
-
const iframes = document.querySelectorAll('iframe');
|
|
121
|
-
const hasLargeIframe = Array.from(iframes).some(f => {
|
|
122
|
-
const rect = f.getBoundingClientRect();
|
|
123
|
-
return rect.width > 200 && rect.height > 200;
|
|
124
|
-
});
|
|
125
190
|
const text = (document.body?.innerText || '').toLowerCase();
|
|
126
191
|
const isResearching = text.includes('researching...') ||
|
|
127
192
|
text.includes('reading sources') ||
|
|
128
|
-
text.includes('
|
|
129
|
-
|
|
193
|
+
text.includes('正在研究') ||
|
|
194
|
+
text.includes('正在阅读来源');
|
|
195
|
+
return { isResearching };
|
|
130
196
|
})()`,
|
|
131
197
|
returnByValue: true,
|
|
132
198
|
});
|
|
133
199
|
const val = result?.value;
|
|
134
200
|
if (val?.isResearching) {
|
|
135
|
-
|
|
136
|
-
return;
|
|
201
|
+
return reportResearchStarted();
|
|
137
202
|
}
|
|
138
|
-
await delay(
|
|
203
|
+
await delay(2_000);
|
|
139
204
|
}
|
|
140
|
-
logger("
|
|
205
|
+
logger("[browser] Deep Research plan wait elapsed; proceeding to monitor research progress.");
|
|
206
|
+
return capturedPlan;
|
|
141
207
|
}
|
|
142
208
|
/**
|
|
143
209
|
* Polls for Deep Research completion over 5-30+ minutes.
|
|
@@ -600,6 +666,44 @@ function buildDeepResearchFrameStatusExpression() {
|
|
|
600
666
|
return `(() => {
|
|
601
667
|
const rawText = document.body?.innerText || '';
|
|
602
668
|
const html = document.body?.innerHTML || '';
|
|
669
|
+
const cleanText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
670
|
+
const sections = typeof document.querySelectorAll === 'function'
|
|
671
|
+
? Array.from(document.querySelectorAll('section'))
|
|
672
|
+
: [];
|
|
673
|
+
const planSection = sections.find((section) => {
|
|
674
|
+
if (typeof section.querySelector !== 'function' ||
|
|
675
|
+
typeof section.querySelectorAll !== 'function' ||
|
|
676
|
+
!cleanText(section.querySelector('h2')?.textContent) ||
|
|
677
|
+
section.querySelectorAll('ul li').length === 0) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
const buttons = Array.from(section.querySelectorAll('button'));
|
|
681
|
+
const hasPlanAction = buttons.some((button) =>
|
|
682
|
+
/^(edit|update|编辑|更新)$/i.test(cleanText(button.textContent))
|
|
683
|
+
);
|
|
684
|
+
const hasResearchStatus = Boolean(section.querySelector('p.loading-shimmer')) ||
|
|
685
|
+
buttons.some((button) =>
|
|
686
|
+
/stop research|停止研究/i.test(cleanText(button.getAttribute?.('aria-label')))
|
|
687
|
+
);
|
|
688
|
+
return hasPlanAction || hasResearchStatus;
|
|
689
|
+
});
|
|
690
|
+
const planTitle = cleanText(planSection?.querySelector?.('h2')?.textContent);
|
|
691
|
+
const planSteps = planSection && typeof planSection.querySelectorAll === 'function'
|
|
692
|
+
? Array.from(planSection.querySelectorAll('ul li'))
|
|
693
|
+
.map((item) => cleanText(item.textContent))
|
|
694
|
+
.filter(Boolean)
|
|
695
|
+
: [];
|
|
696
|
+
const planActionText = planSection && typeof planSection.querySelectorAll === 'function'
|
|
697
|
+
? Array.from(planSection.querySelectorAll('button'))
|
|
698
|
+
.map((button) => cleanText(button.textContent))
|
|
699
|
+
.find((text) => /^(edit|update|编辑|更新)$/i.test(text)) || ''
|
|
700
|
+
: '';
|
|
701
|
+
const hasResearchShimmer = Boolean(planSection?.querySelector?.('p.loading-shimmer'));
|
|
702
|
+
const hasStopResearchControl = typeof planSection?.querySelectorAll === 'function' &&
|
|
703
|
+
Array.from(planSection.querySelectorAll('button')).some((button) =>
|
|
704
|
+
/stop research|停止研究/i.test(cleanText(button.getAttribute?.('aria-label')))
|
|
705
|
+
);
|
|
706
|
+
const researchStarted = planSteps.length > 0 && (hasResearchShimmer || hasStopResearchControl);
|
|
603
707
|
const isPlaceholder = (line) => /^(called tool|used tool|użyto narzędzia|narzędzie wywołane)$/i.test(line);
|
|
604
708
|
const isCompletionLine = (line) =>
|
|
605
709
|
/^(research completed|badanie ukończone)\\b/i.test(line);
|
|
@@ -641,6 +745,10 @@ function buildDeepResearchFrameStatusExpression() {
|
|
|
641
745
|
return {
|
|
642
746
|
completed,
|
|
643
747
|
inProgress,
|
|
748
|
+
researchStarted,
|
|
749
|
+
planTitle: planTitle || undefined,
|
|
750
|
+
planSteps: planSteps.length > 0 ? planSteps : undefined,
|
|
751
|
+
planActionText: planActionText || undefined,
|
|
644
752
|
textLength: reportText.length || rawText.trim().length,
|
|
645
753
|
text: completed ? reportText : undefined,
|
|
646
754
|
html: completed ? html : undefined,
|
|
@@ -773,9 +881,9 @@ export function buildDeepResearchCompletionPollExpressionForTest(minTurnIndex =
|
|
|
773
881
|
return buildDeepResearchCompletionPollExpression(minTurnIndex);
|
|
774
882
|
}
|
|
775
883
|
function buildFindDeepResearchPillExpression(functionName = "findDeepResearchPill") {
|
|
776
|
-
const
|
|
884
|
+
const pillLabels = JSON.stringify([DEEP_RESEARCH_PILL_LABEL, "深度研究"]);
|
|
777
885
|
return `const ${functionName} = () => {
|
|
778
|
-
const
|
|
886
|
+
const labels = ${pillLabels}.map(label => label.toLowerCase());
|
|
779
887
|
const selectors = [
|
|
780
888
|
'.__composer-pill-composite',
|
|
781
889
|
'.__composer-pill',
|
|
@@ -798,7 +906,7 @@ function buildFindDeepResearchPillExpression(functionName = "findDeepResearchPil
|
|
|
798
906
|
pill.querySelector('button')?.getAttribute('aria-label') ||
|
|
799
907
|
''
|
|
800
908
|
).toLowerCase();
|
|
801
|
-
if (text.includes(label) || aria.includes(label)) {
|
|
909
|
+
if (labels.some(label => text.includes(label) || aria.includes(label))) {
|
|
802
910
|
return pill;
|
|
803
911
|
}
|
|
804
912
|
}
|
|
@@ -819,6 +927,18 @@ function buildWaitForDeepResearchPillExpression(timeoutMs) {
|
|
|
819
927
|
function buildActivateDeepResearchExpression() {
|
|
820
928
|
const plusBtnSelector = JSON.stringify(DEEP_RESEARCH_PLUS_BUTTON);
|
|
821
929
|
const targetText = JSON.stringify(DEEP_RESEARCH_DROPDOWN_ITEM_TEXT);
|
|
930
|
+
const targetLabels = JSON.stringify([DEEP_RESEARCH_DROPDOWN_ITEM_TEXT, "深度研究"]);
|
|
931
|
+
const descriptionLabels = JSON.stringify(["Get a detailed report", "获取详细报告"]);
|
|
932
|
+
const addFilesLabels = JSON.stringify(["add files", "添加文件"]);
|
|
933
|
+
const dropdownReadyLabels = JSON.stringify([
|
|
934
|
+
"add photos",
|
|
935
|
+
"create image",
|
|
936
|
+
"web search",
|
|
937
|
+
"deep research",
|
|
938
|
+
"get a detailed report",
|
|
939
|
+
"深度研究",
|
|
940
|
+
"获取详细报告",
|
|
941
|
+
]);
|
|
822
942
|
return `(async () => {
|
|
823
943
|
${buildClickDispatcher()}
|
|
824
944
|
${buildFindDeepResearchPillExpression()}
|
|
@@ -864,8 +984,12 @@ function buildActivateDeepResearchExpression() {
|
|
|
864
984
|
'[data-radix-popper-content-wrapper]',
|
|
865
985
|
'[data-floating-ui-portal]',
|
|
866
986
|
].join(',');
|
|
867
|
-
const target = ${targetText}.toLowerCase();
|
|
868
987
|
const normalizeText = (value) => String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
|
|
988
|
+
const compactText = (value) => normalizeText(value).replace(/\\s+/g, '');
|
|
989
|
+
const targetLabels = ${targetLabels}.map(normalizeText);
|
|
990
|
+
const descriptionLabels = ${descriptionLabels}.map(normalizeText);
|
|
991
|
+
const addFilesLabels = ${addFilesLabels}.map(normalizeText);
|
|
992
|
+
const dropdownReadyLabels = ${dropdownReadyLabels}.map(normalizeText);
|
|
869
993
|
const getText = (item) => normalizeText(item.textContent || item.getAttribute?.('aria-label') || '');
|
|
870
994
|
const isInPopover = (item) => Boolean(item.closest?.(popoverSelector));
|
|
871
995
|
const isVisible = (item) => {
|
|
@@ -891,14 +1015,16 @@ function buildActivateDeepResearchExpression() {
|
|
|
891
1015
|
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
|
|
892
1016
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
893
1017
|
};
|
|
894
|
-
const isDeepResearchText = (text) =>
|
|
895
|
-
|
|
896
|
-
text.startsWith(
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1018
|
+
const isDeepResearchText = (text) => {
|
|
1019
|
+
const compact = compactText(text);
|
|
1020
|
+
const exactLabel = targetLabels.some(label => compact === compactText(label) || text.startsWith(label + ' '));
|
|
1021
|
+
const exactDescription = descriptionLabels.some(
|
|
1022
|
+
label => text === label || text.startsWith(label + ' ')
|
|
1023
|
+
);
|
|
1024
|
+
const combinedLabel = targetLabels.some(label => compact.startsWith(compactText(label))) &&
|
|
1025
|
+
descriptionLabels.some(label => compact.includes(compactText(label)));
|
|
1026
|
+
return exactLabel || exactDescription || combinedLabel;
|
|
1027
|
+
};
|
|
902
1028
|
const getClickableItem = (item) => item.closest?.(
|
|
903
1029
|
'[data-radix-collection-item], [role="option"], [cmdk-item], button, [role="menuitem"], [role="menuitemradio"], .__menu-item, [class*="__menu-item"], [class*="menu-item"]'
|
|
904
1030
|
) || item;
|
|
@@ -915,7 +1041,7 @@ function buildActivateDeepResearchExpression() {
|
|
|
915
1041
|
.map(item => {
|
|
916
1042
|
const text = getText(item);
|
|
917
1043
|
const clickable = getClickableItem(item);
|
|
918
|
-
const exact = text
|
|
1044
|
+
const exact = targetLabels.includes(text) ? 0 : 1;
|
|
919
1045
|
const menuRow = /(^|\\s)__menu-item(\\s|$)/.test(clickable.className || '') ? 0 : 1;
|
|
920
1046
|
return { item: clickable, score: exact + menuRow, textLength: text.length };
|
|
921
1047
|
})
|
|
@@ -946,7 +1072,9 @@ function buildActivateDeepResearchExpression() {
|
|
|
946
1072
|
// mutate the main composer and can be submitted as normal prompt text.
|
|
947
1073
|
const plusBtn = document.querySelector(${plusBtnSelector}) ||
|
|
948
1074
|
Array.from(document.querySelectorAll('button')).find(
|
|
949
|
-
b =>
|
|
1075
|
+
b => addFilesLabels.some(label =>
|
|
1076
|
+
normalizeText(b.getAttribute('aria-label') || '').includes(label)
|
|
1077
|
+
)
|
|
950
1078
|
);
|
|
951
1079
|
if (!plusBtn) return { status: 'plus-button-missing' };
|
|
952
1080
|
dispatchClickSequence(plusBtn);
|
|
@@ -958,11 +1086,7 @@ function buildActivateDeepResearchExpression() {
|
|
|
958
1086
|
const items = collectAvailableItems({ requirePopover: true });
|
|
959
1087
|
if (findDeepResearchItem({ requirePopover: true }) || items.some(text => {
|
|
960
1088
|
const normalized = normalizeText(text);
|
|
961
|
-
return normalized.includes(
|
|
962
|
-
normalized.includes('create image') ||
|
|
963
|
-
normalized.includes('web search') ||
|
|
964
|
-
normalized.includes('deep research') ||
|
|
965
|
-
normalized.includes('get a detailed report');
|
|
1089
|
+
return dropdownReadyLabels.some(label => normalized.includes(label));
|
|
966
1090
|
})) { resolve(items); return; }
|
|
967
1091
|
elapsed += 150;
|
|
968
1092
|
if (elapsed > 3000) { resolve(items.length ? items : null); return; }
|