/ clis / gemini / deep-research.js
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  });