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 收尾阶段如果页面切换导致旧内容脚本失联,后台单次消息等待不会再卡住超过当前收尾预算;若最终仍未恢复,则会输出中文的步骤级错误,而不是直接暴露底层英文通信超时
实际使用的密码会写入会话状态,并同步到侧边栏显示。
+1
View File
@@ -6256,6 +6256,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
isGeneratedAliasProvider,
isReusableGeneratedAliasEmail,
isSignupEmailVerificationPageUrl,
isRetryableContentScriptTransportError,
isHotmailProvider,
isLuckmailProvider,
isSignupPasswordPageUrl,
+15 -1
View File
@@ -3,6 +3,7 @@
})(typeof self !== 'undefined' ? self : globalThis, function createSignupFlowHelpersModule() {
function createSignupFlowHelpers(deps = {}) {
const {
addLog,
buildGeneratedAliasEmail,
chrome,
ensureContentScriptReadyOnTab,
@@ -12,6 +13,7 @@
isGeneratedAliasProvider,
isReusableGeneratedAliasEmail,
isHotmailProvider,
isRetryableContentScriptTransportError = () => false,
isLuckmailProvider,
isSignupEmailVerificationPageUrl,
isSignupPasswordPageUrl,
@@ -164,7 +166,9 @@
logMessage: `步骤 ${step}:认证页仍在切换,正在等待页面恢复后继续确认提交流程...`,
});
const result = await sendToContentScriptResilient('signup-page', {
let result;
try {
result = await sendToContentScriptResilient('signup-page', {
type: 'PREPARE_SIGNUP_VERIFICATION',
step,
source: 'background',
@@ -178,6 +182,16 @@
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) {
throw new Error(result.error);
+36 -6
View File
@@ -376,6 +376,17 @@
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) {
const parts = [source || 'unknown', message?.type || 'UNKNOWN'];
if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`);
@@ -439,7 +450,13 @@
pendingCommands.delete(source);
reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
}, 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)`);
});
}
@@ -449,7 +466,7 @@
if (pending) {
clearTimeout(pending.timer);
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})`);
}
}
@@ -564,13 +581,13 @@
if (!entry || !entry.ready) {
throwIfStopped();
return queueCommand(source, message);
return queueCommand(source, message, responseTimeoutMs);
}
const alive = await isTabAlive(source);
throwIfStopped();
if (!alive) {
return queueCommand(source, message);
return queueCommand(source, message, responseTimeoutMs);
}
throwIfStopped();
@@ -592,12 +609,18 @@
while (Date.now() - start < timeoutMs) {
throwIfStopped();
attempt += 1;
const remainingTimeoutMs = Math.max(1, timeoutMs - (Date.now() - start));
const effectiveResponseTimeoutMs = resolveResponseTimeoutMs(
message,
responseTimeoutMs,
remainingTimeoutMs
);
try {
return await sendToContentScript(
source,
message,
responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
{ responseTimeoutMs: effectiveResponseTimeoutMs }
);
} catch (err) {
const retryable = isRetryableContentScriptTransportError(err);
@@ -631,12 +654,18 @@
while (Date.now() - start < timeoutMs) {
throwIfStopped();
const remainingTimeoutMs = Math.max(1, timeoutMs - (Date.now() - start));
const effectiveResponseTimeoutMs = resolveResponseTimeoutMs(
message,
responseTimeoutMs,
remainingTimeoutMs
);
try {
return await sendToContentScript(
mail.source,
message,
responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
{ responseTimeoutMs: effectiveResponseTimeoutMs }
);
} catch (err) {
if (!isRetryableContentScriptTransportError(err)) {
@@ -684,6 +713,7 @@
queueCommand,
registerTab,
rememberSourceLastUrl,
resolveResponseTimeoutMs,
reuseOrCreateTab,
sendTabMessageWithTimeout,
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 () => {
let ensureCalls = 0;
const messages = [];
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 (...args) => {
@@ -188,6 +192,7 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification
isGeneratedAliasProvider: () => false,
isReusableGeneratedAliasEmail: () => false,
isHotmailProvider: () => false,
isRetryableContentScriptTransportError: () => false,
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
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.equal(ensureCalls, 1);
assert.deepStrictEqual(logs, []);
assert.deepStrictEqual(messages.find((item) => item.type === 'send')?.message, {
type: 'PREPARE_SIGNUP_VERIFICATION',
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');
});
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 () => {
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
const globalScope = {};
+1
View File
@@ -312,6 +312,7 @@
6. 上报完成后再异步点击提交,避免页面跳转打断响应通道
7. 延迟提交真正触发前会再次检查 Stop 状态,避免用户已停止时页面仍继续自动提交
8. 后台在真正确认 Step 3 完成前,会额外检查提交后是否切换页面;如果出现认证页 `Try again / 重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路
9. Step 3 收尾阶段如果页面切换导致旧内容脚本失联,后台会把单次消息等待收口到当前收尾预算内,优先尽快重试重连;若最终仍未恢复,则输出中文的步骤级错误,而不是直接暴露底层英文通信超时
### Step 4 / Step 8