deep-research.js
1 import { cli, Strategy } from '@jackwener/opencli/registry'; 2 import { GEMINI_DEEP_RESEARCH_DEFAULT_CONFIRM_LABELS, GEMINI_DEEP_RESEARCH_DEFAULT_TOOL_LABELS, GEMINI_APP_URL, GEMINI_DOMAIN, getCurrentGeminiUrl, getLatestGeminiAssistantResponse, parseGeminiPositiveInt, readGeminiSnapshot, resolveGeminiLabels, selectGeminiTool, sendGeminiMessage, startNewGeminiChat, waitForGeminiSubmission, waitForGeminiConfirmButton, } from './utils.js'; 3 function isGeminiRootAppUrl(url) { 4 try { 5 const parsed = new URL(url); 6 return parsed.origin + parsed.pathname.replace(/\/+$/, '') === GEMINI_APP_URL; 7 } 8 catch { 9 return false; 10 } 11 } 12 function parseDeepResearchProgress(text) { 13 const isResearching = /\bresearching(?:\s+websites?)?\b|research in progress|working on your research|正在研究|研究中/i.test(text); 14 const waitingForStart = /\bstart(?:\s+deep)?\s+research\b|begin\s+research|generate(?:\s+deep)?\s+research\s+plan|开始研究|开始深度研究|开始调研|生成研究计划|生成调研计划|try again without deep research/i.test(text); 15 return { isResearching, waitingForStart }; 16 } 17 export const deepResearchCommand = cli({ 18 site: 'gemini', 19 name: 'deep-research', 20 description: 'Start a Gemini Deep Research run and confirm it', 21 domain: GEMINI_DOMAIN, 22 strategy: Strategy.COOKIE, 23 browser: true, 24 navigateBefore: false, 25 defaultFormat: 'plain', 26 timeoutSeconds: 180, 27 args: [ 28 { name: 'prompt', positional: true, required: true, help: 'Prompt to send' }, 29 { name: 'timeout', type: 'int', required: false, help: 'Max seconds to wait for confirm (default: 30)', default: 30 }, 30 { name: 'tool', required: false, help: 'Override tool label (default: Deep Research)' }, 31 { name: 'confirm', required: false, help: 'Override confirm button label (default: Start research)' }, 32 ], 33 columns: ['status', 'url'], 34 func: async (page, kwargs) => { 35 const prompt = kwargs.prompt; 36 const timeout = parseGeminiPositiveInt(kwargs.timeout, 30); 37 const submitTimeout = Math.min(Math.max(timeout, 6), 20); 38 await startNewGeminiChat(page); 39 const toolLabels = resolveGeminiLabels(kwargs.tool, GEMINI_DEEP_RESEARCH_DEFAULT_TOOL_LABELS); 40 const confirmLabels = resolveGeminiLabels(kwargs.confirm, GEMINI_DEEP_RESEARCH_DEFAULT_CONFIRM_LABELS); 41 const toolMatched = await selectGeminiTool(page, toolLabels); 42 if (!toolMatched) { 43 const url = await getCurrentGeminiUrl(page); 44 return [{ status: 'tool-not-found', url }]; 45 } 46 let baseline = await readGeminiSnapshot(page); 47 await sendGeminiMessage(page, prompt); 48 let submitted = await waitForGeminiSubmission(page, baseline, submitTimeout); 49 if (!submitted) { 50 // Retry once when submit did not stick (e.g. composer swallowed Enter/click in this UI state). 51 await selectGeminiTool(page, toolLabels); 52 baseline = await readGeminiSnapshot(page); 53 await sendGeminiMessage(page, prompt); 54 submitted = await waitForGeminiSubmission(page, baseline, submitTimeout); 55 } 56 if (!submitted) { 57 const url = await getCurrentGeminiUrl(page); 58 return [{ status: 'submit-not-found', url }]; 59 } 60 const confirmed = await waitForGeminiConfirmButton(page, confirmLabels, timeout); 61 let url = await getCurrentGeminiUrl(page); 62 if (confirmed && !isGeminiRootAppUrl(url)) { 63 return [{ status: 'started', url }]; 64 } 65 // false-positive confirm click can happen on generic buttons while still at /app root. 66 { 67 // Retry once when we are still at the root app URL, which usually means submit did not stick. 68 if (isGeminiRootAppUrl(url)) { 69 await selectGeminiTool(page, toolLabels); 70 // Avoid resending prompt here: it can create a duplicate conversation thread. 71 const confirmedRetry = await waitForGeminiConfirmButton(page, confirmLabels, timeout); 72 url = await getCurrentGeminiUrl(page); 73 if (confirmedRetry && !isGeminiRootAppUrl(url)) { 74 return [{ status: 'started', url }]; 75 } 76 } 77 let response = await getLatestGeminiAssistantResponse(page); 78 let { isResearching, waitingForStart } = parseDeepResearchProgress(response); 79 // Some UIs render the plan card first; click confirm one more time without resending prompt. 80 if (!isResearching && waitingForStart) { 81 const fallbackConfirmLabels = Array.from(new Set([ 82 ...confirmLabels, 83 ...GEMINI_DEEP_RESEARCH_DEFAULT_CONFIRM_LABELS, 84 ])); 85 const confirmedFallback = await waitForGeminiConfirmButton(page, fallbackConfirmLabels, Math.min(timeout, 8)); 86 if (confirmedFallback) { 87 url = await getCurrentGeminiUrl(page); 88 response = await getLatestGeminiAssistantResponse(page); 89 ({ isResearching, waitingForStart } = parseDeepResearchProgress(response)); 90 } 91 } 92 if (isResearching && !waitingForStart) { 93 return [{ status: 'started', url }]; 94 } 95 return [{ status: 'confirm-not-found', url }]; 96 } 97 }, 98 });