feat:重试页面检测和重试点击的逻辑抽取为公共逻辑
This commit is contained in:
@@ -477,6 +477,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
- 使用第 2 步已经确定好的邮箱
|
||||
- 使用自定义密码或自动生成密码
|
||||
- 在密码页填写密码并提交注册表单
|
||||
- 后台会在真正把 Step 3 记为完成前,再确认页面是否已经推进;如果此时出现认证页 `重试` 页面,会先自动点击 `重试` 恢复,再继续后续链路
|
||||
|
||||
实际使用的密码会写入会话状态,并同步到侧边栏显示。
|
||||
|
||||
@@ -484,7 +485,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
|
||||
根据 `Mail` 配置,轮询邮箱并提取 6 位验证码。
|
||||
|
||||
进入邮箱轮询前,脚本会先确认认证页是否已经进入验证码页面;如果密码页出现 `糟糕,出错了 / 操作超时(Operation timed out)` 并带有 `重试` 按钮,会先自动点击 `重试`、回到密码页重新提交,再继续等待验证码页面。
|
||||
进入邮箱轮询前,脚本会先确认认证页是否已经进入验证码页面;如果密码页出现 `糟糕,出错了 / 操作超时(Operation timed out)` 并带有 `重试` 按钮,会先通过共享恢复逻辑自动点击 `重试`、回到密码页重新提交,再继续等待验证码页面。
|
||||
|
||||
支持:
|
||||
|
||||
@@ -520,7 +521,8 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
- 直接删除 `chatgpt.com / openai.com` 相关 cookies
|
||||
- 再去 CPA / SUB2API 刷新最新 OAuth 链接
|
||||
- 认证页已经真正进入登录验证码页面
|
||||
- 如遇登录超时报错或登录页长时间停滞,会由后台刷新 OAuth 后重跑整个 Step 6
|
||||
- 如遇登录超时报错,会先尝试自动点击认证页上的 `重试` 恢复当前页面,再由后台按既有逻辑重跑整个 Step 6
|
||||
- 如遇登录页长时间停滞,会由后台刷新 OAuth 后重跑整个 Step 6
|
||||
|
||||
支持:
|
||||
|
||||
@@ -554,6 +556,7 @@ Step 7 默认要求当前认证页已经处于登录验证码页。
|
||||
- 等待按钮可点击
|
||||
- 获取按钮坐标
|
||||
- 通过 Chrome `debugger` 的输入事件点击该按钮
|
||||
- 点击后会持续检查页面是否真正离开当前状态;如果出现认证页 `重试` 页面,会先自动点击 `重试` 恢复,再重新执行当前轮的“继续”点击
|
||||
- 同时监听 `chrome.webNavigation.onBeforeNavigate`
|
||||
- 一旦捕获本地回调地址,就把结果保存到 `Callback`
|
||||
|
||||
|
||||
+72
-1
@@ -5465,7 +5465,7 @@ async function resumeAutoRun() {
|
||||
// ============================================================
|
||||
|
||||
const SIGNUP_ENTRY_URL = 'https://chatgpt.com/';
|
||||
const SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/signup-page.js'];
|
||||
const SIGNUP_PAGE_INJECT_FILES = ['content/utils.js', 'content/auth-page-recovery.js', 'content/signup-page.js'];
|
||||
const panelBridge = self.MultiPageBackgroundPanelBridge?.createPanelBridge({
|
||||
chrome,
|
||||
addLog,
|
||||
@@ -5684,6 +5684,15 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
executeStepViaCompletionSignal,
|
||||
exportSettingsBundle,
|
||||
fetchGeneratedEmail,
|
||||
finalizeStep3Completion: async () => {
|
||||
const currentState = await getState();
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
return signupFlowHelpers.finalizeSignupPasswordSubmitInTab(
|
||||
signupTabId,
|
||||
currentState.password || currentState.customPassword || '',
|
||||
3
|
||||
);
|
||||
},
|
||||
finalizeIcloudAliasAfterSuccessfulFlow,
|
||||
findHotmailAccount,
|
||||
flushCommand,
|
||||
@@ -6308,6 +6317,7 @@ async function getStep8PageState(tabId, responseTimeoutMs = 1500) {
|
||||
async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) {
|
||||
const start = Date.now();
|
||||
let recovered = false;
|
||||
let retryRecovered = false;
|
||||
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
throwIfStopped();
|
||||
@@ -6315,7 +6325,21 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS)
|
||||
if (pageState?.addPhonePage) {
|
||||
throw new Error('步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
|
||||
}
|
||||
if (pageState?.retryPage) {
|
||||
await recoverAuthRetryPageOnTab(tabId, {
|
||||
flow: 'auth',
|
||||
logLabel: '步骤 8:检测到认证页重试页,正在点击“重试”恢复',
|
||||
step: 8,
|
||||
timeoutMs: Math.max(5000, Math.min(12000, timeoutMs)),
|
||||
});
|
||||
retryRecovered = true;
|
||||
await sleepWithStop(250);
|
||||
continue;
|
||||
}
|
||||
if (pageState?.consentReady) {
|
||||
if (retryRecovered) {
|
||||
await addLog('步骤 8:认证页重试页已恢复,准备重新定位“继续”按钮...', 'info');
|
||||
}
|
||||
return pageState;
|
||||
}
|
||||
if (pageState === null && !recovered) {
|
||||
@@ -6381,6 +6405,29 @@ async function triggerStep8ContentStrategy(tabId, strategy) {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function recoverAuthRetryPageOnTab(tabId, payload = {}, options = {}) {
|
||||
await ensureStep8SignupPageReady(tabId, {
|
||||
timeoutMs: options.readyTimeoutMs ?? 15000,
|
||||
retryDelayMs: options.retryDelayMs ?? 600,
|
||||
logMessage: options.readyLogMessage || '步骤 8:认证页内容脚本已失联,正在恢复后继续处理重试页...',
|
||||
});
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'RECOVER_AUTH_RETRY_PAGE',
|
||||
source: 'background',
|
||||
payload,
|
||||
}, {
|
||||
timeoutMs: options.timeoutMs ?? 15000,
|
||||
retryDelayMs: options.retryDelayMs ?? 600,
|
||||
logMessage: options.logMessage || '步骤 8:认证页正在切换,等待“重试”按钮重新就绪...',
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function reloadStep8ConsentPage(tabId, timeoutMs = 30000) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
throw new Error('步骤 8:缺少有效的认证页标签页,无法刷新后重试。');
|
||||
@@ -6443,6 +6490,20 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI
|
||||
if (pageState?.addPhonePage) {
|
||||
throw new Error('步骤 8:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。');
|
||||
}
|
||||
if (pageState?.retryPage) {
|
||||
await recoverAuthRetryPageOnTab(tabId, {
|
||||
flow: 'auth',
|
||||
logLabel: '步骤 8:点击“继续”后进入重试页,正在点击“重试”恢复',
|
||||
step: 8,
|
||||
timeoutMs: Math.max(5000, Math.min(12000, timeoutMs)),
|
||||
});
|
||||
return {
|
||||
progressed: false,
|
||||
reason: 'retry_page_recovered',
|
||||
restartCurrentStep: true,
|
||||
url: pageState.url || baselineUrl || '',
|
||||
};
|
||||
}
|
||||
if (pageState === null) {
|
||||
if (!recovered) {
|
||||
recovered = true;
|
||||
@@ -6457,6 +6518,14 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI
|
||||
}
|
||||
recovered = false;
|
||||
|
||||
if (pageState?.consentPage === false && !pageState?.verificationPage) {
|
||||
return {
|
||||
progressed: true,
|
||||
reason: 'left_consent_page',
|
||||
url: pageState.url || baselineUrl || '',
|
||||
};
|
||||
}
|
||||
|
||||
await sleepWithStop(200);
|
||||
}
|
||||
|
||||
@@ -6467,6 +6536,8 @@ function getStep8EffectLabel(effect) {
|
||||
switch (effect?.reason) {
|
||||
case 'url_changed':
|
||||
return `URL 已变化:${effect.url}`;
|
||||
case 'retry_page_recovered':
|
||||
return '页面进入重试页并已恢复,需要重新执行当前步骤';
|
||||
case 'page_reloading':
|
||||
return '页面正在跳转或重载';
|
||||
case 'left_consent_page':
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
executeStepViaCompletionSignal,
|
||||
exportSettingsBundle,
|
||||
fetchGeneratedEmail,
|
||||
finalizeStep3Completion,
|
||||
finalizeIcloudAliasAfterSuccessfulFlow,
|
||||
findHotmailAccount,
|
||||
flushCommand,
|
||||
@@ -218,6 +219,19 @@
|
||||
notifyStepError(message.step, '流程已被用户停止。');
|
||||
return { ok: true };
|
||||
}
|
||||
try {
|
||||
if (message.step === 3 && typeof finalizeStep3Completion === 'function') {
|
||||
await finalizeStep3Completion(message.payload || {});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error?.message || String(error || '步骤 3 提交后确认失败');
|
||||
await setStepStatus(message.step, 'failed');
|
||||
await addLog(`步骤 ${message.step} 失败:${errorMessage}`, 'error');
|
||||
await appendManualAccountRunRecordIfNeeded(`step${message.step}_failed`, null, errorMessage);
|
||||
notifyStepError(message.step, errorMessage);
|
||||
return { ok: true, error: errorMessage };
|
||||
}
|
||||
|
||||
const completionState = message.step === 9 ? await getState() : null;
|
||||
await setStepStatus(message.step, 'completed');
|
||||
await addLog(`步骤 ${message.step} 已完成`, 'ok');
|
||||
|
||||
@@ -150,6 +150,37 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
async function finalizeSignupPasswordSubmitInTab(tabId, password = '', step = 3) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
throw new Error(`认证页面标签页已关闭,无法完成步骤 ${step} 的提交后确认。`);
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||
inject: SIGNUP_PAGE_INJECT_FILES,
|
||||
injectSource: 'signup-page',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: `步骤 ${step}:认证页仍在切换,正在等待页面恢复后继续确认提交流程...`,
|
||||
});
|
||||
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'PREPARE_SIGNUP_VERIFICATION',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: { password: password || '' },
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${step}:密码已提交,正在确认是否进入下一页面,必要时自动恢复重试页...`,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function resolveSignupEmailForFlow(state) {
|
||||
let resolvedEmail = state.email;
|
||||
if (isHotmailProvider(state)) {
|
||||
@@ -182,6 +213,7 @@
|
||||
return {
|
||||
ensureSignupEntryPageReady,
|
||||
ensureSignupPostEmailPageReadyInTab,
|
||||
finalizeSignupPasswordSubmitInTab,
|
||||
ensureSignupPasswordPageReadyInTab,
|
||||
openSignupEntryTab,
|
||||
resolveSignupEmailForFlow,
|
||||
|
||||
@@ -152,6 +152,12 @@
|
||||
break;
|
||||
}
|
||||
|
||||
if (effect.restartCurrentStep) {
|
||||
await addLog(`步骤 8:${getStep8EffectLabel(effect)},准备重新定位“继续”按钮并重试...`, 'warn');
|
||||
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (round >= STEP8_MAX_ROUNDS) {
|
||||
throw new Error(`步骤 8:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
(function authPageRecoveryModule(root, factory) {
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = factory();
|
||||
return;
|
||||
}
|
||||
|
||||
root.MultiPageAuthPageRecovery = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createAuthPageRecoveryModule() {
|
||||
function createAuthPageRecovery(deps = {}) {
|
||||
const {
|
||||
detailPattern = null,
|
||||
getActionText,
|
||||
getPageTextSnapshot,
|
||||
humanPause,
|
||||
isActionEnabled,
|
||||
isVisibleElement,
|
||||
log,
|
||||
simulateClick,
|
||||
sleep,
|
||||
throwIfStopped,
|
||||
titlePattern = null,
|
||||
} = deps;
|
||||
|
||||
function matchesPathPatterns(pathname, pathPatterns = []) {
|
||||
if (!Array.isArray(pathPatterns) || !pathPatterns.length) {
|
||||
return true;
|
||||
}
|
||||
return pathPatterns.some((pattern) => pattern instanceof RegExp && pattern.test(pathname));
|
||||
}
|
||||
|
||||
function getAuthRetryButton(options = {}) {
|
||||
const { allowDisabled = false } = options;
|
||||
const direct = document.querySelector('button[data-dd-action-name="Try again"]');
|
||||
if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
const candidates = document.querySelectorAll('button, [role="button"]');
|
||||
return Array.from(candidates).find((element) => {
|
||||
if (!isVisibleElement(element) || (!allowDisabled && !isActionEnabled(element))) {
|
||||
return false;
|
||||
}
|
||||
const text = typeof getActionText === 'function' ? getActionText(element) : '';
|
||||
return /重试|try\s+again/i.test(text);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getAuthTimeoutErrorPageState(options = {}) {
|
||||
const { pathPatterns = [] } = options;
|
||||
const pathname = location.pathname || '';
|
||||
if (!matchesPathPatterns(pathname, pathPatterns)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const retryButton = getAuthRetryButton({ allowDisabled: true });
|
||||
if (!retryButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = typeof getPageTextSnapshot === 'function' ? getPageTextSnapshot() : '';
|
||||
const title = typeof document !== 'undefined' ? String(document.title || '') : '';
|
||||
const titleMatched = titlePattern instanceof RegExp
|
||||
? titlePattern.test(text) || titlePattern.test(title)
|
||||
: false;
|
||||
const detailMatched = detailPattern instanceof RegExp
|
||||
? detailPattern.test(text)
|
||||
: false;
|
||||
|
||||
if (!titleMatched && !detailMatched) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
path: pathname,
|
||||
url: location.href,
|
||||
retryButton,
|
||||
retryEnabled: isActionEnabled(retryButton),
|
||||
titleMatched,
|
||||
detailMatched,
|
||||
};
|
||||
}
|
||||
|
||||
async function recoverAuthRetryPage(options = {}) {
|
||||
const {
|
||||
logLabel = '',
|
||||
pathPatterns = [],
|
||||
pollIntervalMs = 250,
|
||||
step = null,
|
||||
timeoutMs = 12000,
|
||||
waitAfterClickMs = 1200,
|
||||
} = options;
|
||||
const start = Date.now();
|
||||
let clickCount = 0;
|
||||
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (typeof throwIfStopped === 'function') {
|
||||
throwIfStopped();
|
||||
}
|
||||
|
||||
const retryState = getAuthTimeoutErrorPageState({ pathPatterns });
|
||||
if (!retryState) {
|
||||
return {
|
||||
recovered: clickCount > 0,
|
||||
clickCount,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
if (retryState.retryButton && retryState.retryEnabled) {
|
||||
clickCount += 1;
|
||||
if (typeof log === 'function') {
|
||||
const prefix = logLabel || `步骤 ${step || '?'}:检测到重试页,正在点击“重试”恢复`;
|
||||
log(`${prefix}(第 ${clickCount} 次)...`, 'warn');
|
||||
}
|
||||
if (typeof humanPause === 'function') {
|
||||
await humanPause(300, 800);
|
||||
}
|
||||
simulateClick(retryState.retryButton);
|
||||
await sleep(waitAfterClickMs);
|
||||
continue;
|
||||
}
|
||||
|
||||
await sleep(pollIntervalMs);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`${logLabel || `步骤 ${step || '?'}:重试页恢复`}超时。URL: ${location.href}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
getAuthRetryButton,
|
||||
getAuthTimeoutErrorPageState,
|
||||
recoverAuthRetryPage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createAuthPageRecovery,
|
||||
};
|
||||
});
|
||||
+158
-30
@@ -19,6 +19,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|
||||
|| message.type === 'STEP8_TRIGGER_CONTINUE'
|
||||
|| message.type === 'GET_LOGIN_AUTH_STATE'
|
||||
|| message.type === 'PREPARE_SIGNUP_VERIFICATION'
|
||||
|| message.type === 'RECOVER_AUTH_RETRY_PAGE'
|
||||
|| message.type === 'RESEND_VERIFICATION_CODE'
|
||||
|| message.type === 'ENSURE_SIGNUP_ENTRY_READY'
|
||||
|| message.type === 'ENSURE_SIGNUP_PASSWORD_PAGE_READY'
|
||||
@@ -71,6 +72,8 @@ async function handleCommand(message) {
|
||||
return serializeLoginAuthState(inspectLoginAuthState());
|
||||
case 'PREPARE_SIGNUP_VERIFICATION':
|
||||
return await prepareSignupVerificationFlow(message.payload);
|
||||
case 'RECOVER_AUTH_RETRY_PAGE':
|
||||
return await recoverCurrentAuthRetryPage(message.payload);
|
||||
case 'RESEND_VERIFICATION_CODE':
|
||||
return await resendVerificationCode(message.step);
|
||||
case 'ENSURE_SIGNUP_ENTRY_READY':
|
||||
@@ -626,6 +629,20 @@ const AUTH_TIMEOUT_ERROR_TITLE_PATTERN = /糟糕,出错了|something\s+went\s+
|
||||
const AUTH_TIMEOUT_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时/i;
|
||||
const SIGNUP_EMAIL_EXISTS_PATTERN = /与此电子邮件地址相关联的帐户已存在|account\s+associated\s+with\s+this\s+email\s+address\s+already\s+exists|email\s+address.*already\s+exists/i;
|
||||
|
||||
const authPageRecovery = self.MultiPageAuthPageRecovery?.createAuthPageRecovery?.({
|
||||
detailPattern: AUTH_TIMEOUT_ERROR_DETAIL_PATTERN,
|
||||
getActionText,
|
||||
getPageTextSnapshot,
|
||||
humanPause,
|
||||
isActionEnabled,
|
||||
isVisibleElement,
|
||||
log,
|
||||
simulateClick,
|
||||
sleep,
|
||||
throwIfStopped,
|
||||
titlePattern: AUTH_TIMEOUT_ERROR_TITLE_PATTERN,
|
||||
}) || null;
|
||||
|
||||
function getVerificationErrorText() {
|
||||
const messages = [];
|
||||
const selectors = [
|
||||
@@ -863,6 +880,10 @@ function getSignupPasswordSubmitButton({ allowDisabled = false } = {}) {
|
||||
}
|
||||
|
||||
function getAuthRetryButton({ allowDisabled = false } = {}) {
|
||||
if (authPageRecovery?.getAuthRetryButton) {
|
||||
return authPageRecovery.getAuthRetryButton({ allowDisabled });
|
||||
}
|
||||
|
||||
const direct = document.querySelector('button[data-dd-action-name="Try again"]');
|
||||
if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
|
||||
return direct;
|
||||
@@ -877,6 +898,10 @@ function getAuthRetryButton({ allowDisabled = false } = {}) {
|
||||
}
|
||||
|
||||
function getAuthTimeoutErrorPageState(options = {}) {
|
||||
if (authPageRecovery?.getAuthTimeoutErrorPageState) {
|
||||
return authPageRecovery.getAuthTimeoutErrorPageState(options);
|
||||
}
|
||||
|
||||
const { pathPatterns = [] } = options;
|
||||
const path = location.pathname || '';
|
||||
if (pathPatterns.length && !pathPatterns.some((pattern) => pattern.test(path))) {
|
||||
@@ -907,6 +932,70 @@ function getAuthTimeoutErrorPageState(options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function getAuthRetryPathPatternsForFlow(flow = 'auth') {
|
||||
switch (flow) {
|
||||
case 'signup_password':
|
||||
return [/\/create-account\/password(?:[/?#]|$)/i];
|
||||
case 'login':
|
||||
return [/\/log-in(?:[/?#]|$)/i];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentAuthRetryPageState(flow = 'auth') {
|
||||
return getAuthTimeoutErrorPageState({
|
||||
pathPatterns: getAuthRetryPathPatternsForFlow(flow),
|
||||
});
|
||||
}
|
||||
|
||||
async function recoverCurrentAuthRetryPage(payload = {}) {
|
||||
const {
|
||||
flow = 'auth',
|
||||
logLabel = '',
|
||||
step = null,
|
||||
timeoutMs = 12000,
|
||||
waitAfterClickMs = 1200,
|
||||
} = payload;
|
||||
const pathPatterns = getAuthRetryPathPatternsForFlow(flow);
|
||||
if (authPageRecovery?.recoverAuthRetryPage) {
|
||||
return authPageRecovery.recoverAuthRetryPage({
|
||||
logLabel,
|
||||
pathPatterns,
|
||||
step,
|
||||
timeoutMs,
|
||||
waitAfterClickMs,
|
||||
});
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
let clickCount = 0;
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
throwIfStopped();
|
||||
const retryState = getCurrentAuthRetryPageState(flow);
|
||||
if (!retryState) {
|
||||
return {
|
||||
recovered: clickCount > 0,
|
||||
clickCount,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
if (retryState.retryButton && retryState.retryEnabled) {
|
||||
clickCount += 1;
|
||||
log(`${logLabel || `步骤 ${step || '?'}:检测到重试页,正在点击“重试”恢复`}(第 ${clickCount} 次)...`, 'warn');
|
||||
await humanPause(300, 800);
|
||||
simulateClick(retryState.retryButton);
|
||||
await sleep(waitAfterClickMs);
|
||||
continue;
|
||||
}
|
||||
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
throw new Error(`${logLabel || `步骤 ${step || '?'}:重试页恢复`}超时。URL: ${location.href}`);
|
||||
}
|
||||
|
||||
function getSignupPasswordTimeoutErrorPageState() {
|
||||
return getAuthTimeoutErrorPageState({
|
||||
pathPatterns: [/\/create-account\/password(?:[/?#]|$)/i],
|
||||
@@ -1121,6 +1210,29 @@ function createStep6RecoverableResult(reason, snapshot, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message) {
|
||||
const resolvedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
|
||||
if (resolvedSnapshot?.state === 'login_timeout_error_page') {
|
||||
try {
|
||||
const recoveryResult = await recoverCurrentAuthRetryPage({
|
||||
flow: 'login',
|
||||
logLabel: '步骤 6:检测到登录超时报错,正在点击“重试”恢复当前页面',
|
||||
step: 6,
|
||||
timeoutMs: 12000,
|
||||
});
|
||||
if (recoveryResult?.recovered) {
|
||||
log('步骤 6:登录超时报错页已点击“重试”,准备重新执行当前步骤。', 'warn');
|
||||
}
|
||||
} catch (error) {
|
||||
log(`步骤 6:登录超时报错页自动点击“重试”失败:${error.message}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
return createStep6RecoverableResult(reason, resolvedSnapshot, {
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeStep6Snapshot(snapshot) {
|
||||
if (snapshot?.state !== 'oauth_consent_page') {
|
||||
return snapshot;
|
||||
@@ -1260,15 +1372,12 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
|
||||
recoveryRound += 1;
|
||||
|
||||
if (snapshot.state === 'error') {
|
||||
if (snapshot.retryButton && isActionEnabled(snapshot.retryButton)) {
|
||||
log(`步骤 4:检测到密码页超时报错,正在点击“重试”(第 ${recoveryRound}/${maxRecoveryRounds} 次)...`, 'warn');
|
||||
await humanPause(350, 900);
|
||||
simulateClick(snapshot.retryButton);
|
||||
await sleep(1200);
|
||||
continue;
|
||||
}
|
||||
|
||||
log(`步骤 4:检测到异常页,但“重试”按钮暂不可用,准备继续等待(${recoveryRound}/${maxRecoveryRounds})...`, 'warn');
|
||||
await recoverCurrentAuthRetryPage({
|
||||
flow: 'signup_password',
|
||||
logLabel: `步骤 4:检测到密码页超时报错,正在点击“重试”恢复(第 ${recoveryRound}/${maxRecoveryRounds} 次)`,
|
||||
step: 4,
|
||||
timeoutMs: 12000,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1459,9 +1568,11 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '提交邮箱后进入登录超时报错页。',
|
||||
}),
|
||||
result: await createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交邮箱后进入登录超时报错页。'
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1492,9 +1603,11 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '提交邮箱后进入登录超时报错页。',
|
||||
}),
|
||||
result: await createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交邮箱后进入登录超时报错页。'
|
||||
),
|
||||
};
|
||||
}
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
@@ -1533,9 +1646,11 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '提交密码后进入登录超时报错页。',
|
||||
}),
|
||||
result: await createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交密码后进入登录超时报错页。'
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1563,9 +1678,11 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return {
|
||||
action: 'recoverable',
|
||||
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '提交密码后进入登录超时报错页。',
|
||||
}),
|
||||
result: await createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'提交密码后进入登录超时报错页。'
|
||||
),
|
||||
};
|
||||
}
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
@@ -1602,9 +1719,11 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou
|
||||
}
|
||||
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '切换到一次性验证码登录后进入登录超时报错页。',
|
||||
});
|
||||
return await createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'切换到一次性验证码登录后进入登录超时报错页。'
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
@@ -1626,9 +1745,11 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou
|
||||
});
|
||||
}
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
return createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '切换到一次性验证码登录后进入登录超时报错页。',
|
||||
});
|
||||
return await createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'切换到一次性验证码登录后进入登录超时报错页。'
|
||||
);
|
||||
}
|
||||
if (snapshot.state === 'oauth_consent_page') {
|
||||
throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
|
||||
@@ -1757,9 +1878,11 @@ async function step6_login(payload) {
|
||||
|
||||
if (snapshot.state === 'login_timeout_error_page') {
|
||||
log('步骤 6:检测到登录超时报错,准备重新执行步骤 6。', 'warn');
|
||||
return createStep6RecoverableResult('login_timeout_error_page', snapshot, {
|
||||
message: '当前页面处于登录超时报错页。',
|
||||
});
|
||||
return await createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
snapshot,
|
||||
'当前页面处于登录超时报错页。'
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.state === 'email_page') {
|
||||
@@ -1797,12 +1920,17 @@ async function step8_findAndClick() {
|
||||
|
||||
function getStep8State() {
|
||||
const continueBtn = getPrimaryContinueButton();
|
||||
const retryState = getCurrentAuthRetryPageState('auth');
|
||||
const state = {
|
||||
url: location.href,
|
||||
consentPage: isOAuthConsentPage(),
|
||||
consentReady: isStep8Ready(),
|
||||
verificationPage: isVerificationPageStillVisible(),
|
||||
addPhonePage: isAddPhonePageReady(),
|
||||
retryPage: Boolean(retryState),
|
||||
retryEnabled: Boolean(retryState?.retryEnabled),
|
||||
retryTitleMatched: Boolean(retryState?.titleMatched),
|
||||
retryDetailMatched: Boolean(retryState?.detailMatched),
|
||||
buttonFound: Boolean(continueBtn),
|
||||
buttonEnabled: isButtonEnabled(continueBtn),
|
||||
buttonText: continueBtn ? getActionText(continueBtn) : '',
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"js": [
|
||||
"content/activation-utils.js",
|
||||
"content/utils.js",
|
||||
"content/auth-page-recovery.js",
|
||||
"content/signup-page.js"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { createAuthPageRecovery } = require('../content/auth-page-recovery.js');
|
||||
|
||||
function createRetryButton() {
|
||||
return {
|
||||
disabled: false,
|
||||
textContent: 'Try again',
|
||||
getAttribute(name) {
|
||||
if (name === 'data-dd-action-name') return 'Try again';
|
||||
if (name === 'aria-disabled') return 'false';
|
||||
return '';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createRecoveryApi(state) {
|
||||
const retryButton = createRetryButton();
|
||||
global.location = {
|
||||
pathname: '/log-in',
|
||||
href: 'https://auth.openai.com/log-in',
|
||||
};
|
||||
global.document = {
|
||||
title: 'Something went wrong',
|
||||
querySelector(selector) {
|
||||
if (selector === 'button[data-dd-action-name="Try again"]' && state.retryVisible) {
|
||||
return retryButton;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
querySelectorAll() {
|
||||
return state.retryVisible ? [retryButton] : [];
|
||||
},
|
||||
};
|
||||
|
||||
return createAuthPageRecovery({
|
||||
detailPattern: /timed out/i,
|
||||
getActionText: (element) => element?.textContent || '',
|
||||
getPageTextSnapshot: () => state.pageText,
|
||||
humanPause: async () => {},
|
||||
isActionEnabled: (element) => Boolean(element) && !element.disabled && element.getAttribute('aria-disabled') !== 'true',
|
||||
isVisibleElement: () => true,
|
||||
log: () => {},
|
||||
simulateClick: () => {
|
||||
state.clickCount += 1;
|
||||
state.retryVisible = false;
|
||||
state.pageText = 'Recovered login form';
|
||||
},
|
||||
sleep: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
titlePattern: /something went wrong/i,
|
||||
});
|
||||
}
|
||||
|
||||
test('auth page recovery detects retry page state', () => {
|
||||
const state = {
|
||||
clickCount: 0,
|
||||
pageText: 'Something went wrong. Operation timed out.',
|
||||
retryVisible: true,
|
||||
};
|
||||
const api = createRecoveryApi(state);
|
||||
|
||||
const snapshot = api.getAuthTimeoutErrorPageState({
|
||||
pathPatterns: [/\/log-in(?:[/?#]|$)/i],
|
||||
});
|
||||
|
||||
assert.equal(Boolean(snapshot), true);
|
||||
assert.equal(snapshot.retryEnabled, true);
|
||||
assert.equal(snapshot.titleMatched, true);
|
||||
assert.equal(snapshot.detailMatched, true);
|
||||
});
|
||||
|
||||
test('auth page recovery clicks retry and waits until page recovers', async () => {
|
||||
const state = {
|
||||
clickCount: 0,
|
||||
pageText: 'Something went wrong. Operation timed out.',
|
||||
retryVisible: true,
|
||||
};
|
||||
const api = createRecoveryApi(state);
|
||||
|
||||
const result = await api.recoverAuthRetryPage({
|
||||
logLabel: '步骤 8:检测到重试页,正在点击“重试”恢复',
|
||||
pathPatterns: [/\/log-in(?:[/?#]|$)/i],
|
||||
step: 8,
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
recovered: true,
|
||||
clickCount: 1,
|
||||
url: 'https://auth.openai.com/log-in',
|
||||
});
|
||||
assert.equal(state.clickCount, 1);
|
||||
assert.equal(state.retryVisible, false);
|
||||
});
|
||||
@@ -11,6 +11,9 @@ function createRouter(overrides = {}) {
|
||||
logs: [],
|
||||
stepStatuses: [],
|
||||
emailStates: [],
|
||||
finalizePayloads: [],
|
||||
notifyCompletions: [],
|
||||
notifyErrors: [],
|
||||
};
|
||||
|
||||
const router = api.createMessageRouter({
|
||||
@@ -41,6 +44,9 @@ function createRouter(overrides = {}) {
|
||||
executeStepViaCompletionSignal: async () => {},
|
||||
exportSettingsBundle: async () => ({}),
|
||||
fetchGeneratedEmail: async () => '',
|
||||
finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => {
|
||||
events.finalizePayloads.push(payload);
|
||||
}),
|
||||
finalizeIcloudAliasAfterSuccessfulFlow: async () => {},
|
||||
findHotmailAccount: async () => null,
|
||||
flushCommand: async () => {},
|
||||
@@ -63,8 +69,12 @@ function createRouter(overrides = {}) {
|
||||
normalizeHotmailAccounts: (items) => items,
|
||||
normalizeRunCount: (value) => value,
|
||||
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
|
||||
notifyStepComplete: () => {},
|
||||
notifyStepError: () => {},
|
||||
notifyStepComplete: (step, payload) => {
|
||||
events.notifyCompletions.push({ step, payload });
|
||||
},
|
||||
notifyStepError: (step, error) => {
|
||||
events.notifyErrors.push({ step, error });
|
||||
},
|
||||
patchHotmailAccount: async () => {},
|
||||
registerTab: async () => {},
|
||||
requestStop: async () => {},
|
||||
@@ -125,3 +135,63 @@ test('message router does not overwrite a completed step 3 when step 2 is replay
|
||||
|
||||
assert.deepStrictEqual(events.stepStatuses, []);
|
||||
});
|
||||
|
||||
test('message router finalizes step 3 before marking it completed', async () => {
|
||||
const { router, events } = createRouter();
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'STEP_COMPLETE',
|
||||
step: 3,
|
||||
source: 'signup-page',
|
||||
payload: {
|
||||
email: 'user@example.com',
|
||||
signupVerificationRequestedAt: 123,
|
||||
},
|
||||
}, {});
|
||||
|
||||
assert.deepStrictEqual(events.finalizePayloads, [
|
||||
{
|
||||
email: 'user@example.com',
|
||||
signupVerificationRequestedAt: 123,
|
||||
},
|
||||
]);
|
||||
assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'completed' }]);
|
||||
assert.deepStrictEqual(events.emailStates, ['user@example.com']);
|
||||
assert.deepStrictEqual(events.notifyCompletions, [
|
||||
{
|
||||
step: 3,
|
||||
payload: {
|
||||
email: 'user@example.com',
|
||||
signupVerificationRequestedAt: 123,
|
||||
},
|
||||
},
|
||||
]);
|
||||
assert.deepStrictEqual(response, { ok: true });
|
||||
});
|
||||
|
||||
test('message router marks step 3 failed when post-submit finalize fails', async () => {
|
||||
const { router, events } = createRouter({
|
||||
finalizeStep3Completion: async () => {
|
||||
throw new Error('步骤 3 提交后仍停留在密码页。');
|
||||
},
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'STEP_COMPLETE',
|
||||
step: 3,
|
||||
source: 'signup-page',
|
||||
payload: {
|
||||
email: 'user@example.com',
|
||||
},
|
||||
}, {});
|
||||
|
||||
assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'failed' }]);
|
||||
assert.deepStrictEqual(events.notifyErrors, [
|
||||
{
|
||||
step: 3,
|
||||
error: '步骤 3 提交后仍停留在密码页。',
|
||||
},
|
||||
]);
|
||||
assert.equal(events.logs.some(({ message }) => /步骤 3 失败:步骤 3 提交后仍停留在密码页。/.test(message)), true);
|
||||
assert.deepStrictEqual(response, { ok: true, error: '步骤 3 提交后仍停留在密码页。' });
|
||||
});
|
||||
|
||||
@@ -171,3 +171,45 @@ test('signup flow helper reuses existing managed alias email when it is still co
|
||||
assert.equal(buildCalls, 0);
|
||||
assert.equal(setEmailCalls, 0);
|
||||
});
|
||||
|
||||
test('signup flow helper finalizes step 3 submit by reusing signup verification preparation', async () => {
|
||||
let ensureCalls = 0;
|
||||
const messages = [];
|
||||
|
||||
const helpers = signupFlowApi.createSignupFlowHelpers({
|
||||
buildGeneratedAliasEmail: () => '',
|
||||
chrome: { tabs: { get: async () => ({ id: 31, url: 'https://auth.openai.com/create-account/password' }) } },
|
||||
ensureContentScriptReadyOnTab: async (...args) => {
|
||||
ensureCalls += 1;
|
||||
messages.push({ type: 'ensure', args });
|
||||
},
|
||||
ensureHotmailAccountForFlow: async () => ({}),
|
||||
ensureLuckmailPurchaseForFlow: async () => ({}),
|
||||
isGeneratedAliasProvider: () => false,
|
||||
isReusableGeneratedAliasEmail: () => false,
|
||||
isHotmailProvider: () => false,
|
||||
isLuckmailProvider: () => false,
|
||||
isSignupEmailVerificationPageUrl: () => false,
|
||||
isSignupPasswordPageUrl: () => true,
|
||||
reuseOrCreateTab: async () => 31,
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
messages.push({ type: 'send', message });
|
||||
return { ready: true, retried: 1 };
|
||||
},
|
||||
setEmailState: async () => {},
|
||||
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
|
||||
SIGNUP_PAGE_INJECT_FILES: ['content/utils.js', 'content/signup-page.js'],
|
||||
waitForTabUrlMatch: async () => null,
|
||||
});
|
||||
|
||||
const result = await helpers.finalizeSignupPasswordSubmitInTab(31, 'Secret123!', 3);
|
||||
|
||||
assert.deepStrictEqual(result, { ready: true, retried: 1 });
|
||||
assert.equal(ensureCalls, 1);
|
||||
assert.deepStrictEqual(messages.find((item) => item.type === 'send')?.message, {
|
||||
type: 'PREPARE_SIGNUP_VERIFICATION',
|
||||
step: 3,
|
||||
source: 'background',
|
||||
payload: { password: 'Secret123!' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('content/signup-page.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let index = start; index < source.length; index += 1) {
|
||||
const char = source[index];
|
||||
if (char === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (char === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (char === '{' && signatureEnded) {
|
||||
braceStart = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const char = source[end];
|
||||
if (char === '{') depth += 1;
|
||||
if (char === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('step 6 timeout recoverable result clicks retry before asking background to rerun', async () => {
|
||||
const api = new Function(`
|
||||
const logs = [];
|
||||
let recoverCalls = 0;
|
||||
|
||||
const location = {
|
||||
href: 'https://auth.openai.com/log-in',
|
||||
};
|
||||
|
||||
function inspectLoginAuthState() {
|
||||
return {
|
||||
state: 'login_timeout_error_page',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
async function recoverCurrentAuthRetryPage() {
|
||||
recoverCalls += 1;
|
||||
return { recovered: true };
|
||||
}
|
||||
|
||||
function log(message, level = 'info') {
|
||||
logs.push({ message, level });
|
||||
}
|
||||
|
||||
${extractFunction('createStep6RecoverableResult')}
|
||||
${extractFunction('normalizeStep6Snapshot')}
|
||||
${extractFunction('createStep6LoginTimeoutRecoverableResult')}
|
||||
|
||||
return {
|
||||
async run() {
|
||||
return createStep6LoginTimeoutRecoverableResult(
|
||||
'login_timeout_error_page',
|
||||
{ state: 'login_timeout_error_page', url: location.href },
|
||||
'当前页面处于登录超时报错页。'
|
||||
);
|
||||
},
|
||||
snapshot() {
|
||||
return { logs, recoverCalls };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.run();
|
||||
const snapshot = api.snapshot();
|
||||
|
||||
assert.equal(snapshot.recoverCalls, 1);
|
||||
assert.equal(result.step6Outcome, 'recoverable');
|
||||
assert.equal(result.reason, 'login_timeout_error_page');
|
||||
assert.equal(result.state, 'login_timeout_error_page');
|
||||
assert.equal(result.message, '当前页面处于登录超时报错页。');
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let index = start; index < source.length; index += 1) {
|
||||
const char = source[index];
|
||||
if (char === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (char === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (char === '{' && signatureEnded) {
|
||||
braceStart = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const char = source[end];
|
||||
if (char === '{') depth += 1;
|
||||
if (char === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('step 8 click effect returns restart_current_step when retry page is recovered', async () => {
|
||||
const api = new Function(`
|
||||
let recoverCalls = 0;
|
||||
|
||||
const chrome = {
|
||||
tabs: {
|
||||
async get() {
|
||||
return {
|
||||
id: 88,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function throwIfStopped() {}
|
||||
async function sleepWithStop() {}
|
||||
async function ensureStep8SignupPageReady() {}
|
||||
async function recoverAuthRetryPageOnTab() {
|
||||
recoverCalls += 1;
|
||||
return { recovered: true };
|
||||
}
|
||||
async function getStep8PageState() {
|
||||
return {
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
retryPage: true,
|
||||
addPhonePage: false,
|
||||
consentPage: false,
|
||||
verificationPage: false,
|
||||
};
|
||||
}
|
||||
|
||||
${extractFunction('waitForStep8ClickEffect')}
|
||||
|
||||
return {
|
||||
async run() {
|
||||
return waitForStep8ClickEffect(88, 'https://auth.openai.com/authorize', 1000);
|
||||
},
|
||||
snapshot() {
|
||||
return { recoverCalls };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.run();
|
||||
const snapshot = api.snapshot();
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
progressed: false,
|
||||
reason: 'retry_page_recovered',
|
||||
restartCurrentStep: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
assert.equal(snapshot.recoverCalls, 1);
|
||||
});
|
||||
@@ -224,6 +224,7 @@
|
||||
4. 让内容脚本填写密码
|
||||
5. 内容脚本先上报 Step 3 完成信号
|
||||
6. 上报完成后再异步点击提交,避免页面跳转打断响应通道
|
||||
7. 后台在真正确认 Step 3 完成前,会额外检查提交后是否切换页面;如果出现认证页 `Try again / 重试` 页面,会先自动点击 `重试` 恢复,再继续后续链路
|
||||
|
||||
### Step 4 / Step 7
|
||||
|
||||
@@ -463,3 +464,9 @@
|
||||
更新 [项目完整链路说明.md](c:/Users/projectf/Downloads/codex注册扩展/项目完整链路说明.md)
|
||||
- 规范、边界、步骤接入方式变更
|
||||
更新 [项目开发规范(AI协作).md](c:/Users/projectf/Downloads/codex注册扩展/项目开发规范(AI协作).md)
|
||||
## 2026-04 链路补充:认证页共享恢复
|
||||
|
||||
- 新增共享恢复层:`content/auth-page-recovery.js`。
|
||||
- Step 4 在等待注册验证码页时,如果命中认证页 `Try again / 重试` 页,会先自动点击 `重试` 恢复,再继续回到密码页重提和验证码页确认流程。
|
||||
- Step 6 在识别到登录超时报错页时,会先尝试自动点击 `重试` 恢复当前页面,再按原有有限重试逻辑重新执行整步。
|
||||
- Step 8 在点击 OAuth 同意页 `继续` 后,会额外检查是否进入认证页重试页;若命中则先自动点击 `重试` 恢复,再重新执行当前轮的 `继续` 点击。
|
||||
|
||||
@@ -208,3 +208,9 @@
|
||||
- `tests/step9-status-diagnostics.test.js`閿涙碍绁寸拠?Step 9 缁墽鈥橀幋鎰閸掋倕鐣鹃妴浣哄閼规煡鏁婄拠顖涒偓浣界箖濠娿倧绱濇禒銉ュ挤閹存劕濮涘鑺ョ垼娑撳骸銇戠拹銉﹀絹缁€鍝勮嫙鐎涙ɑ妞傞惃鍕喘閸忓牏楠囬妴?
|
||||
- `tests/verification-stop-propagation.test.js`閿涙碍绁寸拠鏇㈢崣鐠囦胶鐖滃ù浣衡柤閸?Stop 閸︾儤娅欐稉瀣畱闁挎瑨顕ゆ导鐘虫尡娑撳簼绗夋稉顓⑩偓鏃堟缁狙佲偓?
|
||||
- `tests/verification-flow-polling.test.js`閿涙碍绁寸拠?2925 闂€鑳枂鐠囥垹寮弫甯礉娴犮儱寮锋宀冪槈閻焦褰佹禍銈嗙ウ缁嬪鑵戦惃?`beforeSubmit` 闁解晛鐡欓幍褑顢戞い鍝勭碍閵?
|
||||
## 2026-04 结构补充
|
||||
|
||||
- `content/auth-page-recovery.js`
|
||||
- 认证页共享恢复模块。
|
||||
- 统一封装 `Try again / 重试` 页识别、点击重试、等待退出重试页。
|
||||
- 当前由 Step 4 / Step 6 / Step 8 复用。
|
||||
|
||||
Reference in New Issue
Block a user