From 14a3e9d6b216e910ff8c965276c73de950271732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=81=E8=BF=9E=E6=B5=B7?= <2825305047@qq.com> Date: Fri, 17 Apr 2026 23:46:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E5=8F=B7=E9=A1=B5=E9=9D=A2=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=9B=B4=E6=96=B0=E6=AD=A5=E9=AA=A4?= =?UTF-8?q?7=E9=87=8D=E8=AF=95=E6=9C=BA=E5=88=B6=E5=8F=8A=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background/steps/oauth-login.js | 8 ++++ tests/background-step6-retry-limit.test.js | 51 ++++++++++++++++++++++ 项目完整链路说明.md | 4 +- 项目文件结构说明.md | 4 +- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index ce729ad..19cba8d 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -19,6 +19,11 @@ throwIfStopped, } = deps; + function isAddPhoneAuthFailure(error) { + const message = String(typeof error === 'string' ? error : error?.message || ''); + return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)|add-phone|手机号页面|手机号码|手机号|phone\s+number|telephone/i.test(message); + } + async function executeStep7(state) { if (!state.email) { throw new Error('缺少邮箱地址,请先完成步骤 3。'); @@ -91,6 +96,9 @@ throw new Error('步骤 7:认证页未返回可识别的登录结果。'); } catch (err) { throwIfStopped(err); + if (isAddPhoneAuthFailure(err)) { + throw err; + } lastError = err; if (attempt >= STEP6_MAX_ATTEMPTS) { break; diff --git a/tests/background-step6-retry-limit.test.js b/tests/background-step6-retry-limit.test.js index eed29ca..865b9c5 100644 --- a/tests/background-step6-retry-limit.test.js +++ b/tests/background-step6-retry-limit.test.js @@ -75,6 +75,57 @@ test('step 7 retries up to configured limit and then fails', async () => { assert.equal(events.completed, 0); }); +test('step 7 exits internal retry loop immediately when add-phone is detected', async () => { + const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); + + const events = { + refreshCalls: 0, + sendCalls: 0, + completed: 0, + logs: [], + }; + + const executor = api.createStep7Executor({ + addLog: async (message, level = 'info') => { + events.logs.push({ message, level }); + }, + completeStepFromBackground: async () => { + events.completed += 1; + }, + getErrorMessage: (error) => error?.message || String(error || ''), + getLoginAuthStateLabel: (state) => state || 'unknown', + getState: async () => ({ email: 'user@example.com', password: 'secret' }), + isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable', + isStep6SuccessResult: (result) => result?.step6Outcome === 'success', + refreshOAuthUrlBeforeStep6: async () => { + events.refreshCalls += 1; + return `https://oauth.example/${events.refreshCalls}`; + }, + reuseOrCreateTab: async () => {}, + sendToContentScriptResilient: async () => { + events.sendCalls += 1; + throw new Error('提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: https://auth.openai.com/add-phone'); + }, + STEP6_MAX_ATTEMPTS: 3, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => executor.executeStep7({ email: 'user@example.com', password: 'secret' }), + /add-phone/ + ); + + assert.equal(events.refreshCalls, 1, 'add-phone should stop further OAuth refresh attempts'); + assert.equal(events.sendCalls, 1, 'add-phone should stop after the first failed login attempt'); + assert.equal(events.completed, 0); + assert.ok( + !events.logs.some(({ message }) => /准备重试/.test(message)), + 'add-phone failure should not be logged as an internal retryable attempt' + ); +}); + test('step 7 starts a new oauth timeout window for each refreshed oauth url', async () => { const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); const globalScope = {}; diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 143a58d..77dfe46 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -298,7 +298,7 @@ 2. 打开最新 OAuth 链接 3. 登录 4. 确保真正进入验证码页 -5. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次 +5. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次;但一旦当前失败已经明确进入 `https://auth.openai.com/add-phone`,则立即退出步骤 7 内部重试,不再继续第 2 / 3 次尝试 6. 自动运行一旦进入步骤 7 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 7 重新开始授权流程 7. CPA 回调阶段固定为步骤 9,不再支持“第七步回调”跳过步骤 7/8 @@ -448,7 +448,7 @@ 3. 计算是否从中断点继续 4. 每轮执行前重置必要运行态 5. 执行 `runAutoSequenceFromStep` - - 步骤 7 内部仍保留登录态恢复的有限重试 + - 步骤 7 内部仍保留登录态恢复的有限重试,但 `add-phone / 手机号页` 属于立即跳出的不可重试错误 - 一旦进入步骤 7~10,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 7 无限重开 - 如果是手动停止,则立即退出自动流程,不会再触发“回到步骤 7 重开” 6. 如果失败,根据设置决定: diff --git a/项目文件结构说明.md b/项目文件结构说明.md index f13b7ab..1a72531 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -59,7 +59,7 @@ - `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口。 - `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。 - `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。 -- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页。 +- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。 - `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。 - `background/steps/platform-verify.js`:步骤 10 实现,负责 CPA / SUB2API 回调验证。 - `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。 @@ -143,7 +143,7 @@ - `tests/background-signup-step2-branching.test.js`:测试在 Gmail / 2925 模式下,已有兼容别名邮箱时应直接复用,不应再次重生成。 - `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。 - `tests/background-step5-submit-short-circuit.test.js`:测试步骤 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾。 -- `tests/background-step6-retry-limit.test.js`:测试步骤 6 的 Cookie 清理,以及步骤 7 的有限重试上限。 +- `tests/background-step6-retry-limit.test.js`:测试步骤 6 的 Cookie 清理,以及步骤 7 的有限重试上限与 `add-phone` 命中后的立即跳出行为。 - `tests/background-step7-recovery.test.js`:测试步骤 8 获取登录验证码后直接提交(不再回放步骤 7),并为 2925 关闭重发间隔。 - `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。 - `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成时的 Stop 中断行为。