feat: 添加 waitForTabStableComplete 函数以确保标签页在稳定状态后再执行后续步骤

This commit is contained in:
QLHazyCoder
2026-04-26 02:02:57 +08:00
parent 12e6225eba
commit b421849801
5 changed files with 114 additions and 6 deletions
+9 -3
View File
@@ -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,
});
}
}
+47
View File
@@ -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,
};
@@ -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);
});
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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 的轮询与目标邮箱选择逻辑。