feat(signup): 优化步骤 3 收尾阶段的错误处理与日志记录
This commit is contained in:
@@ -550,6 +550,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
- 使用自定义密码或自动生成密码
|
||||
- 在密码页填写密码并提交注册表单
|
||||
- 后台会在真正把 Step 3 记为完成前,再确认页面是否已经推进;如果此时出现认证页 `重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路
|
||||
- Step 3 收尾阶段如果页面切换导致旧内容脚本失联,后台单次消息等待不会再卡住超过当前收尾预算;若最终仍未恢复,则会输出中文的步骤级错误,而不是直接暴露底层英文通信超时
|
||||
|
||||
实际使用的密码会写入会话状态,并同步到侧边栏显示。
|
||||
|
||||
|
||||
@@ -6256,6 +6256,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
|
||||
isGeneratedAliasProvider,
|
||||
isReusableGeneratedAliasEmail,
|
||||
isSignupEmailVerificationPageUrl,
|
||||
isRetryableContentScriptTransportError,
|
||||
isHotmailProvider,
|
||||
isLuckmailProvider,
|
||||
isSignupPasswordPageUrl,
|
||||
|
||||
@@ -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,20 +166,32 @@
|
||||
logMessage: `步骤 ${step}:认证页仍在切换,正在等待页面恢复后继续确认提交流程...`,
|
||||
});
|
||||
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'PREPARE_SIGNUP_VERIFICATION',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: {
|
||||
password: password || '',
|
||||
prepareSource: 'step3_finalize',
|
||||
prepareLogLabel: '步骤 3 收尾',
|
||||
},
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${step}:密码已提交,正在确认是否进入下一页面,必要时自动恢复重试页...`,
|
||||
});
|
||||
let result;
|
||||
try {
|
||||
result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'PREPARE_SIGNUP_VERIFICATION',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: {
|
||||
password: password || '',
|
||||
prepareSource: 'step3_finalize',
|
||||
prepareLogLabel: '步骤 3 收尾',
|
||||
},
|
||||
}, {
|
||||
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) {
|
||||
throw new Error(result.error);
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user