diff --git a/README.md b/README.md index fd1d808..d52dff2 100644 --- a/README.md +++ b/README.md @@ -525,6 +525,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 - 页面要求 `age` 如果页面是生日模式,会填写年月日;如果页面上存在 `input[name='age']`,则直接填写年龄。 +如果资料页出现顶部“我同意以下所有各项”总勾选框,脚本会优先自动勾选,再点击 `完成帐户创建`。 点击 `完成帐户创建` 后,Step 5 会立刻记为完成,不再等待页面跳转结果;自动运行在进入 Step 6 前只会等待当前页面加载完成,不再接管 ChatGPT 跳转或 onboarding 跳过逻辑。 ### Step 6: Clear Login Cookies diff --git a/content/signup-page.js b/content/signup-page.js index 8632bf9..4ef7f78 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -799,6 +799,69 @@ function normalizeInlineText(text) { return (text || '').replace(/\s+/g, ' ').trim(); } +function isStep5AllConsentText(text) { + const normalizedText = normalizeInlineText(text).toLowerCase(); + if (!normalizedText) return false; + + return /i\s+agree\s+to\s+all\s+of\s+the\s+following/i.test(normalizedText) + || normalizedText.includes('\u6211\u540c\u610f\u4ee5\u4e0b\u6240\u6709\u5404\u9879') + || normalizedText.includes('\u540c\u610f\u4ee5\u4e0b\u6240\u6709\u5404\u9879') + || normalizedText.includes('\u6211\u540c\u610f\u6240\u6709') + || normalizedText.includes('\u5168\u90e8\u540c\u610f'); +} + +function findStep5AllConsentCheckbox() { + const namedCandidates = Array.from(document.querySelectorAll('input[name="allCheckboxes"][type="checkbox"]')) + .filter((el) => { + const checkboxLabel = el.closest?.('label') || null; + return isVisibleElement(el) || (checkboxLabel && isVisibleElement(checkboxLabel)); + }); + + const namedMatch = namedCandidates.find((el) => { + const checkboxLabel = el.closest?.('label') || null; + const checkboxText = normalizeInlineText([ + checkboxLabel?.textContent || '', + el.getAttribute?.('aria-label') || '', + el.getAttribute?.('title') || '', + el.getAttribute?.('name') || '', + ].filter(Boolean).join(' ')); + return isStep5AllConsentText(checkboxText); + }); + if (namedMatch) { + return namedMatch; + } + if (namedCandidates.length > 0) { + return namedCandidates[0]; + } + + return Array.from(document.querySelectorAll('input[type="checkbox"]')) + .find((el) => { + const checkboxLabel = el.closest?.('label') || null; + if (!isVisibleElement(el) && !(checkboxLabel && isVisibleElement(checkboxLabel))) { + return false; + } + const checkboxText = normalizeInlineText([ + checkboxLabel?.textContent || '', + el.getAttribute?.('aria-label') || '', + el.getAttribute?.('title') || '', + el.getAttribute?.('name') || '', + ].filter(Boolean).join(' ')); + return isStep5AllConsentText(checkboxText); + }) || null; +} + +function isStep5CheckboxChecked(checkbox) { + if (!checkbox) return false; + if (checkbox.checked === true) return true; + + const ariaChecked = String( + checkbox.getAttribute?.('aria-checked') + || checkbox.closest?.('[role="checkbox"]')?.getAttribute?.('aria-checked') + || '' + ).toLowerCase(); + return ariaChecked === 'true'; +} + function findBirthdayReactAriaSelect(labelText) { const normalizedLabel = normalizeInlineText(labelText); const roots = document.querySelectorAll('.react-aria-Select'); @@ -2337,16 +2400,10 @@ async function step5_fillNameBirthday(payload) { throw new Error('未找到生日或年龄输入项。URL: ' + location.href); } // 韩国IP判断勾选框""I agree" - const allConsentCheckbox = Array.from(document.querySelectorAll('input[name="allCheckboxes"][type="checkbox"]')) - .find((el) => { - const checkboxLabel = el.closest('label'); - const labelText = normalizeInlineText(checkboxLabel?.textContent || ''); - return (!checkboxLabel || isVisibleElement(checkboxLabel)) - && /I\s+agree\s+to\s+all\s+of\s+the\s+following/i.test(labelText); - }) || null; + const allConsentCheckbox = findStep5AllConsentCheckbox(); if (allConsentCheckbox) { - if (!allConsentCheckbox.checked) { + if (!isStep5CheckboxChecked(allConsentCheckbox)) { const checkboxLabel = allConsentCheckbox.closest('label'); await humanPause(500, 1500); if (checkboxLabel && isVisibleElement(checkboxLabel)) { @@ -2356,12 +2413,12 @@ async function step5_fillNameBirthday(payload) { } await sleep(250); - if (!allConsentCheckbox.checked) { + if (!isStep5CheckboxChecked(allConsentCheckbox)) { allConsentCheckbox.click(); await sleep(250); } - if (!allConsentCheckbox.checked) { + if (!isStep5CheckboxChecked(allConsentCheckbox)) { throw new Error('未能勾选 “I agree to all of the following” 复选框。'); } diff --git a/tests/step5-age-consent.test.js b/tests/step5-age-consent.test.js new file mode 100644 index 0000000..2199a00 --- /dev/null +++ b/tests/step5-age-consent.test.js @@ -0,0 +1,229 @@ +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); +} + +function getStep5Bundle() { + return [ + extractFunction('getStep5DirectCompletionPayload'), + extractFunction('isStep5AllConsentText'), + extractFunction('findStep5AllConsentCheckbox'), + extractFunction('isStep5CheckboxChecked'), + extractFunction('step5_fillNameBirthday'), + ].join('\n'); +} + +test('step 5 clicks the top all-consent checkbox on age page before submit', async () => { + const api = new Function(` +const logs = []; +const completions = []; +const clicks = []; + +const nameInput = { value: '', hidden: false }; +const ageInput = { value: '', hidden: false }; +const completeButton = { + tagName: 'BUTTON', + textContent: '\\u5b8c\\u6210\\u8d26\\u6237\\u521b\\u5efa', + hidden: false, +}; +const allConsentLabel = { + hidden: false, + textContent: '\\u6211\\u540c\\u610f\\u4ee5\\u4e0b\\u6240\\u6709\\u5404\\u9879', + closest() { + return null; + }, +}; +const allConsentCheckbox = { + checked: false, + hidden: true, + name: 'allCheckboxes', + type: 'checkbox', + click() { + this.checked = true; + }, + getAttribute(name) { + if (name === 'name') return this.name; + if (name === 'type') return this.type; + return ''; + }, + closest(selector) { + if (selector === 'label') return allConsentLabel; + return null; + }, +}; + +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="birthday"]': + return null; + case 'input[name="age"]': + return ageInput; + case 'button[type="submit"]': + return completeButton; + default: + return null; + } + }, + querySelectorAll(selector) { + if (selector === 'input[name="allCheckboxes"][type="checkbox"]') { + return [allConsentCheckbox]; + } + if (selector === 'input[type="checkbox"]') { + return [allConsentCheckbox]; + } + return []; + }, + execCommand() {}, +}; + +const location = { + href: 'https://auth.openai.com/u/signup/profile', +}; + +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() { + return null; +} + +function isVisibleElement(el) { + return Boolean(el) && !el.hidden; +} + +async function setReactAriaBirthdaySelect() { + throw new Error('setReactAriaBirthdaySelect should not run in age-mode test'); +} + +async function waitForElementByText() { + throw new Error('waitForElementByText should not run in this test'); +} + +function simulateClick(el) { + clicks.push(el.textContent || el.tagName || 'element'); + if (el === allConsentLabel || el === allConsentCheckbox) { + allConsentCheckbox.checked = true; + } +} + +function reportComplete(step, payload) { + completions.push({ step, payload }); +} + +function normalizeInlineText(text) { + return String(text || '').replace(/\\s+/g, ' ').trim(); +} + +${getStep5Bundle()} + +return { + async run(payload) { + return step5_fillNameBirthday(payload); + }, + snapshot() { + return { + logs, + completions, + clicks, + nameValue: nameInput.value, + ageValue: ageInput.value, + consentChecked: allConsentCheckbox.checked, + }; + }, +}; +`)(); + + const result = await api.run({ + firstName: 'Mia', + lastName: 'Harris', + age: 19, + }); + + const snapshot = api.snapshot(); + assert.deepStrictEqual(result, { + skippedPostSubmitCheck: true, + directProceedToStep6: true, + ageMode: true, + }); + assert.equal(snapshot.nameValue, 'Mia Harris'); + assert.equal(snapshot.ageValue, '19'); + assert.equal(snapshot.consentChecked, true); + assert.deepStrictEqual(snapshot.clicks, [ + '\u6211\u540c\u610f\u4ee5\u4e0b\u6240\u6709\u5404\u9879', + '\u5b8c\u6210\u8d26\u6237\u521b\u5efa', + ]); + assert.deepStrictEqual(snapshot.completions, [ + { + step: 5, + payload: { + skippedPostSubmitCheck: true, + directProceedToStep6: true, + ageMode: true, + }, + }, + ]); +}); diff --git a/tests/step5-direct-complete.test.js b/tests/step5-direct-complete.test.js index 07df234..1c11b6e 100644 --- a/tests/step5-direct-complete.test.js +++ b/tests/step5-direct-complete.test.js @@ -51,6 +51,16 @@ function extractFunction(name) { return source.slice(start, end); } +function getStep5Bundle() { + return [ + extractFunction('getStep5DirectCompletionPayload'), + extractFunction('isStep5AllConsentText'), + extractFunction('findStep5AllConsentCheckbox'), + extractFunction('isStep5CheckboxChecked'), + extractFunction('step5_fillNameBirthday'), + ].join('\n'); +} + test('step 5 clicks submit and completes immediately on birthday page', async () => { const step5Source = extractFunction('step5_fillNameBirthday'); assert.ok( @@ -158,12 +168,11 @@ function reportComplete(step, payload) { completions.push({ step, payload }); } -function normalizeInlineText(text) { - return text; -} + function normalizeInlineText(text) { + return String(text || '').replace(/\\s+/g, ' ').trim(); + } -${extractFunction('getStep5DirectCompletionPayload')} -${extractFunction('step5_fillNameBirthday')} + ${getStep5Bundle()} return { async run(payload) { diff --git a/项目完整链路说明.md b/项目完整链路说明.md index cf33226..140c3e9 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -298,9 +298,10 @@ 流程: 1. 生成随机姓名和生日 -2. 内容脚本填写资料并点击“完成帐户创建” -3. 点击后立即上报 Step 5 完成,不再等待页面结果 -4. 自动运行在进入 Step 6 前仅额外等待当前页面加载完成,不再在 Step 5 阶段接管 ChatGPT 跳转或 onboarding 跳过 +2. 内容脚本填写资料;如果资料页出现顶部“我同意以下所有各项”总勾选框,会先自动勾选后再提交 +3. 内容脚本点击“完成帐户创建” +4. 点击后立即上报 Step 5 完成,不再等待页面结果 +5. 自动运行在进入 Step 6 前仅额外等待当前页面加载完成,不再在 Step 5 阶段接管 ChatGPT 跳转或 onboarding 跳过 ### Step 6 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 3b4b99b..4d90214 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -170,6 +170,7 @@ - `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。 - `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。 - `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。 +- `tests/step5-age-consent.test.js`:测试步骤 5 在年龄页会先勾选顶部“我同意以下所有各项”复选框,再点击提交。 - `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。 - `tests/step6-login-state.test.js`:测试 OAuth 登录状态识别逻辑,当前对应步骤 7 链路。 - `tests/step6-timeout-recovery.test.js`:测试 OAuth 登录超时报错页的可恢复结果构造,当前对应步骤 7 链路。