feat(signup): 优化步骤 3 收尾阶段的错误处理与日志记录

This commit is contained in:
QLHazyCoder
2026-04-23 15:47:38 +08:00
parent 05bf4d0185
commit 4a55cf210b
7 changed files with 150 additions and 20 deletions
+1
View File
@@ -550,6 +550,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
- 使用自定义密码或自动生成密码 - 使用自定义密码或自动生成密码
- 在密码页填写密码并提交注册表单 - 在密码页填写密码并提交注册表单
- 后台会在真正把 Step 3 记为完成前,再确认页面是否已经推进;如果此时出现认证页 `重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路 - 后台会在真正把 Step 3 记为完成前,再确认页面是否已经推进;如果此时出现认证页 `重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路
- Step 3 收尾阶段如果页面切换导致旧内容脚本失联,后台单次消息等待不会再卡住超过当前收尾预算;若最终仍未恢复,则会输出中文的步骤级错误,而不是直接暴露底层英文通信超时
实际使用的密码会写入会话状态,并同步到侧边栏显示。 实际使用的密码会写入会话状态,并同步到侧边栏显示。
+1
View File
@@ -6256,6 +6256,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
isGeneratedAliasProvider, isGeneratedAliasProvider,
isReusableGeneratedAliasEmail, isReusableGeneratedAliasEmail,
isSignupEmailVerificationPageUrl, isSignupEmailVerificationPageUrl,
isRetryableContentScriptTransportError,
isHotmailProvider, isHotmailProvider,
isLuckmailProvider, isLuckmailProvider,
isSignupPasswordPageUrl, isSignupPasswordPageUrl,
+28 -14
View File
@@ -3,6 +3,7 @@
})(typeof self !== 'undefined' ? self : globalThis, function createSignupFlowHelpersModule() { })(typeof self !== 'undefined' ? self : globalThis, function createSignupFlowHelpersModule() {
function createSignupFlowHelpers(deps = {}) { function createSignupFlowHelpers(deps = {}) {
const { const {
addLog,
buildGeneratedAliasEmail, buildGeneratedAliasEmail,
chrome, chrome,
ensureContentScriptReadyOnTab, ensureContentScriptReadyOnTab,
@@ -12,6 +13,7 @@
isGeneratedAliasProvider, isGeneratedAliasProvider,
isReusableGeneratedAliasEmail, isReusableGeneratedAliasEmail,
isHotmailProvider, isHotmailProvider,
isRetryableContentScriptTransportError = () => false,
isLuckmailProvider, isLuckmailProvider,
isSignupEmailVerificationPageUrl, isSignupEmailVerificationPageUrl,
isSignupPasswordPageUrl, isSignupPasswordPageUrl,
@@ -164,20 +166,32 @@
logMessage: `步骤 ${step}:认证页仍在切换,正在等待页面恢复后继续确认提交流程...`, logMessage: `步骤 ${step}:认证页仍在切换,正在等待页面恢复后继续确认提交流程...`,
}); });
const result = await sendToContentScriptResilient('signup-page', { let result;
type: 'PREPARE_SIGNUP_VERIFICATION', try {
step, result = await sendToContentScriptResilient('signup-page', {
source: 'background', type: 'PREPARE_SIGNUP_VERIFICATION',
payload: { step,
password: password || '', source: 'background',
prepareSource: 'step3_finalize', payload: {
prepareLogLabel: '步骤 3 收尾', password: password || '',
}, prepareSource: 'step3_finalize',
}, { prepareLogLabel: '步骤 3 收尾',
timeoutMs: 30000, },
retryDelayMs: 700, }, {
logMessage: `步骤 ${step}:密码已提交,正在确认是否进入下一页面,必要时自动恢复重试页...`, timeoutMs: 30000,
}); retryDelayMs: 700,
logMessage: `步骤 ${step}:密码已提交,正在确认是否进入下一页面,必要时自动恢复重试页...`,
});
} catch (error) {
if (isRetryableContentScriptTransportError(error)) {
const message = `步骤 ${step}:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。`;
if (typeof addLog === 'function') {
await addLog(message, 'warn');
}
throw new Error(message);
}
throw error;
}
if (result?.error) { if (result?.error) {
throw new Error(result.error); throw new Error(result.error);
+36 -6
View File
@@ -376,6 +376,17 @@
return 30000; return 30000;
} }
function resolveResponseTimeoutMs(message, requestedResponseTimeoutMs, remainingTimeoutMs = null) {
const fallbackTimeoutMs = getContentScriptResponseTimeoutMs(message);
const requestedTimeoutMs = Number.isFinite(Number(requestedResponseTimeoutMs))
? Math.max(1, Math.floor(Number(requestedResponseTimeoutMs)))
: fallbackTimeoutMs;
if (!Number.isFinite(Number(remainingTimeoutMs))) {
return requestedTimeoutMs;
}
return Math.max(1, Math.min(requestedTimeoutMs, Math.floor(Number(remainingTimeoutMs))));
}
function getMessageDebugLabel(source, message, tabId = null) { function getMessageDebugLabel(source, message, tabId = null) {
const parts = [source || 'unknown', message?.type || 'UNKNOWN']; const parts = [source || 'unknown', message?.type || 'UNKNOWN'];
if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`); if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`);
@@ -439,7 +450,13 @@
pendingCommands.delete(source); pendingCommands.delete(source);
reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`)); reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
}, timeout); }, timeout);
pendingCommands.set(source, { message, resolve, reject, timer }); pendingCommands.set(source, {
message,
resolve,
reject,
timer,
responseTimeoutMs: timeout,
});
console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`); console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
}); });
} }
@@ -449,7 +466,7 @@
if (pending) { if (pending) {
clearTimeout(pending.timer); clearTimeout(pending.timer);
pendingCommands.delete(source); pendingCommands.delete(source);
sendTabMessageWithTimeout(tabId, source, pending.message).then(pending.resolve).catch(pending.reject); sendTabMessageWithTimeout(tabId, source, pending.message, pending.responseTimeoutMs).then(pending.resolve).catch(pending.reject);
console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`); console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
} }
} }
@@ -564,13 +581,13 @@
if (!entry || !entry.ready) { if (!entry || !entry.ready) {
throwIfStopped(); throwIfStopped();
return queueCommand(source, message); return queueCommand(source, message, responseTimeoutMs);
} }
const alive = await isTabAlive(source); const alive = await isTabAlive(source);
throwIfStopped(); throwIfStopped();
if (!alive) { if (!alive) {
return queueCommand(source, message); return queueCommand(source, message, responseTimeoutMs);
} }
throwIfStopped(); throwIfStopped();
@@ -592,12 +609,18 @@
while (Date.now() - start < timeoutMs) { while (Date.now() - start < timeoutMs) {
throwIfStopped(); throwIfStopped();
attempt += 1; attempt += 1;
const remainingTimeoutMs = Math.max(1, timeoutMs - (Date.now() - start));
const effectiveResponseTimeoutMs = resolveResponseTimeoutMs(
message,
responseTimeoutMs,
remainingTimeoutMs
);
try { try {
return await sendToContentScript( return await sendToContentScript(
source, source,
message, message,
responseTimeoutMs !== undefined ? { responseTimeoutMs } : {} { responseTimeoutMs: effectiveResponseTimeoutMs }
); );
} catch (err) { } catch (err) {
const retryable = isRetryableContentScriptTransportError(err); const retryable = isRetryableContentScriptTransportError(err);
@@ -631,12 +654,18 @@
while (Date.now() - start < timeoutMs) { while (Date.now() - start < timeoutMs) {
throwIfStopped(); throwIfStopped();
const remainingTimeoutMs = Math.max(1, timeoutMs - (Date.now() - start));
const effectiveResponseTimeoutMs = resolveResponseTimeoutMs(
message,
responseTimeoutMs,
remainingTimeoutMs
);
try { try {
return await sendToContentScript( return await sendToContentScript(
mail.source, mail.source,
message, message,
responseTimeoutMs !== undefined ? { responseTimeoutMs } : {} { responseTimeoutMs: effectiveResponseTimeoutMs }
); );
} catch (err) { } catch (err) {
if (!isRetryableContentScriptTransportError(err)) { if (!isRetryableContentScriptTransportError(err)) {
@@ -684,6 +713,7 @@
queueCommand, queueCommand,
registerTab, registerTab,
rememberSourceLastUrl, rememberSourceLastUrl,
resolveResponseTimeoutMs,
reuseOrCreateTab, reuseOrCreateTab,
sendTabMessageWithTimeout, sendTabMessageWithTimeout,
sendToContentScript, sendToContentScript,
@@ -175,8 +175,12 @@ test('signup flow helper reuses existing managed alias email when it is still co
test('signup flow helper finalizes step 3 submit by reusing signup verification preparation', async () => { test('signup flow helper finalizes step 3 submit by reusing signup verification preparation', async () => {
let ensureCalls = 0; let ensureCalls = 0;
const messages = []; const messages = [];
const logs = [];
const helpers = signupFlowApi.createSignupFlowHelpers({ const helpers = signupFlowApi.createSignupFlowHelpers({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
buildGeneratedAliasEmail: () => '', buildGeneratedAliasEmail: () => '',
chrome: { tabs: { get: async () => ({ id: 31, url: 'https://auth.openai.com/create-account/password' }) } }, chrome: { tabs: { get: async () => ({ id: 31, url: 'https://auth.openai.com/create-account/password' }) } },
ensureContentScriptReadyOnTab: async (...args) => { ensureContentScriptReadyOnTab: async (...args) => {
@@ -188,6 +192,7 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification
isGeneratedAliasProvider: () => false, isGeneratedAliasProvider: () => false,
isReusableGeneratedAliasEmail: () => false, isReusableGeneratedAliasEmail: () => false,
isHotmailProvider: () => false, isHotmailProvider: () => false,
isRetryableContentScriptTransportError: () => false,
isLuckmailProvider: () => false, isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false, isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => true, isSignupPasswordPageUrl: () => true,
@@ -206,6 +211,7 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification
assert.deepStrictEqual(result, { ready: true, retried: 1 }); assert.deepStrictEqual(result, { ready: true, retried: 1 });
assert.equal(ensureCalls, 1); assert.equal(ensureCalls, 1);
assert.deepStrictEqual(logs, []);
assert.deepStrictEqual(messages.find((item) => item.type === 'send')?.message, { assert.deepStrictEqual(messages.find((item) => item.type === 'send')?.message, {
type: 'PREPARE_SIGNUP_VERIFICATION', type: 'PREPARE_SIGNUP_VERIFICATION',
step: 3, step: 3,
@@ -217,3 +223,45 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification
}, },
}); });
}); });
test('signup flow helper rewrites retryable step 3 finalize transport timeout into a Chinese error', async () => {
const logs = [];
const helpers = signupFlowApi.createSignupFlowHelpers({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
buildGeneratedAliasEmail: () => '',
chrome: { tabs: { get: async () => ({ id: 31, url: 'https://auth.openai.com/create-account/password' }) } },
ensureContentScriptReadyOnTab: async () => {},
ensureHotmailAccountForFlow: async () => ({}),
ensureLuckmailPurchaseForFlow: async () => ({}),
isGeneratedAliasProvider: () => false,
isReusableGeneratedAliasEmail: () => false,
isHotmailProvider: () => false,
isRetryableContentScriptTransportError: (error) => /did not respond in 45s/i.test(error?.message || String(error || '')),
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => true,
reuseOrCreateTab: async () => 31,
sendToContentScriptResilient: async () => {
throw new Error('Content script on signup-page did not respond in 45s. Try refreshing the tab and retry.');
},
setEmailState: async () => {},
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
SIGNUP_PAGE_INJECT_FILES: ['content/utils.js', 'content/signup-page.js'],
waitForTabUrlMatch: async () => null,
});
await assert.rejects(
() => helpers.finalizeSignupPasswordSubmitInTab(31, 'Secret123!', 3),
/步骤 3:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。/
);
assert.deepStrictEqual(logs, [
{
message: '步骤 3:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。',
level: 'warn',
},
]);
});
@@ -16,6 +16,41 @@ test('tab runtime module exposes a factory', () => {
assert.equal(typeof api?.createTabRuntime, 'function'); assert.equal(typeof api?.createTabRuntime, 'function');
}); });
test('tab runtime caps per-attempt response timeout to the remaining resilient timeout budget', () => {
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope);
const runtime = api.createTabRuntime({
LOG_PREFIX: '[test]',
addLog: async () => {},
chrome: {
tabs: {
get: async () => ({ id: 1, url: 'https://example.com', status: 'complete' }),
query: async () => [],
},
},
getSourceLabel: (source) => source || 'unknown',
getState: async () => ({ tabRegistry: {}, sourceLastUrls: {} }),
matchesSourceUrlFamily: () => false,
normalizeLocalCpaStep9Mode: () => 'submit',
parseUrlSafely: () => null,
registerTab: async () => {},
setState: async () => {},
shouldBypassStep9ForLocalCpa: () => false,
throwIfStopped: () => {},
});
assert.equal(
runtime.resolveResponseTimeoutMs({ type: 'PREPARE_SIGNUP_VERIFICATION' }, undefined, 30000),
30000
);
assert.equal(
runtime.resolveResponseTimeoutMs({ type: 'PREPARE_SIGNUP_VERIFICATION' }, 12000, 5000),
5000
);
});
test('tab runtime waitForTabComplete waits until tab status becomes complete', async () => { test('tab runtime waitForTabComplete waits until tab status becomes complete', async () => {
const source = fs.readFileSync('background/tab-runtime.js', 'utf8'); const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
const globalScope = {}; const globalScope = {};
+1
View File
@@ -312,6 +312,7 @@
6. 上报完成后再异步点击提交,避免页面跳转打断响应通道 6. 上报完成后再异步点击提交,避免页面跳转打断响应通道
7. 延迟提交真正触发前会再次检查 Stop 状态,避免用户已停止时页面仍继续自动提交 7. 延迟提交真正触发前会再次检查 Stop 状态,避免用户已停止时页面仍继续自动提交
8. 后台在真正确认 Step 3 完成前,会额外检查提交后是否切换页面;如果出现认证页 `Try again / 重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路 8. 后台在真正确认 Step 3 完成前,会额外检查提交后是否切换页面;如果出现认证页 `Try again / 重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路
9. Step 3 收尾阶段如果页面切换导致旧内容脚本失联,后台会把单次消息等待收口到当前收尾预算内,优先尽快重试重连;若最终仍未恢复,则输出中文的步骤级错误,而不是直接暴露底层英文通信超时
### Step 4 / Step 8 ### Step 4 / Step 8