Browse Source

feat: 优化步骤 5 提交逻辑,点击后立即上报完成状态,避免等待页面结果

QLHazyCoder 4 months ago
parent
commit
ce0fe101a3

+ 1 - 0
README.md

@@ -485,6 +485,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
 - 页面要求 `age`
 
 如果页面是生日模式,会填写年月日;如果页面上存在 `input[name='age']`,则直接填写年龄。
+点击 `完成帐户创建` / `继续` 后,Step 5 会立刻记为完成,不再等待页面跳转结果;只有在提交瞬间发生页面跳转、且后台还没收到完成信号时,才会兜底检查是否已跳到 ChatGPT。
 
 ### Step 6: Login via OAuth
 

+ 1 - 0
background.js

@@ -5269,6 +5269,7 @@ const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({
   addLog,
   generateRandomBirthday,
   generateRandomName,
+  getState,
   getTabId,
   handleChatgptOnboardingSkip,
   isRetryableContentScriptTransportError,

+ 14 - 0
background/steps/fill-profile.js

@@ -6,6 +6,7 @@
       addLog,
       generateRandomBirthday,
       generateRandomName,
+      getState,
       getTabId,
       handleChatgptOnboardingSkip,
       isRetryableContentScriptTransportError,
@@ -14,6 +15,14 @@
       waitForStep5ChatgptRedirect,
     } = deps;
 
+    async function isStepAlreadyCompleted(step) {
+      if (typeof getState !== 'function') {
+        return false;
+      }
+      const latestState = await getState().catch(() => null);
+      return latestState?.stepStatuses?.[step] === 'completed';
+    }
+
     async function executeStep5() {
       const { firstName, lastName } = generateRandomName();
       const { year, month, day } = generateRandomBirthday();
@@ -39,6 +48,11 @@
         }
       }
 
