From 35bf6a2c32d029b8cad8177bc3d9338be77aebe6 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 10 May 2026 01:31:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=20GPC=20=E7=BB=93?= =?UTF-8?q?=E8=B4=A6=E6=B5=81=E7=A8=8B=E7=9A=84=E9=94=99=E8=AF=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86=E4=B8=8E=E9=87=8D=E8=AF=95=E6=9C=BA=E5=88=B6=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=B6=85=E6=97=B6=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 32 +++++ background/steps/create-plus-checkout.js | 29 +++- background/steps/fill-plus-checkout.js | 29 +++- .../gpc_sms_helper_macos.cpython-313.pyc | Bin 0 -> 25673 bytes tests/auto-run-step6-restart.test.js | 136 +++++++++++++++--- 项目完整链路说明.md | 15 +- 项目文件结构说明.md | 4 +- 7 files changed, 214 insertions(+), 31 deletions(-) create mode 100644 scripts/__pycache__/gpc_sms_helper_macos.cpython-313.pyc diff --git a/background.js b/background.js index a16dbb6..c601312 100644 --- a/background.js +++ b/background.js @@ -7743,6 +7743,17 @@ function isGpcTaskEndedFailure(error) { return /GPC_TASK_ENDED::/i.test(message); } +function isGpcCheckoutRestartRequiredFailure(error) { + const message = getErrorMessage(error); + if (/PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格/i.test(message)) { + return false; + } + if (/GPC_TASK_ENDED::/i.test(message)) { + return true; + } + return /GPC\s*API\s*请求超时|步骤\s*[67][\s\S]*GPC[\s\S]*(?:任务轮询超时|请求超时|超时|timeout|timed\s*out|卡死|无响应)|account\s+already\s+linked|GOPAY已经绑了订阅|(?:账号|账户|GoPay|GOPAY)[\s\S]*(?:已绑定|已经绑定|已绑|绑了订阅|绑定了订阅)|创建\s*GPC\s*订单失败[\s\S]*(?:任务已结束|任务结束|failed|expired|discarded|请求超时|timeout|timed\s*out)/i.test(message); +} + function isGoPayCheckoutRestartRequiredFailure(error) { const message = getErrorMessage(error); return /GOPAY_RESTART_FROM_STEP6::|GOPAY_RETRY_REQUIRED::/i.test(message); @@ -10432,6 +10443,7 @@ async function runAutoSequenceFromStep(startStep, context = {}) { const { targetRun, totalRuns, attemptRuns, continued = false } = context; let postStep7RestartCount = 0; let goPayCheckoutRestartCount = 0; + let gpcCheckoutRestartCount = 0; let step4RestartCount = 0; let currentStartStep = startStep; let continueCurrentAttempt = continued; @@ -10525,6 +10537,26 @@ async function runAutoSequenceFromStep(startStep, context = {}) { throw err; } + const stepExecutionKey = typeof getStepExecutionKeyForState === 'function' + ? getStepExecutionKeyForState(step, latestState) + : ''; + const isGpcCheckoutStep = normalizePlusPaymentMethod(latestState?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GPC_HELPER + || String(latestState?.plusCheckoutSource || '').trim() === PLUS_PAYMENT_METHOD_GPC_HELPER; + if (isGpcCheckoutStep + && (stepExecutionKey === 'plus-checkout-create' || stepExecutionKey === 'plus-checkout-billing') + && isGpcCheckoutRestartRequiredFailure(err)) { + gpcCheckoutRestartCount += 1; + await addLog( + `步骤 ${step}:检测到 GPC 任务失败/卡住,准备回到步骤 6 重新创建 GPC 任务(第 ${gpcCheckoutRestartCount} 次)。原因:${getErrorMessage(err)}`, + 'warn' + ); + await invalidateDownstreamAfterStepRestart(5, { + logLabel: `步骤 ${step} GPC 任务失败后准备回到步骤 6 重试(第 ${gpcCheckoutRestartCount} 次)`, + }); + step = 6; + continue; + } + if (step === 8 && isGoPayCheckoutRestartRequiredFailure(err)) { goPayCheckoutRestartCount += 1; if (goPayCheckoutRestartCount > 3) { diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js index 0651049..b07c970 100644 --- a/background/steps/create-plus-checkout.js +++ b/background/steps/create-plus-checkout.js @@ -257,11 +257,34 @@ throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。'); } const controller = typeof AbortController === 'function' ? new AbortController() : null; - const timer = controller ? setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 30000)) : null; + const effectiveTimeoutMs = Math.max(1000, Number(timeoutMs) || 30000); + let didTimeout = false; + let timer = null; + const buildTimeoutError = () => new Error(`GPC API 请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`); + const timeoutPromise = new Promise((_, reject) => { + timer = setTimeout(() => { + didTimeout = true; + reject(buildTimeoutError()); + if (controller) { + controller.abort(); + } + }, effectiveTimeoutMs); + }); try { - const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) }); - const data = await response.json().catch(() => ({})); + const response = await Promise.race([ + fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) }), + timeoutPromise, + ]); + const data = await Promise.race([ + response.json().catch(() => ({})), + timeoutPromise, + ]); return { response, data }; + } catch (error) { + if (didTimeout || error?.name === 'AbortError') { + throw buildTimeoutError(); + } + throw error; } finally { if (timer) clearTimeout(timer); } diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index d14f137..bdfea34 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -209,11 +209,34 @@ throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。'); } const controller = typeof AbortController === 'function' ? new AbortController() : null; - const timer = controller ? setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 30000)) : null; + const effectiveTimeoutMs = Math.max(1000, Number(timeoutMs) || 30000); + let didTimeout = false; + let timer = null; + const buildTimeoutError = () => new Error(`GPC API 请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`); + const timeoutPromise = new Promise((_, reject) => { + timer = setTimeout(() => { + didTimeout = true; + reject(buildTimeoutError()); + if (controller) { + controller.abort(); + } + }, effectiveTimeoutMs); + }); try { - const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) }); - const data = await response.json().catch(() => ({})); + const response = await Promise.race([ + fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) }), + timeoutPromise, + ]); + const data = await Promise.race([ + response.json().catch(() => ({})), + timeoutPromise, + ]); return { response, data }; + } catch (error) { + if (didTimeout || error?.name === 'AbortError') { + throw buildTimeoutError(); + } + throw error; } finally { if (timer) clearTimeout(timer); } diff --git a/scripts/__pycache__/gpc_sms_helper_macos.cpython-313.pyc b/scripts/__pycache__/gpc_sms_helper_macos.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f85f8201be60d90c8448d57846cb7d92afbef4f GIT binary patch literal 25673 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 literal 0 HcmV?d00001 diff --git a/tests/auto-run-step6-restart.test.js b/tests/auto-run-step6-restart.test.js index 4cf598f..1210f2e 100644 --- a/tests/auto-run-step6-restart.test.js +++ b/tests/auto-run-step6-restart.test.js @@ -56,10 +56,24 @@ const bundle = [ extractFunction('isAddPhoneAuthFailure'), extractFunction('isAddPhoneAuthUrl'), extractFunction('isAddPhoneAuthState'), + extractFunction('isGpcCheckoutRestartRequiredFailure'), extractFunction('getPostStep6AutoRestartDecision'), extractFunction('runAutoSequenceFromStep'), ].join('\n'); +const defaultStepDefinitions = { + 1: { key: 'open-signup' }, + 2: { key: 'prepare-email' }, + 3: { key: 'fill-password' }, + 4: { key: 'verify-email' }, + 5: { key: 'profile-basic' }, + 6: { key: 'profile-finish' }, + 7: { key: 'auth-login' }, + 8: { key: 'auth-email-code' }, + 9: { key: 'confirm-oauth' }, + 10: { key: 'platform-verify' }, +}; + function createHarness(options = {}) { const { startStep = 7, @@ -68,13 +82,18 @@ function createHarness(options = {}) { failureMessage = '认证失败: Request failed with status code 502', authState = { state: 'password_page', url: 'https://auth.openai.com/log-in' }, customState = {}, + stepDefinitions = defaultStepDefinitions, + stepIds = Object.keys(stepDefinitions).map(Number).sort((a, b) => a - b), + lastStepId = Math.max(...stepIds), + finalOAuthChainStartStep = 7, } = options; return new Function(` -const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 }; -const LAST_STEP_ID = 10; -const FINAL_OAUTH_CHAIN_START_STEP = 7; +const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0 }; +const LAST_STEP_ID = ${JSON.stringify(lastStepId)}; +const FINAL_OAUTH_CHAIN_START_STEP = ${JSON.stringify(finalOAuthChainStartStep)}; const SIGNUP_METHOD_PHONE = 'phone'; +const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper'; const LOG_PREFIX = '[test]'; const chrome = { tabs: { @@ -104,21 +123,10 @@ async function getState() { }; } function getStepIdsForState() { - return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + return ${JSON.stringify(stepIds)}; } function getStepDefinitionForState(step) { - const map = { - 1: { key: 'open-signup' }, - 2: { key: 'prepare-email' }, - 3: { key: 'fill-password' }, - 4: { key: 'verify-email' }, - 5: { key: 'profile-basic' }, - 6: { key: 'profile-finish' }, - 7: { key: 'auth-login' }, - 8: { key: 'auth-email-code' }, - 9: { key: 'confirm-oauth' }, - 10: { key: 'platform-verify' }, - }; + const map = ${JSON.stringify(stepDefinitions)}; return map[Number(step)] || null; } function getStepExecutionKeyForState(step, state = {}) { @@ -149,6 +157,10 @@ function getLoginAuthStateLabel(state) { function getErrorMessage(error) { return error?.message || String(error || ''); } +function normalizePlusPaymentMethod(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === PLUS_PAYMENT_METHOD_GPC_HELPER ? PLUS_PAYMENT_METHOD_GPC_HELPER : normalized; +} function isPhoneSmsPlatformRateLimitFailure(error) { const message = getErrorMessage(error); return /FIVE_SIM_RATE_LIMIT::|5sim[\s\S]*(?:限流|rate\s*limit)/i.test(message); @@ -321,3 +333,95 @@ test('auto-run restarts from confirm-oauth step after transient step10 token_exc }); assert.ok(events.logs.some(({ message }) => /回到步骤 9 重新开始授权流程/.test(message))); }); + +test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls', async () => { + const plusGpcSteps = { + 6: { key: 'plus-checkout-create' }, + 7: { key: 'plus-checkout-billing' }, + 10: { key: 'oauth-login' }, + 11: { key: 'fetch-login-code' }, + 12: { key: 'confirm-oauth' }, + 13: { key: 'platform-verify' }, + }; + const harness = createHarness({ + startStep: 6, + failureStep: 7, + failureBudget: 2, + failureMessage: 'GPC API 请求超时(>30 秒):https://gpc.qlhazycoder.top/api/gp/tasks/task_stalled', + stepDefinitions: plusGpcSteps, + finalOAuthChainStartStep: 10, + customState: { + stepStatuses: { 3: 'completed' }, + plusPaymentMethod: 'gpc-helper', + plusCheckoutSource: 'gpc-helper', + }, + }); + + const events = await harness.run(); + + assert.deepStrictEqual( + events.steps, + [6, 7, 6, 7, 6, 7, 10, 11, 12, 13] + ); + assert.deepStrictEqual( + events.invalidations.map((entry) => entry.step), + [5, 5] + ); + assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message))); +}); + +test('auto-run treats GPC account binding as recoverable step 6 restart', async () => { + const plusGpcSteps = { + 6: { key: 'plus-checkout-create' }, + 7: { key: 'plus-checkout-billing' }, + 10: { key: 'oauth-login' }, + 11: { key: 'fetch-login-code' }, + 12: { key: 'confirm-oauth' }, + 13: { key: 'platform-verify' }, + }; + const harness = createHarness({ + startStep: 6, + failureStep: 7, + failureBudget: 1, + failureMessage: 'GPC_TASK_ENDED::GOPAY已经绑了订阅,需要手动解绑', + stepDefinitions: plusGpcSteps, + finalOAuthChainStartStep: 10, + customState: { + stepStatuses: { 3: 'completed' }, + plusPaymentMethod: 'gpc-helper', + plusCheckoutSource: 'gpc-helper', + }, + }); + + const events = await harness.run(); + + assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 10, 11, 12, 13]); + assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]); +}); + +test('auto-run does not restart GPC checkout when Plus account has no free-trial eligibility', async () => { + const plusGpcSteps = { + 6: { key: 'plus-checkout-create' }, + 7: { key: 'plus-checkout-billing' }, + 10: { key: 'oauth-login' }, + }; + const harness = createHarness({ + startStep: 6, + failureStep: 7, + failureBudget: 1, + failureMessage: 'PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 7:今日应付金额不是 0(IDR 299000),当前账号没有免费试用资格,已跳过支付提交。', + stepDefinitions: plusGpcSteps, + finalOAuthChainStartStep: 10, + customState: { + stepStatuses: { 3: 'completed' }, + plusPaymentMethod: 'gpc-helper', + plusCheckoutSource: 'gpc-helper', + }, + }); + + const result = await harness.runAndCaptureError(); + + assert.ok(result?.error); + assert.deepStrictEqual(result.events.steps, [6, 7]); + assert.equal(result.events.invalidations.length, 0); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index a14ae57..2ef245c 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -328,13 +328,14 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功 1. 解析本轮应使用的注册方式:默认邮箱注册,开启接码且普通模式下可切到手机号注册 2. 打开或复用注册页 -3. 邮箱注册时,如果注册弹窗默认停留在手机号输入模式,会先尝试点击 `继续使用电子邮件地址登录 / Continue using email address` 一类按钮切回邮箱输入模式 -4. 邮箱注册分支:提交邮箱;邮箱输入框识别同时兼容本地化占位与 `aria-label` -5. 手机号注册分支:先点击官网“免费注册”并切换到手机号注册入口,必要时展开更多选项;确认手机号输入页就绪后,按“已有 signup activation > 手动填写的运行态手机号 > 接码平台新申请号码”的顺序决定手机号来源,避免重复买号。内容脚本会通过 `content/phone-country-utils.js` 共享工具按号码最长区号和平台国家名切换手机号国家下拉框,兼容 `+(44)` / `(+44)` / `+44` 等页面文案;必须确认可视下拉按钮的区号已同步后才填写并提交,若仍显示旧国家则停止提交 -6. 以当前统一账号标识先写入一条“停止(流程尚未完成)”的记录占位;邮箱账号占位邮箱,phone-only 账号占位手机号 -7. 等待身份提交后的真实落地页 -8. 如果进入密码页,则继续执行 Step 3 -9. 如果直接进入邮箱验证码页或手机验证码页,则自动跳过 Step 3 并进入 Step 4 +3. 内容脚本识别到官网“免费注册 / Sign up / Register”入口后,会先等待 3 秒再点击;如果点击后仍停留在官网入口页、没有进入邮箱或手机号输入页,会再次等待 3 秒后重试,最多额外重试 5 次,且每次真正点击前都会检查 Stop 状态 +4. 邮箱注册时,如果注册弹窗默认停留在手机号输入模式,会先尝试点击 `继续使用电子邮件地址登录 / Continue using email address` 一类按钮切回邮箱输入模式 +5. 邮箱注册分支:提交邮箱;邮箱输入框识别同时兼容本地化占位与 `aria-label` +6. 手机号注册分支:先点击官网“免费注册”并切换到手机号注册入口,必要时展开更多选项;确认手机号输入页就绪后,按“已有 signup activation > 手动填写的运行态手机号 > 接码平台新申请号码”的顺序决定手机号来源,避免重复买号。内容脚本会通过 `content/phone-country-utils.js` 共享工具按号码最长区号和平台国家名切换手机号国家下拉框,兼容 `+(44)` / `(+44)` / `+44` 等页面文案;必须确认可视下拉按钮的区号已同步后才填写并提交,若仍显示旧国家则停止提交 +7. 以当前统一账号标识先写入一条“停止(流程尚未完成)”的记录占位;邮箱账号占位邮箱,phone-only 账号占位手机号 +8. 等待身份提交后的真实落地页 +9. 如果进入密码页,则继续执行 Step 3 +10. 如果直接进入邮箱验证码页或手机验证码页,则自动跳过 Step 3 并进入 Step 4 ### Step 3 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index cee6d2a..06ad991 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -96,7 +96,7 @@ - `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。 - `content/plus-checkout.js`:ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。 - `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。 -- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号国家下拉框同步校验、手机号填写与提交;Step 3 收尾会识别手机号注册密码页的 `Incorrect phone number or password` 并上报当前轮重开信号;Step 7 / 8 登录链路会额外识别手机号输入页、手机号登录入口、`phone-verification` 与真实 `add-email` 页面,避免把手机验证码页或添加邮箱页误判成普通邮箱登录入口;Step 9 后置手机号验证完成后若偶发进入资料页,会复用 Step 5 的姓名生日填写逻辑再继续等待 OAuth 同意页。 +- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、官网注册入口点击前固定等待 3 秒、入口点击失败后最多 5 次重试、手机号国家下拉框同步校验、手机号填写与提交;Step 3 收尾会识别手机号注册密码页的 `Incorrect phone number or password` 并上报当前轮重开信号;Step 7 / 8 登录链路会额外识别手机号输入页、手机号登录入口、`phone-verification` 与真实 `add-email` 页面,避免把手机验证码页或添加邮箱页误判成普通邮箱登录入口;Step 9 后置手机号验证完成后若偶发进入资料页,会复用 Step 5 的姓名生日填写逻辑再继续等待 OAuth 同意页。 - `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;平台验证请求会读取 `visibleStep`,普通模式承接步骤 10,Plus 模式承接步骤 13。 - `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。 - `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10 与 Plus 可见步骤 13。 @@ -249,7 +249,7 @@ - `tests/sidepanel-mail2925-mode.test.js`:测试侧边栏保留 `2925 provide / receive` 模式行与独立的号池配置行,并验证 sidepanel 只在 provide 模式下把 2925 视为别名邮箱 provider。 - `tests/sidepanel-paypal-manager.test.js`:测试侧边栏公共表单弹窗脚本与 PayPal manager 的加载顺序,以及 PayPal 账号保存后会立即选中当前账号。 - `tests/signup-entry-diagnostics.test.js`:测试注册入口与密码页诊断快照输出,包括密码页错误文案字段。 -- `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式、本地化邮箱输入框识别,以及手机号注册时国家下拉框可视区号同步。 +- `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式、本地化邮箱输入框识别、官网注册入口点击等待与最多 5 次重试,以及手机号注册时国家下拉框可视区号同步。 - `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。 - `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。 - `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。