Просмотр исходного кода

feat: 实现步骤 8 的状态管理和点击策略,优化 OAuth 授权流程

QLHazyCoder 4 месяцев назад
Родитель
Сommit
078a5457c5
2 измененных файлов с 301 добавлено и 31 удалено
  1. 181 22
      background.js
  2. 120 9
      content/signup-page.js

+ 181 - 22
background.js

@@ -2443,6 +2443,138 @@ async function executeStep7(state) {
 // ============================================================
 
 let webNavListener = null;
+const STEP8_CLICK_EFFECT_TIMEOUT_MS = 3500;
+const STEP8_CLICK_RETRY_DELAY_MS = 500;
+const STEP8_READY_WAIT_TIMEOUT_MS = 30000;
+const STEP8_STRATEGIES = [
+  { mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
+  { mode: 'debugger', label: 'debugger click' },
+  { mode: 'content', strategy: 'nativeClick', label: 'element.click' },
+  { mode: 'content', strategy: 'dispatchClick', label: 'dispatch click' },
+  { mode: 'debugger', label: 'debugger click retry' },
+];
+
+async function getStep8PageState(tabId, responseTimeoutMs = 1500) {
+  try {
+    const result = await sendTabMessageWithTimeout(tabId, 'signup-page', {
+      type: 'STEP8_GET_STATE',
+      source: 'background',
+      payload: {},
+    }, responseTimeoutMs);
+    if (result?.error) {
+      throw new Error(result.error);
+    }
+    return result;
+  } catch (err) {
+    if (isRetryableContentScriptTransportError(err)) {
+      return null;
+    }
+    throw err;
+  }
+}
+
+async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) {
+  const start = Date.now();
+
+  while (Date.now() - start < timeoutMs) {
+    throwIfStopped();
+    const pageState = await getStep8PageState(tabId);
+    if (pageState?.addPhonePage) {
+      throw new Error('步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
+    }
+    if (pageState?.consentReady) {
+      return pageState;
+    }
+    await sleepWithStop(250);
+  }
+
+  throw new Error('步骤 8:长时间未进入 OAuth 同意页,无法定位“继续”按钮。');
+}
+
+async function prepareStep8DebuggerClick() {
+  const result = await sendToContentScriptResilient('signup-page', {
+    type: 'STEP8_FIND_AND_CLICK',
+    source: 'background',
+    payload: {},
+  }, {
+    timeoutMs: 15000,
+    retryDelayMs: 600,
+    logMessage: '步骤 8:认证页正在切换,等待 OAuth 同意页按钮重新就绪...',
+  });
+
+  if (result?.error) {
+    throw new Error(result.error);
+  }
+
+  return result;
+}
+
+async function triggerStep8ContentStrategy(strategy) {
+  const result = await sendToContentScriptResilient('signup-page', {
+    type: 'STEP8_TRIGGER_CONTINUE',
+    source: 'background',
+    payload: {
+      strategy,
+      findTimeoutMs: 4000,
+      enabledTimeoutMs: 3000,
+    },
+  }, {
+    timeoutMs: 15000,
+    retryDelayMs: 600,
+    logMessage: '步骤 8:认证页正在切换,等待“继续”按钮重新就绪...',
+  });
+
+  if (result?.error) {
+    throw new Error(result.error);
+  }
+
+  return result;
+}
+
+async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLICK_EFFECT_TIMEOUT_MS) {
+  const start = Date.now();
+
+  while (Date.now() - start < timeoutMs) {
+    throwIfStopped();
+
+    const tab = await chrome.tabs.get(tabId).catch(() => null);
+    if (!tab) {
+      throw new Error('步骤 8:认证页面标签页已关闭,无法继续自动授权。');
+    }
+
+    if (baselineUrl && typeof tab.url === 'string' && tab.url !== baselineUrl) {
+      return { progressed: true, reason: 'url_changed', url: tab.url };
+    }
+
+    const pageState = await getStep8PageState(tabId);
+    if (pageState?.addPhonePage) {
+      throw new Error('步骤 8:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。');
+    }
+    if (pageState === null) {
+      return { progressed: true, reason: 'page_reloading' };
+    }
+    if (pageState && !pageState.consentPage) {
+      return { progressed: true, reason: 'left_consent_page', url: pageState.url };
+    }
+
+    await sleepWithStop(200);
+  }
+
+  return { progressed: false, reason: 'no_effect' };
+}
+
+function getStep8EffectLabel(effect) {
+  switch (effect?.reason) {
+    case 'url_changed':
+      return `URL 已变化:${effect.url}`;
+    case 'page_reloading':
+      return '页面正在跳转或重载';
+    case 'left_consent_page':
+      return `页面已离开 OAuth 同意页:${effect.url || 'unknown'}`;
+    default:
+      return '页面仍停留在 OAuth 同意页';
+  }
+}
 
 async function executeStep8(state) {
   if (!state.oauthUrl) {
@@ -2451,7 +2583,6 @@ async function executeStep8(state) {
 
   await addLog('步骤 8:正在监听 localhost 回调地址...');
 
-  // 只在当前步骤内注册 webNavigation 监听
   return new Promise((resolve, reject) => {
     let resolved = false;
     let signupTabId = null;
@@ -2463,9 +2594,16 @@ async function executeStep8(state) {
       }
     };
 
-    const timeout = setTimeout(() => {
+    const failStep8 = (error) => {
+      if (resolved) return;
+      resolved = true;
+      clearTimeout(timeout);
       cleanupListener();
-      reject(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 的点击可能被拦截了。'));
+      reject(error);
+    };
+
+    const timeout = setTimeout(() => {
+      failStep8(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 已持续重试点击“继续”但页面仍未完成授权。'));
     }, 120000);
 
     webNavListener = (details) => {
@@ -2487,40 +2625,61 @@ async function executeStep8(state) {
       }
     };
 
-
-    // 步骤 7 之后,认证页会进入 OAuth 同意页,出现“继续”按钮。
-    // 这里先在页面内定位按钮,再通过 debugger 输入事件发起点击。
     (async () => {
       try {
         signupTabId = await getTabId('signup-page');
         if (signupTabId) {
           await chrome.tabs.update(signupTabId, { active: true });
-          await addLog('步骤 8:已切回认证页,正在准备调试器点击...');
+          await addLog('步骤 8:已切回认证页,准备循环确认“继续”按钮直到页面真正跳转...');
         } else {
           signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
-          await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...');
+          await addLog('步骤 8:已重新打开认证页,准备循环确认“继续”按钮直到页面真正跳转...');
         }
 
         chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
 
-        const clickResult = await sendToContentScript('signup-page', {
-          type: 'STEP8_FIND_AND_CLICK',
-          source: 'background',
-          payload: {},
-        });
+        let attempt = 0;
+        while (!resolved) {
+          const pageState = await waitForStep8Ready(signupTabId);
+          if (!pageState?.consentReady) {
+            await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
+            continue;
+          }
 
-        if (clickResult?.error) {
-          throw new Error(clickResult.error);
-        }
+          const strategy = STEP8_STRATEGIES[attempt % STEP8_STRATEGIES.length];
+          const round = attempt + 1;
+          attempt += 1;
 
-        if (!resolved) {
-          await clickWithDebugger(signupTabId, clickResult?.rect);
-          await addLog('步骤 8:已发送调试器点击,正在等待跳转...');
+          await addLog(`步骤 8:第 ${round} 次尝试点击“继续”(${strategy.label})...`);
+
+          if (strategy.mode === 'debugger') {
+            const clickTarget = await prepareStep8DebuggerClick();
+            if (!resolved) {
+              await clickWithDebugger(signupTabId, clickTarget?.rect);
+            }
+          } else {
+            await triggerStep8ContentStrategy(strategy.strategy);
+          }
+
+          if (resolved) {
+            return;
+          }
+
+          const effect = await waitForStep8ClickEffect(signupTabId, pageState.url);
+          if (resolved) {
+            return;
+          }
+
+          if (effect.progressed) {
+            await addLog(`步骤 8:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info');
+            break;
+          }
+
+          await addLog(`步骤 8:${strategy.label} 本次未触发页面离开同意页,准备继续重试。`, 'warn');
+          await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
         }
       } catch (err) {
-        clearTimeout(timeout);
-        cleanupListener();
-        reject(err);
+        failStep8(err);
       }
     })();
   });

+ 120 - 9
content/signup-page.js

@@ -9,6 +9,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
     message.type === 'EXECUTE_STEP'
     || message.type === 'FILL_CODE'
     || message.type === 'STEP8_FIND_AND_CLICK'
+    || message.type === 'STEP8_GET_STATE'
+    || message.type === 'STEP8_TRIGGER_CONTINUE'
     || message.type === 'PREPARE_LOGIN_CODE'
     || message.type === 'PREPARE_SIGNUP_VERIFICATION'
     || message.type === 'RESEND_VERIFICATION_CODE'
@@ -58,6 +60,10 @@ async function handleCommand(message) {
       return await resendVerificationCode(message.step);
     case 'STEP8_FIND_AND_CLICK':
       return await step8_findAndClick();
+    case 'STEP8_GET_STATE':
+      return getStep8State();
+    case 'STEP8_TRIGGER_CONTINUE':
+      return await step8_triggerContinue(message.payload);
   }
 }
 
@@ -928,13 +934,7 @@ async function step6_login(payload) {
 async function step8_findAndClick() {
   log('步骤 8:正在查找 OAuth 同意页的“继续”按钮...');
 
-  const continueBtn = await findContinueButton();
-  await waitForButtonEnabled(continueBtn);
-
-  await humanPause(350, 900);
-  continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
-  continueBtn.focus();
-  await sleep(250);
+  const continueBtn = await prepareStep8ContinueButton();
 
   const rect = getSerializableRect(continueBtn);
   log('步骤 8:已找到“继续”按钮并准备好调试器点击坐标。');
@@ -945,9 +945,82 @@ async function step8_findAndClick() {
   };
 }
 
-async function findContinueButton() {
+function getStep8State() {
+  const pageText = getPageTextSnapshot();
+  const continueBtn = getPrimaryContinueButton();
+  const state = {
+    url: location.href,
+    consentPage: OAUTH_CONSENT_PAGE_PATTERN.test(pageText),
+    consentReady: isStep8Ready(),
+    verificationPage: isVerificationPageStillVisible(),
+    addPhonePage: isAddPhonePageReady(),
+    buttonFound: Boolean(continueBtn),
+    buttonEnabled: isButtonEnabled(continueBtn),
+    buttonText: continueBtn ? getActionText(continueBtn) : '',
+  };
+
+  if (continueBtn) {
+    try {
+      state.rect = getSerializableRect(continueBtn);
+    } catch {
+      state.rect = null;
+    }
+  }
+
+  return state;
+}
+
+async function step8_triggerContinue(payload = {}) {
+  const strategy = payload?.strategy || 'requestSubmit';
+  const continueBtn = await prepareStep8ContinueButton({
+    findTimeoutMs: payload?.findTimeoutMs,
+    enabledTimeoutMs: payload?.enabledTimeoutMs,
+  });
+  const form = continueBtn.form || continueBtn.closest('form');
+
+  switch (strategy) {
+    case 'requestSubmit':
+      if (!form || typeof form.requestSubmit !== 'function') {
+        throw new Error('“继续”按钮当前不在可提交的 form 中,无法使用 requestSubmit。URL: ' + location.href);
+      }
+      form.requestSubmit(continueBtn);
+      break;
+    case 'nativeClick':
+      continueBtn.click();
+      break;
+    case 'dispatchClick':
+      simulateClick(continueBtn);
+      break;
+    default:
+      throw new Error(`未知的 Step 8 触发策略:${strategy}`);
+  }
+
+  log(`Step 8: continue button triggered via ${strategy}.`);
+  return {
+    strategy,
+    ...getStep8State(),
+  };
+}
+
+async function prepareStep8ContinueButton(options = {}) {
+  const {
+    findTimeoutMs = 10000,
+    enabledTimeoutMs = 8000,
+  } = options;
+
+  const continueBtn = await findContinueButton(findTimeoutMs);
+  await waitForButtonEnabled(continueBtn, enabledTimeoutMs);
+
+  await humanPause(250, 700);
+  continueBtn.scrollIntoView({ behavior: 'auto', block: 'center' });
+  continueBtn.focus();
+  await waitForStableButtonRect(continueBtn);
+  return continueBtn;
+}
+
+async function findContinueButton(timeout = 10000) {
   const start = Date.now();
-  while (Date.now() - start < 10000) {
+  while (Date.now() - start < timeout) {
     throwIfStopped();
     if (isAddPhonePageReady()) {
       throw new Error('当前页面已进入手机号页面,不是 OAuth 授权同意页。URL: ' + location.href);
@@ -978,6 +1051,44 @@ function isButtonEnabled(button) {
     && button.getAttribute('aria-disabled') !== 'true';
 }
 
+async function waitForStableButtonRect(button, timeout = 1500) {
+  let previous = null;
+  let stableSamples = 0;
+  const start = Date.now();
+
+  while (Date.now() - start < timeout) {
+    throwIfStopped();
+    const rect = button?.getBoundingClientRect?.();
+    if (rect && rect.width > 0 && rect.height > 0) {
+      const snapshot = {
+        left: rect.left,
+        top: rect.top,
+        width: rect.width,
+        height: rect.height,
+      };
+
+      if (
+        previous
+        && Math.abs(snapshot.left - previous.left) < 1
+        && Math.abs(snapshot.top - previous.top) < 1
+        && Math.abs(snapshot.width - previous.width) < 1
+        && Math.abs(snapshot.height - previous.height) < 1
+      ) {
+        stableSamples += 1;
+        if (stableSamples >= 2) {
+          return;
+        }
+      } else {
+        stableSamples = 0;
+      }
+
+      previous = snapshot;
+    }
+
+    await sleep(80);
+  }
+}
+
 function getSerializableRect(el) {
   const rect = el.getBoundingClientRect();
   if (!rect.width || !rect.height) {