From b42184980100d22d805e63581662442d72abf37a Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 26 Apr 2026 02:02:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20waitForTabStableCo?= =?UTF-8?q?mplete=20=E5=87=BD=E6=95=B0=E4=BB=A5=E7=A1=AE=E4=BF=9D=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E9=A1=B5=E5=9C=A8=E7=A8=B3=E5=AE=9A=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=90=8E=E5=86=8D=E6=89=A7=E8=A1=8C=E5=90=8E=E7=BB=AD=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 12 +++-- background/tab-runtime.js | 47 ++++++++++++++++++ tests/background-tab-runtime-module.test.js | 55 +++++++++++++++++++++ 项目完整链路说明.md | 2 +- 项目文件结构说明.md | 4 +- 5 files changed, 114 insertions(+), 6 deletions(-) diff --git a/background.js b/background.js index b5767be..1e605b4 100644 --- a/background.js +++ b/background.js @@ -3972,6 +3972,10 @@ async function waitForTabComplete(tabId, options = {}) { return tabRuntime.waitForTabComplete(tabId, options); } +async function waitForTabStableComplete(tabId, options = {}) { + return tabRuntime.waitForTabStableComplete(tabId, options); +} + async function ensureContentScriptReadyOnTab(source, tabId, options = {}) { return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options); } @@ -5598,10 +5602,12 @@ async function executeStepAndWait(step, delayAfter = 2000) { if (step === 5) { const signupTabId = await getTabId('signup-page'); if (signupTabId) { - await addLog('自动运行:步骤 5 已收到完成信号,正在等待当前页面完成加载...', 'info'); - await waitForTabComplete(signupTabId, { - timeoutMs: 15000, + await addLog('自动运行:步骤 5 已收到完成信号,正在等待当前页面完成加载并稳定...', 'info'); + await waitForTabStableComplete(signupTabId, { + timeoutMs: 30000, retryDelayMs: 300, + stableMs: 1000, + initialDelayMs: 800, }); } } diff --git a/background/tab-runtime.js b/background/tab-runtime.js index 51619cb..17b3275 100644 --- a/background/tab-runtime.js +++ b/background/tab-runtime.js @@ -286,6 +286,52 @@ } } + async function waitForTabStableComplete(tabId, options = {}) { + const { + timeoutMs = 30000, + retryDelayMs = 300, + stableMs = 1000, + initialDelayMs = 0, + } = options; + const start = Date.now(); + let lastUrl = ''; + let lastStatus = ''; + let stableStartedAt = 0; + let lastTab = null; + + if (initialDelayMs > 0) { + await sleepOrStop(initialDelayMs); + } + + while (Date.now() - start < timeoutMs) { + throwIfStopped(); + try { + lastTab = await chrome.tabs.get(tabId); + } catch { + return null; + } + + const currentUrl = String(lastTab?.url || ''); + const currentStatus = String(lastTab?.status || ''); + if (currentStatus === 'complete') { + if (currentUrl !== lastUrl || currentStatus !== lastStatus || !stableStartedAt) { + stableStartedAt = Date.now(); + } + if (Date.now() - stableStartedAt >= stableMs) { + return lastTab; + } + } else { + stableStartedAt = 0; + } + + lastUrl = currentUrl; + lastStatus = currentStatus; + await sleepOrStop(retryDelayMs); + } + + return lastTab; + } + async function ensureContentScriptReadyOnTab(source, tabId, options = {}) { const { inject = null, @@ -721,6 +767,7 @@ sendToMailContentScriptResilient, summarizeMessageResultForDebug, waitForTabComplete, + waitForTabStableComplete, waitForTabUrlFamily, waitForTabUrlMatch, }; diff --git a/tests/background-tab-runtime-module.test.js b/tests/background-tab-runtime-module.test.js index 9a239cf..35e6ef8 100644 --- a/tests/background-tab-runtime-module.test.js +++ b/tests/background-tab-runtime-module.test.js @@ -133,3 +133,58 @@ test('tab runtime waitForTabComplete aborts promptly when stop is requested', as /Flow stopped\./ ); }); + +test('tab runtime waitForTabStableComplete waits through a late navigation after an initial complete state', async () => { + const source = fs.readFileSync('background/tab-runtime.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope); + + let getCalls = 0; + const runtime = api.createTabRuntime({ + LOG_PREFIX: '[test]', + addLog: async () => {}, + chrome: { + tabs: { + get: async () => { + getCalls += 1; + if (getCalls === 1) { + return { + id: 9, + url: 'https://auth.openai.com/u/signup/profile', + status: 'complete', + }; + } + if (getCalls === 2) { + return { + id: 9, + url: 'https://chatgpt.com/', + status: 'loading', + }; + } + return { + id: 9, + url: 'https://chatgpt.com/', + status: 'complete', + }; + }, + query: async () => [], + }, + }, + getSourceLabel: (sourceName) => sourceName || 'unknown', + getState: async () => ({ tabRegistry: {}, sourceLastUrls: {} }), + matchesSourceUrlFamily: () => false, + setState: async () => {}, + throwIfStopped: () => {}, + }); + + const result = await runtime.waitForTabStableComplete(9, { + timeoutMs: 2000, + retryDelayMs: 5, + stableMs: 5, + initialDelayMs: 1, + }); + + assert.equal(result?.url, 'https://chatgpt.com/'); + assert.equal(result?.status, 'complete'); + assert.ok(getCalls >= 4); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 15daf08..39d7153 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -365,7 +365,7 @@ 2. 内容脚本填写资料;如果资料页出现顶部“我同意以下所有各项”总勾选框,会先自动勾选后再提交 3. 内容脚本点击“完成帐户创建” 4. 点击后立即上报 Step 5 完成,不再等待页面结果 -5. 自动运行在进入 Step 6 前仅额外等待当前页面加载完成,不再在 Step 5 阶段接管 ChatGPT 跳转或 onboarding 跳过 +5. 自动运行在进入 Step 6 前会等待当前页面完成加载并短暂稳定,避免 Step 5 刚点击提交、后续跳转尚未真正开始时就执行下一步;但仍不在 Step 5 阶段接管 ChatGPT 跳转或 onboarding 跳过 ### Step 6 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index db47d43..45966c6 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -53,7 +53,7 @@ - `background/phone-verification-flow.js`:手机号验证共享流程模块,负责在 OAuth 链路命中 `add-phone / phone-verification` 页面后向 HeroSMS 申请或复用号码、轮询短信验证码、提交手机号码与短信验证码,并在号码长期收不到短信时把后续自动流拉回步骤 7 重新拿号。 - `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。 - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。 -- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。 +- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。 - `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给步骤 9,而不是误判为普通失败;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。 ## `background/steps/` @@ -164,7 +164,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 中断行为。 +- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂,并覆盖等待标签完成、等待标签稳定完成,以及等待过程中的 Stop 中断行为。 - `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。 - `tests/phone-verification-flow.test.js`:测试手机号验证共享流程对 HeroSMS 的取号、复用、重发、换号与 Step 7 重开错误分支。 - `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。