+      if (step5TransportError && await isStepAlreadyCompleted(5)) {
+        await addLog('步骤 5:提交后的页面跳转打断了响应,但已收到完成信号,直接结束当前步骤。', 'info');
+        return;
+      }
+
       if (step5Result?.chatgptOnboarding) {
         await addLog('步骤 5:检测到 ChatGPT 引导页跳转,正在处理引导页跳过...');
         await handleChatgptOnboardingSkip();

+ 19 - 73
content/signup-page.js

@@ -844,47 +844,6 @@ async function waitForChatgptPostSignupState(timeout = 15000) {
   return null;
 }
 
-async function waitForStep5SubmitOutcome(timeout = 15000) {
-  const start = Date.now();
-
-  while (Date.now() - start < timeout) {
-    throwIfStopped();
-
-    const errorText = getStep5ErrorText();
-    if (errorText) {
-      return { invalidProfile: true, errorText };
-    }
-
-    if (isAddPhonePageReady()) {
-      return { success: true, addPhonePage: true };
-    }
-
-    if (isChatgptOnboardingPage()) {
-      return { success: true, chatgptOnboarding: true };
-    }
-
-    if (isChatgptAuthenticatedHomePage()) {
-      return { success: true, chatgptHome: true };
-    }
-
-    if (isStep8Ready()) {
-      return { success: true };
-    }
-
-    await sleep(150);
-  }
-
-  const errorText = getStep5ErrorText();
-  if (errorText) {
-    return { invalidProfile: true, errorText };
-  }
-
-  return {
-    invalidProfile: true,
-    errorText: '提交后未进入下一阶段,请检查生日是否真正被页面接受。',
-  };
-}
-
 function isSignupPasswordPage() {
   return /\/create-account\/password(?:[/?#]|$)/i.test(location.pathname || '');
 }
@@ -2004,6 +1963,17 @@ function getSerializableRect(el) {
 // Step 5: Fill Name & Birthday / Age
 // ============================================================
 
+function getStep5DirectCompletionPayload({ isAgeMode = false } = {}) {
+  const payload = {
+    skippedPostSubmitCheck: true,
+    directProceedToStep6: true,
+  };
+  if (isAgeMode) {
+    payload.ageMode = true;
+  }
+  return payload;
+}
+
 async function step5_fillNameBirthday(payload) {
   const { firstName, lastName, age, year, month, day } = payload;
   if (!firstName || !lastName) throw new Error('未提供姓名数据。');
@@ -2220,46 +2190,22 @@ async function step5_fillNameBirthday(payload) {
 
   const isAgeMode = !birthdayMode && Boolean(ageInput);
   if (isAgeMode) {
-    log('步骤 5:当前为年龄输入模式,点击“继续”后将直接视为完成并进入步骤 6。', 'warn');
-    reportComplete(5, {
-      skippedPostSubmitCheck: true,
-      directProceedToStep6: true,
-    });
+    log('步骤 5:当前为年龄输入模式,点击“继续”后将直接完成当前步骤。', 'warn');
   }
 
   await humanPause(500, 1300);
   simulateClick(completeBtn);
 
-  if (isAgeMode) {
-    log('步骤 5:年龄模式已点击“继续”,已跳过后续结果等待。', 'warn');
-    return;
-  }
-
-  log('步骤 5:已点击“完成帐户创建”,正在等待页面结果...');
-
-  const outcome = await waitForStep5SubmitOutcome();
-  if (outcome.invalidProfile) {
-    throw new Error(`步骤 5:${outcome.errorText}`);
-  }
+  const completionPayload = getStep5DirectCompletionPayload({ isAgeMode });
+  reportComplete(5, completionPayload);
 
-  if (outcome.chatgptOnboarding || outcome.chatgptHome) {
-    if (outcome.chatgptOnboarding) {
-      log('步骤 5:资料已通过,页面已跳转到 ChatGPT 引导页。', 'ok');
-    } else {
-      log('步骤 5:资料已通过,页面已进入已登录的 ChatGPT 页面。', 'ok');
-    }
-    reportComplete(5, {
-      chatgptOnboarding: Boolean(outcome.chatgptOnboarding),
-      chatgptHome: Boolean(outcome.chatgptHome),
-    });
-    return {
-      chatgptOnboarding: Boolean(outcome.chatgptOnboarding),
-      chatgptHome: Boolean(outcome.chatgptHome),
-    };
+  if (isAgeMode) {
+    log('步骤 5:年龄模式已点击“继续”,当前步骤直接完成,不再等待页面结果。', 'warn');
+    return completionPayload;
   }
 
-  log(`步骤 5:资料已通过。`, 'ok');
-  reportComplete(5, { addPhonePage: Boolean(outcome.addPhonePage) });
+  log('步骤 5:已点击“完成帐户创建”,当前步骤直接完成,不再等待页面结果。');
+  return completionPayload;
 }
 
 // ============================================================

+ 52 - 0
tests/background-step5-submit-short-circuit.test.js

@@ -0,0 +1,52 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+const source = fs.readFileSync('background/steps/fill-profile.js', 'utf8');
+const globalScope = {};
+const api = new Function('self', `${source}; return self.MultiPageBackgroundStep5;`)(globalScope);
+
+test('step 5 transport error returns immediately after completion signal arrives', async () => {
+  const events = {
+    redirectWaitCalls: 0,
+    onboardingCalls: 0,
+    logs: [],
+  };
+
+  const transportError = new Error('The message port closed before a response was received.');
+
+  const executor = api.createStep5Executor({
+    addLog: async (message, level) => {
+      events.logs.push({ message, level: level || 'info' });
+    },
+    generateRandomBirthday: () => ({ year: 2003, month: 6, day: 19 }),
+    generateRandomName: () => ({ firstName: 'Test', lastName: 'User' }),
+    getState: async () => ({
+      stepStatuses: {
+        5: 'completed',
+      },
+    }),
+    getTabId: async () => 123,
+    handleChatgptOnboardingSkip: async () => {
+      events.onboardingCalls += 1;
+    },
+    isRetryableContentScriptTransportError: (error) => error === transportError,
+    LOG_PREFIX: '[test]',
+    sendToContentScript: async () => {
+      throw transportError;
+    },
+    waitForStep5ChatgptRedirect: async () => {
+      events.redirectWaitCalls += 1;
+      return { id: 123, url: 'https://chatgpt.com/' };
+    },
+  });
+
+  await executor.executeStep5();
+
+  assert.equal(events.redirectWaitCalls, 0, '收到完成信号后不应再等待 ChatGPT 跳转');
+  assert.equal(events.onboardingCalls, 0, '收到完成信号后不应再触发 onboarding 跳过');
+  assert.ok(
+    events.logs.some(({ message }) => /已收到完成信号,直接结束当前步骤/.test(message)),
+    '应记录 Step 5 已按完成信号直接结束'
+  );
+});

+ 217 - 0
tests/step5-direct-complete.test.js

@@ -0,0 +1,217 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+const source = fs.readFileSync('content/signup-page.js', 'utf8');
+
+function extractFunction(name) {
+  const markers = [`async function ${name}(`, `function ${name}(`];
+  const start = markers
+    .map((marker) => source.indexOf(marker))
+    .find((index) => index >= 0);
+  if (start < 0) {
+    throw new Error(`missing function ${name}`);
+  }
+
+  let parenDepth = 0;
+  let signatureEnded = false;
+  let braceStart = -1;
+  for (let i = start; i < source.length; i += 1) {
+    const ch = source[i];
+    if (ch === '(') {
+      parenDepth += 1;
+    } else if (ch === ')') {
+      parenDepth -= 1;
+      if (parenDepth === 0) {
+        signatureEnded = true;
+      }
+    } else if (ch === '{' && signatureEnded) {
+      braceStart = i;
+      break;
+    }
+  }
+  if (braceStart < 0) {
+    throw new Error(`missing body for function ${name}`);
+  }
+
+  let depth = 0;
+  let end = braceStart;
+  for (; end < source.length; end += 1) {
+    const ch = source[end];
+    if (ch === '{') depth += 1;
+    if (ch === '}') {
+      depth -= 1;
+      if (depth === 0) {
+        end += 1;
+        break;
+      }
+    }
+  }
+
+  return source.slice(start, end);
+}
+
+test('step 5 clicks submit and completes immediately on birthday page', async () => {
+  const step5Source = extractFunction('step5_fillNameBirthday');
+  assert.ok(
+    !step5Source.includes('waitForStep5SubmitOutcome('),
+    'Step 5 提交后不应再等待页面结果'
+  );
+
+  const api = new Function(`
+const logs = [];
+const completions = [];
+const clicks = [];
+const selectedBirthday = {};
+
+const nameInput = { value: '', hidden: false };
+const hiddenBirthday = {
+  value: '',
+  hidden: false,
+  dispatchEvent() {},
+};
+const completeButton = {
+  tagName: 'BUTTON',
+  textContent: '完成帐户创建',
+  hidden: false,
+};
+
+const birthdaySelects = {
+  '年': { label: '年', button: { hidden: false }, nativeSelect: {} },
+  '月': { label: '月', button: { hidden: false }, nativeSelect: {} },
+  '天': { label: '天', button: { hidden: false }, nativeSelect: {} },
+};
+
+const document = {
+  querySelector(selector) {
+    switch (selector) {
+      case '[role="spinbutton"][data-type="year"]':
+      case '[role="spinbutton"][data-type="month"]':
+      case '[role="spinbutton"][data-type="day"]':
+      case 'input[name="age"]':
+        return null;
+      case 'input[name="birthday"]':
+        return hiddenBirthday;
+      case 'button[type="submit"]':
+        return completeButton;
+      default:
+        return null;
+    }
+  },
+  querySelectorAll(selector) {
+    if (selector === 'input[name="allCheckboxes"][type="checkbox"]') {
+      return [];
+    }
+    return [];
+  },
+  execCommand() {},
+};
+
+const location = {
+  href: 'https://auth.openai.com/u/signup/profile',
+};
+
+function Event(type, init = {}) {
+  this.type = type;
+  this.bubbles = Boolean(init.bubbles);
+}
+
+function log(message, level = 'info') {
+  logs.push({ message, level });
+}
+
+async function waitForElement() {
+  return nameInput;
+}
+
+async function humanPause() {}
+async function sleep() {}
+
+function fillInput(input, value) {
+  input.value = value;
+}
+
+function findBirthdayReactAriaSelect(label) {
+  return birthdaySelects[label] || null;
+}
+
+function isVisibleElement(el) {
+  return Boolean(el) && !el.hidden;
+}
+
+async function setReactAriaBirthdaySelect(select, value) {
+  selectedBirthday[select.label] = String(value).padStart(select.label === '年' ? 4 : 2, '0');
+  if (selectedBirthday['年'] && selectedBirthday['月'] && selectedBirthday['天']) {
+    hiddenBirthday.value = \`\${selectedBirthday['年']}-\${selectedBirthday['月']}-\${selectedBirthday['天']}\`;
+  }
+}
+
+async function waitForElementByText() {
+  throw new Error('waitForElementByText should not run in this test');
+}
+
+function simulateClick(el) {
+  clicks.push(el.textContent || el.tagName || 'element');
+}
+
+function reportComplete(step, payload) {
+  completions.push({ step, payload });
+}
+
+function normalizeInlineText(text) {
+  return text;
+}
+
+${extractFunction('getStep5DirectCompletionPayload')}
+${extractFunction('step5_fillNameBirthday')}
+
+return {
+  async run(payload) {
+    return step5_fillNameBirthday(payload);
+  },
+  snapshot() {
+    return {
+      logs,
+      completions,
+      clicks,
+      nameValue: nameInput.value,
+      birthdayValue: hiddenBirthday.value,
+    };
+  },
+};
+`)();
+
+  const result = await api.run({
+    firstName: 'Test',
+    lastName: 'User',
+    year: 2003,
+    month: 6,
+    day: 19,
+  });
+
+  const snapshot = api.snapshot();
+  assert.deepStrictEqual(
+    result,
+    {
+      skippedPostSubmitCheck: true,
+      directProceedToStep6: true,
+    },
+    '生日模式点击提交后应直接返回完成载荷'
+  );
+  assert.deepStrictEqual(snapshot.completions, [
+    {
+      step: 5,
+      payload: {
+        skippedPostSubmitCheck: true,
+        directProceedToStep6: true,
+      },
+    },
+  ]);
+  assert.deepStrictEqual(snapshot.clicks, ['完成帐户创建']);
+  assert.equal(snapshot.nameValue, 'Test User');
+  assert.equal(snapshot.birthdayValue, '2003-06-19');
+  assert.ok(
+    snapshot.logs.some(({ message }) => /不再等待页面结果/.test(message)),
+    '日志应明确说明 Step 5 已直接完成'
+  );
+});

+ 3 - 2
项目完整链路说明.md

@@ -252,8 +252,9 @@
 流程:
 
 1. 生成随机姓名和生日
-2. 内容脚本填写资料
-3. 如果页面跳到 ChatGPT onboarding,则执行跳过链路
+2. 内容脚本填写资料并点击“完成帐户创建” / “继续”
+3. 点击后立即上报 Step 5 完成,不再等待页面结果
+4. 如果提交瞬间页面跳转导致响应通道中断,后台仅在未收到完成信号时兜底判断是否跳到 ChatGPT
 
 ### Step 6