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 记为完成前,再确认页面是否已经推进;如果此时出现认证页 `重试` 页面,或 `/email-verification` 上的 `405 / Route Error` 重试页,会先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再继续后续链路
|
||||||
|
- Step 3 收尾阶段如果页面切换导致旧内容脚本失联,后台单次消息等待不会再卡住超过当前收尾预算;若最终仍未恢复,则会输出中文的步骤级错误,而不是直接暴露底层英文通信超时
|
||||||
|
|
||||||
实际使用的密码会写入会话状态,并同步到侧边栏显示。
|
实际使用的密码会写入会话状态,并同步到侧边栏显示。
|
||||||
|
|
||||||
|
|||||||
@@ -6256,6 +6256,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
|
|||||||
isGeneratedAliasProvider,
|
isGeneratedAliasProvider,
|
||||||
isReusableGeneratedAliasEmail,
|
isReusableGeneratedAliasEmail,
|
||||||
isSignupEmailVerificationPageUrl,
|
isSignupEmailVerificationPageUrl,
|
||||||
|
isRetryableContentScriptTransportError,
|
||||||
isHotmailProvider,
|
isHotmailProvider,
|
||||||
isLuckmailProvider,
|
isLuckmailProvider,
|
||||||
isSignupPasswordPageUrl,
|
isSignupPasswordPageUrl,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 = {};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user