feat: Enhance Step 9 diagnostics and error handling

- Refactor getStatusBadgeEntries to use createStep9Entry for better encapsulation.
- Introduce error visual signals detection in createStep9Entry.
- Add functions to identify OAuth callback timeout failures and step 9 failure texts.
- Update buildStep9StatusDiagnostics to include error visual summaries and handle conflicts between success and failure states.
- Implement new tests for step 9 diagnostics, ensuring correct behavior with success badges and error banners.
- Remove outdated tests related to step 5 onboarding and redirect race conditions.
- Add new tests for auto-run behavior in step 6 restart scenarios.
This commit is contained in:
QLHazyCoder
2026-04-17 04:33:14 +08:00
parent 1a5f569d16
commit c745405f90
17 changed files with 772 additions and 691 deletions
+3 -3
View File
@@ -415,7 +415,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
2. 根据 `Mail` 选择邮箱来源
3. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号
4. 如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取邮箱(Duck / Cloudflare / iCloud 等)
5. Step 2 点击注册、填写邮箱并继续到密码页
5. Step 2 点击注册、填写邮箱,并按真实落地页进入密码页或直接进入邮箱验证码页
6. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue`
7. 继续执行 Step 3 ~ Step 9
@@ -447,7 +447,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
- 自动点击进入注册流程
- 自动填写邮箱
- 点击 `继续`
- 等待跳转到 `https://auth.openai.com/create-account/password`
- 等待真实落地页;进入 `https://auth.openai.com/create-account/password` 时继续 Step 3,进入 `https://auth.openai.com/email-verification` 时自动跳过 Step 3 直接进入 Step 4
### Step 3: Fill Password
@@ -485,7 +485,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
- 页面要求 `age`
如果页面是生日模式,会填写年月日;如果页面上存在 `input[name='age']`,则直接填写年龄。
点击 `完成帐户创建` 后,Step 5 会立刻记为完成,不再等待页面跳转结果;只有在提交瞬间发生页面跳转、且后台还没收到完成信号时,才会兜底检查是否已跳到 ChatGPT
点击 `完成帐户创建` 后,Step 5 会立刻记为完成,不再等待页面跳转结果;自动运行在进入 Step 6 前只会等待当前页面加载完成,不再接管 ChatGPT 跳转或 onboarding 跳过逻辑
### Step 6: Login via OAuth
+100 -107
View File
@@ -3403,6 +3403,10 @@ async function waitForTabUrlMatch(tabId, matcher, options = {}) {
return tabRuntime.waitForTabUrlMatch(tabId, matcher, options);
}
async function waitForTabComplete(tabId, options = {}) {
return tabRuntime.waitForTabComplete(tabId, options);
}
async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options);
}
@@ -3631,7 +3635,7 @@ function isStep9RecoverableAuthError(error) {
function isLegacyStep9RecoverableAuthError(error) {
const message = String(typeof error === 'string' ? error : error?.message || '');
return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*Timeout waiting for OAuth callback/i.test(message);
return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(message);
}
function isStepDoneStatus(status) {
@@ -4708,6 +4712,17 @@ async function executeStepAndWait(step, delayAfter = 2000) {
await executeStep(step);
}
if (step === 5) {
const signupTabId = await getTabId('signup-page');
if (signupTabId) {
await addLog('自动运行:步骤 5 已收到完成信号,正在等待当前页面完成加载...', 'info');
await waitForTabComplete(signupTabId, {
timeoutMs: 15000,
retryDelayMs: 300,
});
}
}
// Extra delay for page transitions / DOM updates
if (delayAfter > 0) {
await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200));
@@ -4791,7 +4806,7 @@ const AUTO_STEP_DELAYS = {
2: 2000,
3: 3000,
4: 2000,
5: 3000,
5: 0,
6: 3000,
7: 2000,
8: 2000,
@@ -4996,8 +5011,7 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
async function runAutoSequenceFromStep(startStep, context = {}) {
const { targetRun, totalRuns, attemptRuns, continued = false } = context;
const maxStep9RestartAttempts = 5;
let step9RestartAttempts = 0;
let postStep6RestartCount = 0;
if (continued) {
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info');
@@ -5048,27 +5062,31 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
}
step += 1;
} catch (err) {
const latestState = await getState();
const currentMail = getMailConfig(latestState);
const shouldRetryStep9 = step === 9
&& (
isLegacyStep9RecoverableAuthError(err)
|| (currentMail.provider === HOTMAIL_PROVIDER && isStep9RecoverableAuthError(err))
)
&& step9RestartAttempts < maxStep9RestartAttempts;
if (shouldRetryStep9) {
step9RestartAttempts += 1;
const restartDecision = await getPostStep6AutoRestartDecision(step, err);
if (restartDecision.shouldRestart) {
postStep6RestartCount += 1;
const authState = restartDecision.authState;
const authStateLabel = authState?.state ? getLoginAuthStateLabel(authState.state) : '未知页面';
const authStateSuffix = authState?.url
? `当前认证页:${authStateLabel}${authState.url}`
: authState?.state
? `当前认证页:${authStateLabel}`
: '未获取到认证页状态';
await addLog(
`步骤 9:检测到 CPA 认证失败,正在回到步骤 6 重新开始授权流程(${step9RestartAttempts}/${maxStep9RestartAttempts}...`,
`步骤 ${step}:检测到报错且当前未进入 add-phone,正在回到步骤 6 重新开始授权流程(${postStep6RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`,
'warn'
);
await invalidateDownstreamAfterStepRestart(6, {
logLabel: `步骤 9 认证失败后准备回到步骤 6 重试(${step9RestartAttempts}/${maxStep9RestartAttempts}`,
await invalidateDownstreamAfterStepRestart(5, {
logLabel: `步骤 ${step} 报错后准备回到步骤 6 重试(${postStep6RestartCount} 次重开`,
});
step = 6;
continue;
}
if (restartDecision.blockedByAddPhone) {
const addPhoneUrl = restartDecision.authState?.url || 'https://auth.openai.com/add-phone';
await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 6 重开。`, 'warn');
}
throw err;
}
}
@@ -5297,13 +5315,7 @@ const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({
addLog,
generateRandomBirthday,
generateRandomName,
getState,
getTabId,
handleChatgptOnboardingSkip,
isRetryableContentScriptTransportError,
LOG_PREFIX,
sendToContentScript,
waitForStep5ChatgptRedirect,
});
const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
addLog,
@@ -5651,91 +5663,10 @@ async function executeStep4(state) {
// Step 5: Fill Name & Birthday (via signup-page.js)
// ============================================================
async function waitForStep5ChatgptRedirect(tabId, timeoutMs = 15000) {
if (!Number.isInteger(tabId)) {
return null;
}
const matchedTab = await waitForTabUrlMatch(tabId, (url) => /chatgpt\.com/i.test(url || ''), {
timeoutMs,
retryDelayMs: 300,
});
if (matchedTab) {
return matchedTab;
}
const currentTab = await chrome.tabs.get(tabId).catch(() => null);
if (currentTab && /chatgpt\.com/i.test(currentTab.url || '')) {
return currentTab;
}
return null;
}
async function executeStep5(state) {
return step5Executor.executeStep5(state);
}
async function handleChatgptOnboardingSkip(knownTabId) {
const signupTabId = knownTabId || await getTabId('signup-page');
if (!signupTabId) {
throw new Error('步骤 5:无法找到注册页标签页,无法处理 ChatGPT 引导页。');
}
// Wait for the tab to navigate to chatgpt.com (may already be there)
const start = Date.now();
const timeout = 15000;
let tabUrl = '';
while (Date.now() - start < timeout) {
throwIfStopped();
const tab = await chrome.tabs.get(signupTabId).catch(() => null);
if (tab && /chatgpt\.com/i.test(tab.url || '')) {
tabUrl = tab.url;
break;
}
await sleepWithStop(500);
}
if (!tabUrl) {
throw new Error('步骤 5:等待页面跳转到 ChatGPT 引导页超时。');
}
// Wait for page to finish loading
await sleepWithStop(2000);
// Inject content script on chatgpt.com
await ensureContentScriptReadyOnTab('signup-page', signupTabId, {
inject: SIGNUP_PAGE_INJECT_FILES,
injectSource: 'signup-page',
timeoutMs: 15000,
logMessage: '步骤 5:正在等待 ChatGPT 引导页内容脚本就绪...',
});
// Send CHATGPT_SKIP_ONBOARDING message
const result = await sendToContentScriptResilient('signup-page', {
type: 'CHATGPT_SKIP_ONBOARDING',
source: 'background',
payload: {},
}, {
timeoutMs: 60000,
retryDelayMs: 1000,
logMessage: '步骤 5:ChatGPT 引导页正在处理,等待跳过完成...',
});
if (result?.error) {
throw new Error(result.error);
}
if (result?.alreadyCompleted) {
await addLog('步骤 5:已进入 ChatGPT 页面,无需继续跳过引导,注册成功。', 'ok');
await completeStepFromBackground(5, { chatgptHome: true });
return;
}
await addLog('步骤 5:ChatGPT 引导页跳过完成,注册成功。', 'ok');
await completeStepFromBackground(5, { chatgptOnboarding: true });
}
// ============================================================
// Step 6 Cookie Cleanup
// ============================================================
@@ -5880,7 +5811,69 @@ function isStep6RecoverableResult(result) {
return result?.step6Outcome === 'recoverable';
}
async function getLoginAuthStateFromContent() {
function isAddPhoneAuthUrl(url) {
return /https:\/\/auth\.openai\.com\/add-phone(?:[/?#]|$)/i.test(String(url || '').trim());
}
function isAddPhoneAuthState(authState = {}) {
return authState?.state === 'add_phone_page'
|| Boolean(authState?.addPhonePage)
|| isAddPhoneAuthUrl(authState?.url);
}
async function getPostStep6AutoRestartDecision(step, error) {
const normalizedStep = Number(step);
const errorMessage = getErrorMessage(error);
if (!Number.isFinite(normalizedStep) || normalizedStep < 6 || normalizedStep > 9) {
return {
shouldRestart: false,
blockedByAddPhone: false,
errorMessage,
authState: null,
};
}
if (isAddPhoneAuthUrl(errorMessage)) {
return {
shouldRestart: false,
blockedByAddPhone: true,
errorMessage,
authState: null,
};
}
let authState = null;
try {
authState = await getLoginAuthStateFromContent({
logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 6 重开...`,
});
} catch (inspectError) {
console.warn(LOG_PREFIX, '[AutoRun] failed to inspect login auth state after post-step6 error', {
step: normalizedStep,
sourceError: errorMessage,
inspectError: inspectError?.message || inspectError,
});
}
if (isAddPhoneAuthState(authState)) {
return {
shouldRestart: false,
blockedByAddPhone: true,
errorMessage,
authState,
};
}
return {
shouldRestart: true,
blockedByAddPhone: false,
errorMessage,
authState,
};
}
async function getLoginAuthStateFromContent(options = {}) {
const { logMessage = '步骤 7:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...' } = options;
const result = await sendToContentScriptResilient(
'signup-page',
{
@@ -5891,7 +5884,7 @@ async function getLoginAuthStateFromContent() {
{
timeoutMs: 15000,
retryDelayMs: 600,
logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...',
logMessage,
}
);
+1 -1
View File
@@ -94,7 +94,7 @@
function isLegacyStep9RecoverableAuthError(error) {
const message = String(typeof error === 'string' ? error : error?.message || '');
return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*Timeout waiting for OAuth callback/i.test(message);
return /STEP9_OAUTH_TIMEOUT::|认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(message);
}
function isStepDoneStatus(status) {
+6 -59
View File
@@ -6,74 +6,21 @@
addLog,
generateRandomBirthday,
generateRandomName,
getState,
getTabId,
handleChatgptOnboardingSkip,
isRetryableContentScriptTransportError,
LOG_PREFIX,
sendToContentScript,
waitForStep5ChatgptRedirect,
} = deps;
async function isStepAlreadyCompleted(step) {
if (typeof getState !== 'function') {
return false;
}
const latestState = await getState().catch(() => null);
return latestState?.stepStatuses?.[step] === 'completed';
}
async function executeStep5() {
const { firstName, lastName } = generateRandomName();
const { year, month, day } = generateRandomBirthday();
await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
let step5Result = null;
let step5TransportError = null;
try {
step5Result = await sendToContentScript('signup-page', {
type: 'EXECUTE_STEP',
step: 5,
source: 'background',
payload: { firstName, lastName, year, month, day },
});
} catch (err) {
if (isRetryableContentScriptTransportError(err)) {
step5TransportError = err;
console.log(LOG_PREFIX, '步骤 5:内容脚本通信中断,正在检查是否跳转到 ChatGPT 引导页...', err?.message);
} else {
throw err;
}
}
if (step5TransportError && await isStepAlreadyCompleted(5)) {
await addLog('步骤 5:提交后的页面跳转打断了响应,但已收到完成信号,直接结束当前步骤。', 'info');
return;
}
if (step5Result?.chatgptOnboarding) {
await addLog('步骤 5:检测到 ChatGPT 引导页跳转,正在处理引导页跳过...');
await handleChatgptOnboardingSkip();
return;
}
if (step5Result?.chatgptHome) {
await addLog('步骤 5:检测到已进入 ChatGPT 页面,注册成功。', 'ok');
return;
}
if (step5TransportError) {
const signupTabId = await getTabId('signup-page');
const redirectedTab = await waitForStep5ChatgptRedirect(signupTabId);
if (redirectedTab) {
await addLog('步骤 5:内容脚本因页面跳转到 ChatGPT 而断开,正在处理引导页跳过...');
await handleChatgptOnboardingSkip(redirectedTab.id);
return;
}
throw step5TransportError;
}
await sendToContentScript('signup-page', {
type: 'EXECUTE_STEP',
step: 5,
source: 'background',
payload: { firstName, lastName, year, month, day },
});
}
return { executeStep5 };
+24
View File
@@ -202,6 +202,29 @@
return null;
}
async function waitForTabComplete(tabId, options = {}) {
const { timeoutMs = 15000, retryDelayMs = 300 } = options;
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const tab = await chrome.tabs.get(tabId);
if (tab?.status === 'complete') {
return tab;
}
} catch {
return null;
}
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
}
try {
return await chrome.tabs.get(tabId);
} catch {
return null;
}
}
async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
const {
inject = null,
@@ -619,6 +642,7 @@
sendToContentScriptResilient,
sendToMailContentScriptResilient,
summarizeMessageResultForDebug,
waitForTabComplete,
waitForTabUrlFamily,
waitForTabUrlMatch,
};
+7 -3
View File
@@ -38,12 +38,16 @@
}
function isRecoverableStep9AuthFailure(statusText) {
const text = String(statusText || '').trim();
if (!/认证失败:\s*/i.test(text)) {
const text = String(statusText || '').replace(/\s+/g, ' ').trim();
if (!text) {
return false;
}
return /timeout waiting for oauth callback|status code 5\d{2}|bad gateway|gateway timeout|temporarily unavailable/i.test(text);
if (/oauth flow is not pending/i.test(text)) {
return true;
}
return /(?:认证失败|回调 URL 提交失败):\s*/i.test(text);
}
return {
-151
View File
@@ -22,7 +22,6 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|| message.type === 'RESEND_VERIFICATION_CODE'
|| message.type === 'ENSURE_SIGNUP_ENTRY_READY'
|| message.type === 'ENSURE_SIGNUP_PASSWORD_PAGE_READY'
|| message.type === 'CHATGPT_SKIP_ONBOARDING'
) {
resetStopState();
handleCommand(message).then((result) => {
@@ -84,8 +83,6 @@ async function handleCommand(message) {
return getStep8State();
case 'STEP8_TRIGGER_CONTINUE':
return await step8_triggerContinue(message.payload);
case 'CHATGPT_SKIP_ONBOARDING':
return await skipChatgptOnboarding();
}
}
@@ -103,8 +100,6 @@ const VERIFICATION_CODE_INPUT_SELECTOR = [
const ONE_TIME_CODE_LOGIN_PATTERN = /使用一次性验证码登录|改用(?:一次性)?验证码(?:登录)?|使用验证码登录|一次性验证码|验证码登录|one[-\s]*time\s*(?:passcode|password|code)|use\s+(?:a\s+)?one[-\s]*time\s*(?:passcode|password|code)(?:\s+instead)?|use\s+(?:a\s+)?code(?:\s+instead)?|sign\s+in\s+with\s+(?:email|code)|email\s+(?:me\s+)?(?:a\s+)?code/i;
const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i;
const CHATGPT_ONBOARDING_TEXT_PATTERN = /welcome\s+to\s+chatgpt|what\s+should\s+chatgpt\s+call\s+you|how\s+do\s+you\s+want\s+chatgpt\s+to\s+respond|tell\s+chatgpt\s+what\s+traits|personal(?:ize|ise)\s+your\s+experience|介绍一下你自己|ChatGPT 应该如何称呼你|你希望 ChatGPT 如何回应/i;
const CHATGPT_HOME_TEXT_PATTERN = /new\s+chat|temporary\s+chat|message\s+chatgpt|send\s+a\s+message|chatgpt\s+can\s+make\s+mistakes|新建聊天|临时聊天|给\s*ChatGPT\s*发消息|ChatGPT\s*可能会犯错/i;
function isVisibleElement(el) {
if (!el) return false;
@@ -780,69 +775,6 @@ function getStep5ErrorText() {
return messages.find((text) => STEP5_SUBMIT_ERROR_PATTERN.test(text)) || '';
}
function isChatgptOnboardingPage() {
if (!isChatgptUrl()) {
return false;
}
if (findChatgptSkipButton()) {
return true;
}
return CHATGPT_ONBOARDING_TEXT_PATTERN.test(getPageTextSnapshot());
}
function isChatgptUrl(rawUrl = location.href) {
return /(^https?:\/\/)?([^.]+\.)?chatgpt\.com(?::\d+)?/i.test(rawUrl || '');
}
function hasVisibleElementMatchingSelector(selector) {
return Array.from(document.querySelectorAll(selector)).some(isVisibleElement);
}
function isChatgptAuthenticatedHomePage() {
if (!isChatgptUrl()) {
return false;
}
if (isChatgptOnboardingPage()) {
return false;
}
const appShellSelectors = [
'textarea[placeholder*="Message" i]',
'textarea[aria-label*="Message" i]',
'[data-testid="composer-root"]',
'[data-testid="history-and-skills"]',
'button[aria-label*="New chat" i]',
'a[aria-label*="New chat" i]',
'a[href^="/c/"]',
];
if (appShellSelectors.some((selector) => hasVisibleElementMatchingSelector(selector))) {
return true;
}
return CHATGPT_HOME_TEXT_PATTERN.test(getPageTextSnapshot());
}
async function waitForChatgptPostSignupState(timeout = 15000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
if (isChatgptOnboardingPage()) {
return 'onboarding';
}
if (isChatgptAuthenticatedHomePage()) {
return 'home';
}
await sleep(300);
}
return null;
}
function isSignupPasswordPage() {
return /\/create-account\/password(?:[/?#]|$)/i.test(location.pathname || '');
@@ -2208,86 +2140,3 @@ async function step5_fillNameBirthday(payload) {
return completionPayload;
}
// ============================================================
// ChatGPT Onboarding: Skip buttons after signup redirect
// ============================================================
function findChatgptSkipButton() {
// Look for buttons containing "Skip" or "跳过" text with btn-ghost class
const buttons = document.querySelectorAll('button');
for (const btn of buttons) {
if (!isVisibleElement(btn)) continue;
const text = (btn.textContent || '').trim();
if (/^(skip|跳过)$/i.test(text) && /btn-ghost/i.test(btn.className)) {
return btn;
}
}
// Fallback: look for any button matching skip text without class constraint
for (const btn of buttons) {
if (!isVisibleElement(btn)) continue;
const text = (btn.textContent || '').trim();
if (/^(skip|跳过)$/i.test(text)) {
return btn;
}
}
return null;
}
async function waitForChatgptSkipButton(timeout = 15000) {
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
const btn = findChatgptSkipButton();
if (btn && isVisibleElement(btn)) {
return btn;
}
await sleep(300);
}
return null;
}
async function skipChatgptOnboarding() {
log('ChatGPT 页面:正在确认是引导页还是已登录主页...');
const initialState = await waitForChatgptPostSignupState(15000);
if (initialState === 'home') {
log('ChatGPT 页面:未出现可跳过引导,已进入登录后的 ChatGPT 页面,按注册成功处理。', 'ok');
return { success: true, alreadyCompleted: true };
}
if (initialState !== 'onboarding') {
throw new Error('ChatGPT 页面:未检测到可跳过引导,也未确认进入登录后的主页。URL: ' + location.href);
}
log('ChatGPT 引导页:正在查找第一个"Skip/跳过"按钮...');
// Find first Skip button: sibling of "Next" button, with btn-ghost class
const firstSkipBtn = await waitForChatgptSkipButton(15000);
if (!firstSkipBtn) {
throw new Error('ChatGPT 引导页:未找到第一个"Skip/跳过"按钮。URL: ' + location.href);
}
await humanPause(500, 1200);
simulateClick(firstSkipBtn);
log('ChatGPT 引导页:已点击第一个"Skip/跳过"按钮,等待第二个按钮出现...');
// Wait 3 seconds for the second skip button to appear
await sleep(3000);
// Find second Skip button: sibling of "Continue" button, with btn-ghost class
const secondSkipBtn = await waitForChatgptSkipButton(5000);
if (!secondSkipBtn) {
if (isChatgptAuthenticatedHomePage()) {
log('ChatGPT 引导页:未找到第二个"Skip/跳过"按钮,但页面已进入登录后的 ChatGPT 页面。', 'ok');
} else {
log('ChatGPT 引导页:5秒内未找到第二个"Skip/跳过"按钮,判定注册已完成。', 'ok');
}
return { success: true };
}
await humanPause(500, 1200);
simulateClick(secondSkipBtn);
log('ChatGPT 引导页:已点击第二个"Skip/跳过"按钮,注册流程完成。', 'ok');
return { success: true };
}
+212 -33
View File
@@ -208,13 +208,7 @@ function getStatusBadgeEntries() {
for (const candidate of candidates) {
if (seen.has(candidate)) continue;
seen.add(candidate);
entries.push({
element: candidate,
selector,
visible: isVisibleElement(candidate),
text: (candidate.textContent || '').replace(/\s+/g, ' ').trim(),
className: String(candidate.className || '').replace(/\s+/g, ' ').trim(),
});
entries.push(createStep9Entry(candidate, selector));
}
}
@@ -227,7 +221,8 @@ function summarizeStatusBadgeEntries(entries) {
.map((entry, index) => {
const text = entry.text || '(空文本)';
const className = entry.className ? ` class=${getInlineTextSnippet(entry.className, 80)}` : '';
return `#${index + 1}="${getInlineTextSnippet(text, 80)}"${className}`;
const errorVisual = entry.errorVisualSummary ? ` error=${getInlineTextSnippet(entry.errorVisualSummary, 80)}` : '';
return `#${index + 1}="${getInlineTextSnippet(text, 80)}"${className}${errorVisual}`;
})
.join(' | ');
}
@@ -242,6 +237,20 @@ function normalizeStep9StatusText(statusText) {
return String(statusText || '').replace(/\s+/g, ' ').trim();
}
function isOAuthCallbackTimeoutFailure(statusText) {
return /认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(statusText || '');
}
function isStep9FailureText(statusText) {
const text = normalizeStep9StatusText(statusText);
if (!text) return false;
if (isOAuthCallbackTimeoutFailure(text)) return true;
if (typeof isRecoverableStep9AuthFailure === 'function' && isRecoverableStep9AuthFailure(text)) {
return true;
}
return /回调\s*url\s*提交失败|callback\s*url\s*submit\s*failed|oauth flow is not pending/i.test(text);
}
function isStep9SuccessStatus(statusText) {
return STEP9_SUCCESS_STATUSES.has(normalizeStep9StatusText(statusText));
}
@@ -251,37 +260,188 @@ function isStep9SuccessLikeStatus(statusText) {
return /authentication successful|аутентификац.*успеш|认证成功/i.test(text);
}
function getStatusBadgeDiagnostics() {
const entries = getStatusBadgeEntries();
function parseCssColorChannels(colorText) {
const text = String(colorText || '').trim().toLowerCase();
if (!text || text === 'transparent' || text === 'inherit' || text === 'initial' || text === 'unset') {
return null;
}
if (text.startsWith('#')) {
const hex = text.slice(1);
if (hex.length === 3 || hex.length === 4) {
const expanded = hex.split('').map((part) => part + part);
const [r, g, b, a = 'ff'] = expanded;
return {
r: Number.parseInt(r, 16),
g: Number.parseInt(g, 16),
b: Number.parseInt(b, 16),
a: Number.parseInt(a, 16) / 255,
};
}
if (hex.length === 6 || hex.length === 8) {
const parts = hex.match(/.{1,2}/g) || [];
const [r, g, b, a = 'ff'] = parts;
return {
r: Number.parseInt(r, 16),
g: Number.parseInt(g, 16),
b: Number.parseInt(b, 16),
a: Number.parseInt(a, 16) / 255,
};
}
}
if (text.startsWith('rgb')) {
const numericParts = text.match(/[\d.]+/g) || [];
if (numericParts.length >= 3) {
const [r, g, b, a = '1'] = numericParts.map(Number);
return { r, g, b, a };
}
}
return null;
}
function isReddishColor(colorText) {
const channels = parseCssColorChannels(colorText);
if (!channels) return false;
const { r, g, b, a = 1 } = channels;
if (a <= 0.05) return false;
return r >= 120 && r >= g + 35 && r >= b + 35;
}
function getStep9ErrorVisualSignals(element, className = '') {
const signals = [];
const normalizedClassName = String(className || '').replace(/\s+/g, ' ').trim();
if (/(?:error|danger|fail|destructive|text-red|text-danger|alert)/i.test(normalizedClassName)) {
signals.push(`class=${getInlineTextSnippet(normalizedClassName, 80)}`);
}
if (!element) {
return signals;
}
const style = window.getComputedStyle(element);
if (isReddishColor(style.color)) {
signals.push(`color=${style.color}`);
}
if (isReddishColor(style.borderColor)) {
signals.push(`border=${style.borderColor}`);
}
if (isReddishColor(style.backgroundColor)) {
signals.push(`background=${style.backgroundColor}`);
}
return signals;
}
function createStep9Entry(candidate, selector) {
const className = String(candidate?.className || '').replace(/\s+/g, ' ').trim();
const errorVisualSignals = getStep9ErrorVisualSignals(candidate, className);
return {
element: candidate,
selector,
visible: isVisibleElement(candidate),
text: normalizeStep9StatusText(candidate?.textContent || ''),
className,
errorVisualSignals,
errorVisualSummary: errorVisualSignals.join(', '),
hasErrorVisualSignal: errorVisualSignals.length > 0,
};
}
function getStep9PageErrorSelectors() {
return [
'[role="alert"]',
'[aria-live="assertive"]',
'[aria-live="polite"]',
'.alert',
'[class*="alert"]',
'[class*="error"]',
'[class*="danger"]',
'.text-danger',
'.text-red',
];
}
function getStep9PageErrorEntries() {
const seen = new Set();
const entries = [];
for (const selector of getStep9PageErrorSelectors()) {
const candidates = document.querySelectorAll(selector);
for (const candidate of candidates) {
if (seen.has(candidate)) continue;
seen.add(candidate);
if (!isVisibleElement(candidate)) continue;
const entry = createStep9Entry(candidate, selector);
if (!isStep9FailureText(entry.text)) continue;
entries.push(entry);
}
}
return entries;
}
function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSnippet = '') {
const visibleEntries = entries.filter((entry) => entry.visible);
const selectedEntry = visibleEntries[0] || null;
const selectedText = selectedEntry?.text || '';
const successLikeEntries = visibleEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text));
const exactSuccessEntries = visibleEntries.filter((entry) => isStep9SuccessStatus(entry.text));
const exactSuccessEntries = visibleEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal);
const failureEntries = visibleEntries.filter((entry) => isStep9FailureText(entry.text));
const errorStyledEntries = visibleEntries.filter((entry) => entry.hasErrorVisualSignal);
const allFailureEntries = [...failureEntries, ...pageErrorEntries];
const decisiveFailureEntry = allFailureEntries[0] || null;
const selectedEntry = decisiveFailureEntry || exactSuccessEntries[0] || visibleEntries[0] || null;
const selectedText = selectedEntry?.text || '';
const visibleSummary = summarizeStatusBadgeEntries(visibleEntries);
const pageSnippet = getPageTextSnippet();
const successLikeSummary = summarizeStatusBadgeEntries(successLikeEntries);
const exactSuccessSummary = summarizeStatusBadgeEntries(exactSuccessEntries);
const failureSummary = summarizeStatusBadgeEntries(failureEntries);
const pageErrorSummary = summarizeStatusBadgeEntries(pageErrorEntries);
const errorStyledSummary = summarizeStatusBadgeEntries(errorStyledEntries);
const extraFailureSuffix = pageErrorEntries.length ? `;额外错误提示:${pageErrorSummary}` : '';
const errorStyledSuffix = errorStyledEntries.length ? `;红色/错误样式徽标:${errorStyledSummary}` : '';
return {
selectedText,
exactSuccessText: exactSuccessEntries[0]?.text || '',
failureText: decisiveFailureEntry?.text || '',
visibleCount: visibleEntries.length,
visibleSummary,
hasSuccessLikeVisibleBadge: successLikeEntries.length > 0,
hasExactSuccessVisibleBadge: exactSuccessEntries.length > 0,
successLikeSummary: summarizeStatusBadgeEntries(successLikeEntries),
exactSuccessSummary: summarizeStatusBadgeEntries(exactSuccessEntries),
hasFailureVisibleBadge: allFailureEntries.length > 0,
hasErrorStyledVisibleBadge: errorStyledEntries.length > 0,
successLikeSummary,
exactSuccessSummary,
failureSummary,
pageErrorSummary,
errorStyledSummary,
pageSnippet,
signature: JSON.stringify({
selectedText,
visibleCount: visibleEntries.length,
visibleSummary,
successLikeSummary: summarizeStatusBadgeEntries(successLikeEntries),
successLikeSummary,
exactSuccessSummary,
failureSummary,
pageErrorSummary,
errorStyledSummary,
}),
summary: selectedText
? `当前选中徽标="${getInlineTextSnippet(selectedText, 80)}";可见徽标 ${visibleEntries.length} 个:${visibleSummary}`
: `当前未选中任何可见状态徽标;可见徽标 ${visibleEntries.length} 个:${visibleSummary};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
? `当前聚焦状态="${getInlineTextSnippet(selectedText, 80)}";可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}`
: `当前未选中任何可见状态徽标;可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
};
}
function getStatusBadgeDiagnostics() {
return buildStep9StatusDiagnostics(
getStatusBadgeEntries(),
getStep9PageErrorEntries(),
getPageTextSnippet()
);
}
function getStatusBadgeElement() {
const visibleEntry = getStatusBadgeEntries().find((entry) => entry.visible);
return visibleEntry ? visibleEntry.element : null;
@@ -292,20 +452,16 @@ function getStatusBadgeText() {
return diagnostics.selectedText;
}
function isOAuthCallbackTimeoutFailure(statusText) {
return /认证失败:\s*Timeout waiting for OAuth callback/i.test(statusText || '');
}
async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS) {
const start = Date.now();
let lastDiagnosticsSignature = '';
let lastHeartbeatLoggedAt = 0;
let lastSuccessLikeMismatchSignature = '';
let lastSuccessFailureConflictSignature = '';
while (Date.now() - start < timeout) {
throwIfStopped();
const diagnostics = getStatusBadgeDiagnostics();
const statusText = diagnostics.selectedText;
const elapsed = Date.now() - start;
if (diagnostics.signature !== lastDiagnosticsSignature) {
@@ -324,36 +480,59 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
selectedText: diagnostics.selectedText,
successLikeSummary: diagnostics.successLikeSummary,
visibleSummary: diagnostics.visibleSummary,
errorStyledSummary: diagnostics.errorStyledSummary,
});
if (mismatchSignature !== lastSuccessLikeMismatchSignature) {
lastSuccessLikeMismatchSignature = mismatchSignature;
const errorStyledSuffix = diagnostics.hasErrorStyledVisibleBadge
? `;错误样式徽标:${diagnostics.errorStyledSummary}`
: '';
log(
`步骤 9:检测到“认证成功”相关徽标,但未命中精确条件。当前选中="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}`,
`步骤 9:检测到“认证成功”相关徽标,但未命中精确成功条件。当前聚焦="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}${errorStyledSuffix}`,
'warn'
);
console.warn(LOG_PREFIX, '[Step 9] success-like badge detected without exact match', diagnostics);
}
}
if (isOAuthCallbackTimeoutFailure(statusText)) {
throw new Error(`STEP9_OAUTH_TIMEOUT::${statusText}`);
if (diagnostics.hasExactSuccessVisibleBadge && diagnostics.hasFailureVisibleBadge) {
const conflictSignature = JSON.stringify({
exactSuccessSummary: diagnostics.exactSuccessSummary,
failureSummary: diagnostics.failureSummary,
pageErrorSummary: diagnostics.pageErrorSummary,
});
if (conflictSignature !== lastSuccessFailureConflictSignature) {
lastSuccessFailureConflictSignature = conflictSignature;
const failureSummary = diagnostics.pageErrorSummary !== '无可见状态徽标'
? diagnostics.pageErrorSummary
: diagnostics.failureSummary;
log(
`步骤 9:同时检测到成功徽标和失败提示,本轮不判定成功。成功徽标:${diagnostics.exactSuccessSummary};失败提示:${failureSummary}`,
'warn'
);
console.warn(LOG_PREFIX, '[Step 9] success badge is blocked by visible failure', diagnostics);
}
}
if (typeof isRecoverableStep9AuthFailure === 'function' && isRecoverableStep9AuthFailure(statusText)) {
throw new Error(`STEP9_OAUTH_RETRY::${statusText}`);
if (diagnostics.failureText) {
if (isOAuthCallbackTimeoutFailure(diagnostics.failureText)) {
throw new Error(`STEP9_OAUTH_TIMEOUT::${diagnostics.failureText}`);
}
throw new Error(`STEP9_OAUTH_RETRY::${diagnostics.failureText}`);
}
if (isStep9SuccessStatus(statusText)) {
return statusText;
if (diagnostics.exactSuccessText) {
return diagnostics.exactSuccessText;
}
await sleep(200);
}
const finalDiagnostics = getStatusBadgeDiagnostics();
const finalText = finalDiagnostics.selectedText;
const finalText = finalDiagnostics.failureText || finalDiagnostics.selectedText;
const diagnosticsSuffix = ` 当前诊断:${finalDiagnostics.summary}`;
if (isOAuthCallbackTimeoutFailure(finalText)) {
throw new Error(`STEP9_OAUTH_TIMEOUT::${finalText}${diagnosticsSuffix}`);
}
if (typeof isRecoverableStep9AuthFailure === 'function' && isRecoverableStep9AuthFailure(finalText)) {
if (isStep9FailureText(finalText)) {
throw new Error(`STEP9_OAUTH_RETRY::${finalText}${diagnosticsSuffix}`);
}
throw new Error(finalText
+10
View File
@@ -71,6 +71,16 @@ test('isRecoverableStep9AuthFailure matches timeout and CPA auth failure statuse
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证失败: timeout of 30000ms exceeded'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('回调 URL 提交失败: oauth flow is not pending'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证成功!'),
false
+197
View File
@@ -0,0 +1,197 @@
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 i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
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 ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
const bundle = [
extractFunction('isAddPhoneAuthUrl'),
extractFunction('isAddPhoneAuthState'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
].join('\n');
function createHarness(options = {}) {
const {
startStep = 6,
failureStep = 9,
failureBudget = 1,
failureMessage = '认证失败: Request failed with status code 502',
authState = { state: 'password_page', url: 'https://auth.openai.com/log-in' },
} = options;
return new Function(`
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0 };
const LOG_PREFIX = '[test]';
const chrome = {
tabs: {
update: async () => {},
},
};
let remainingFailures = ${JSON.stringify(failureBudget)};
const events = {
steps: [],
logs: [],
invalidations: [],
};
async function addLog(message, level = 'info') {
events.logs.push({ message, level });
}
async function ensureAutoEmailReady() {}
async function broadcastAutoRunStatus() {}
async function getState() {
return {
stepStatuses: { 3: 'completed' },
mailProvider: '163',
};
}
function isStepDoneStatus(status) {
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
}
async function executeStepAndWait(step) {
events.steps.push(step);
if (step === ${JSON.stringify(failureStep)} && remainingFailures > 0) {
remainingFailures -= 1;
throw new Error(${JSON.stringify(failureMessage)});
}
}
async function getTabId() {
return 1;
}
function shouldSkipLoginVerificationForCpaCallback() {
return false;
}
async function invalidateDownstreamAfterStepRestart(step, options = {}) {
events.invalidations.push({ step, options });
}
function getLoginAuthStateLabel(state) {
return state || 'unknown';
}
function getErrorMessage(error) {
return error?.message || String(error || '');
}
async function getLoginAuthStateFromContent() {
return ${JSON.stringify(authState)};
}
${bundle}
return {
async run() {
await runAutoSequenceFromStep(${JSON.stringify(startStep)}, {
targetRun: 1,
totalRuns: 1,
attemptRuns: 1,
continued: false,
});
return events;
},
async runAndCaptureError() {
try {
await runAutoSequenceFromStep(${JSON.stringify(startStep)}, {
targetRun: 1,
totalRuns: 1,
attemptRuns: 1,
continued: false,
});
return null;
} catch (error) {
return { error, events };
}
},
};
`)();
}
test('auto-run keeps restarting from step 6 after post-login failures without a hard cap', async () => {
const harness = createHarness({
failureStep: 9,
failureBudget: 6,
failureMessage: '认证失败: Request failed with status code 502',
authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
});
const events = await harness.run();
assert.equal(events.invalidations.length, 6);
assert.deepStrictEqual(
events.steps,
[
6, 7, 8, 9,
6, 7, 8, 9,
6, 7, 8, 9,
6, 7, 8, 9,
6, 7, 8, 9,
6, 7, 8, 9,
6, 7, 8, 9,
]
);
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新开始授权流程/.test(message)));
});
test('auto-run stops restarting once add-phone is detected', async () => {
const harness = createHarness({
failureStep: 6,
failureBudget: 1,
failureMessage: '当前页面已进入手机号页。URL: https://auth.openai.com/add-phone',
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.equal(result.events.invalidations.length, 0);
assert.deepStrictEqual(result.events.steps, [6]);
assert.ok(result.events.logs.some(({ message }) => /进入 add-phone/.test(message)));
});
@@ -6,47 +6,42 @@ const source = fs.readFileSync('background/steps/fill-profile.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep5;`)(globalScope);
test('step 5 transport error returns immediately after completion signal arrives', async () => {
test('step 5 forwards generated profile data and relies on completion signal flow', async () => {
const events = {
redirectWaitCalls: 0,
onboardingCalls: 0,
logs: [],
messages: [],
};
const transportError = new Error('The message port closed before a response was received.');
const executor = api.createStep5Executor({
addLog: async (message, level) => {
events.logs.push({ message, level: level || 'info' });
},
generateRandomBirthday: () => ({ year: 2003, month: 6, day: 19 }),
generateRandomName: () => ({ firstName: 'Test', lastName: 'User' }),
getState: async () => ({
stepStatuses: {
5: 'completed',
},
}),
getTabId: async () => 123,
handleChatgptOnboardingSkip: async () => {
events.onboardingCalls += 1;
},
isRetryableContentScriptTransportError: (error) => error === transportError,
LOG_PREFIX: '[test]',
sendToContentScript: async () => {
throw transportError;
},
waitForStep5ChatgptRedirect: async () => {
events.redirectWaitCalls += 1;
return { id: 123, url: 'https://chatgpt.com/' };
sendToContentScript: async (source, message) => {
events.messages.push({ source, message });
return { accepted: true };
},
});
await executor.executeStep5();
assert.equal(events.redirectWaitCalls, 0, '收到完成信号后不应再等待 ChatGPT 跳转');
assert.equal(events.onboardingCalls, 0, '收到完成信号后不应再触发 onboarding 跳过');
assert.ok(
events.logs.some(({ message }) => /已收到完成信号直接结束当前步骤/.test(message)),
'应记录 Step 5 已按完成信号直接结束'
);
assert.deepStrictEqual(events.messages, [
{
source: 'signup-page',
message: {
type: 'EXECUTE_STEP',
step: 5,
source: 'background',
payload: {
firstName: 'Test',
lastName: 'User',
year: 2003,
month: 6,
day: 19,
},
},
},
]);
assert.ok(events.logs.some(({ message }) => /已生成姓名 Test User/.test(message)));
});
@@ -15,3 +15,45 @@ test('tab runtime module exposes a factory', () => {
assert.equal(typeof api?.createTabRuntime, 'function');
});
test('tab runtime waitForTabComplete waits until tab status becomes complete', async () => {
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope);
let getCalls = 0;
const runtime = api.createTabRuntime({
LOG_PREFIX: '[test]',
addLog: async () => {},
buildLocalhostCleanupPrefix: () => '',
chrome: {
tabs: {
get: async () => {
getCalls += 1;
return {
id: 9,
url: 'https://example.com',
status: getCalls >= 3 ? 'complete' : 'loading',
};
},
query: async () => [],
},
},
getSourceLabel: (source) => source || 'unknown',
getState: async () => ({ tabRegistry: {}, sourceLastUrls: {} }),
matchesSourceUrlFamily: () => false,
normalizeLocalCpaStep9Mode: () => 'submit',
parseUrlSafely: () => null,
registerTab: async () => {},
setState: async () => {},
shouldBypassStep9ForLocalCpa: () => false,
});
const result = await runtime.waitForTabComplete(9, {
timeoutMs: 2000,
retryDelayMs: 1,
});
assert.equal(result?.status, 'complete');
assert.equal(getCalls, 3);
});
-172
View File
@@ -1,172 +0,0 @@
const assert = require('assert');
const fs = require('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 i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
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 ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
const bundle = [
extractFunction('getPageTextSnapshot'),
extractFunction('findChatgptSkipButton'),
extractFunction('waitForChatgptSkipButton'),
extractFunction('isChatgptOnboardingPage'),
extractFunction('isChatgptUrl'),
extractFunction('hasVisibleElementMatchingSelector'),
extractFunction('isChatgptAuthenticatedHomePage'),
extractFunction('waitForChatgptPostSignupState'),
extractFunction('skipChatgptOnboarding'),
].join('\n');
const api = new Function(`
const CHATGPT_ONBOARDING_TEXT_PATTERN = /welcome\\s+to\\s+chatgpt|what\\s+should\\s+chatgpt\\s+call\\s+you|how\\s+do\\s+you\\s+want\\s+chatgpt\\s+to\\s+respond|tell\\s+chatgpt\\s+what\\s+traits|personal(?:ize|ise)\\s+your\\s+experience|介绍一下你自己|ChatGPT 应该如何称呼你|你希望 ChatGPT 如何回应/i;
const CHATGPT_HOME_TEXT_PATTERN = /new\\s+chat|temporary\\s+chat|message\\s+chatgpt|send\\s+a\\s+message|chatgpt\\s+can\\s+make\\s+mistakes|新建聊天|临时聊天|给\\s*ChatGPT\\s*发消息|ChatGPT\\s*可能会犯错/i;
let buttonList = [];
let selectorMap = {};
let pageText = '';
let clickedButtons = [];
let logs = [];
const location = { href: 'https://chatgpt.com/' };
const document = {
body: { innerText: '', textContent: '' },
querySelectorAll(selector) {
if (selector === 'button') {
return buttonList;
}
return selectorMap[selector] || [];
},
};
function isVisibleElement(el) {
return Boolean(el) && !el.hidden;
}
function throwIfStopped() {}
async function sleep() {}
async function humanPause() {}
function log(message, level = 'info') {
logs.push({ message, level });
}
function simulateClick(button) {
clickedButtons.push(button.textContent || button.id || 'button');
button.hidden = true;
}
${bundle}
return {
setPage({ href = 'https://chatgpt.com/', text = '', buttons = [], selectors = {} }) {
location.href = href;
pageText = text;
buttonList = buttons.map((button, index) => ({
id: button.id || \`button-\${index}\`,
textContent: button.textContent || '',
className: button.className || '',
hidden: Boolean(button.hidden),
}));
selectorMap = {};
for (const [selector, count] of Object.entries(selectors)) {
selectorMap[selector] = Array.from({ length: count }, (_, index) => ({ id: \`\${selector}-\${index}\`, hidden: false }));
}
document.body.innerText = pageText;
document.body.textContent = pageText;
clickedButtons = [];
logs = [];
},
isChatgptOnboardingPage() {
return isChatgptOnboardingPage();
},
isChatgptAuthenticatedHomePage() {
return isChatgptAuthenticatedHomePage();
},
async skipChatgptOnboarding() {
return skipChatgptOnboarding();
},
snapshot() {
return { clickedButtons, logs };
},
};
`)();
(async () => {
api.setPage({
href: 'https://chatgpt.com/',
text: 'New chat ChatGPT can make mistakes',
selectors: {
'textarea[placeholder*="Message" i]': 1,
},
});
assert.strictEqual(api.isChatgptOnboardingPage(), false, '已登录主页不应仅因 chatgpt.com URL 被误判为 onboarding');
assert.strictEqual(api.isChatgptAuthenticatedHomePage(), true, '主页特征存在时应识别为已登录 ChatGPT 页面');
let result = await api.skipChatgptOnboarding();
let snapshot = api.snapshot();
assert.deepStrictEqual(result, { success: true, alreadyCompleted: true }, '无 Skip 按钮但已进入主页时应按成功处理');
assert.deepStrictEqual(snapshot.clickedButtons, [], '已登录主页场景不应再误点按钮');
api.setPage({
href: 'https://chatgpt.com/',
text: 'Welcome to ChatGPT',
buttons: [
{ textContent: 'Skip', className: 'btn-ghost' },
{ textContent: 'Skip', className: 'btn-ghost' },
],
});
assert.strictEqual(api.isChatgptOnboardingPage(), true, '存在 Skip 按钮时应识别为 onboarding');
result = await api.skipChatgptOnboarding();
snapshot = api.snapshot();
assert.deepStrictEqual(result, { success: true }, '真实 onboarding 仍应继续执行跳过逻辑');
assert.deepStrictEqual(snapshot.clickedButtons, ['Skip', 'Skip'], '双 Skip onboarding 应依次点击两个按钮');
console.log('step5 chatgpt onboarding tests passed');
})().catch((error) => {
console.error(error);
process.exit(1);
});
-123
View File
@@ -1,123 +0,0 @@
const assert = require('assert');
const fs = require('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 i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
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 ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
const bundle = [
extractFunction('waitForStep5ChatgptRedirect'),
].join('\n');
const api = new Function(`
let waitArgs = null;
let waitResult = null;
let currentTab = null;
const chrome = {
tabs: {
async get() {
return currentTab;
},
},
};
async function waitForTabUrlMatch(tabId, matcher, options = {}) {
waitArgs = { tabId, matcher, options };
return waitResult;
}
${bundle}
return {
async run(tabId, timeoutMs) {
return waitForStep5ChatgptRedirect(tabId, timeoutMs);
},
setWaitResult(value) {
waitResult = value;
},
setCurrentTab(value) {
currentTab = value;
},
snapshot() {
return { waitArgs };
},
};
`)();
(async () => {
const redirected = { id: 86, url: 'https://chatgpt.com/' };
api.setWaitResult(redirected);
api.setCurrentTab({ id: 86, url: 'https://auth.openai.com/' });
let result = await api.run(86, 22000);
let snapshot = api.snapshot();
assert.deepStrictEqual(result, redirected, '等待命中 chatgpt.com 时应直接返回匹配到的标签页');
assert.strictEqual(snapshot.waitArgs.tabId, 86, '应使用 signup-page 当前标签页等待跳转');
assert.strictEqual(snapshot.waitArgs.options.timeoutMs, 22000, '应透传等待超时时间');
assert.strictEqual(snapshot.waitArgs.options.retryDelayMs, 300, '应使用较短轮询间隔覆盖 URL 更新 race');
assert.strictEqual(snapshot.waitArgs.matcher('https://chatgpt.com/?model=gpt-5'), true, 'matcher 应接受 chatgpt.com');
assert.strictEqual(snapshot.waitArgs.matcher('https://auth.openai.com/u/signup'), false, 'matcher 不应把旧认证页误判为成功');
api.setWaitResult(null);
api.setCurrentTab({ id: 86, url: 'https://chatgpt.com/?temporary-chat=true' });
result = await api.run(86, 15000);
assert.deepStrictEqual(result, { id: 86, url: 'https://chatgpt.com/?temporary-chat=true' }, '等待超时后仍应回读当前标签页 URL 兜底');
api.setCurrentTab({ id: 86, url: 'https://auth.openai.com/u/signup' });
result = await api.run(86, 15000);
assert.strictEqual(result, null, '仍停留旧域名时不应误判为跳转成功');
result = await api.run(null, 15000);
assert.strictEqual(result, null, '无有效标签页时应直接返回 null');
console.log('step5 chatgpt redirect race tests passed');
})().catch((error) => {
console.error(error);
process.exit(1);
});
+126
View File
@@ -0,0 +1,126 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('content/vps-panel.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 i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
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 ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
const bundle = [
"const STEP9_SUCCESS_STATUSES = new Set(['Authentication successful!', 'Аутентификация успешна!', '认证成功!']);",
extractFunction('getInlineTextSnippet'),
extractFunction('summarizeStatusBadgeEntries'),
extractFunction('normalizeStep9StatusText'),
extractFunction('isOAuthCallbackTimeoutFailure'),
extractFunction('isStep9FailureText'),
extractFunction('isStep9SuccessStatus'),
extractFunction('isStep9SuccessLikeStatus'),
extractFunction('buildStep9StatusDiagnostics'),
].join('\n');
function createApi() {
return new Function(`
function isRecoverableStep9AuthFailure(text) {
return /(?:认证失败|回调 URL 提交失败):\\s*/i.test(String(text || '').trim())
|| /oauth flow is not pending/i.test(String(text || '').trim());
}
${bundle}
return {
buildStep9StatusDiagnostics,
};
`)();
}
test('step 9 does not treat red success badges as exact success', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics([
{
visible: true,
text: '认证成功!',
className: 'status-badge text-danger',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
], [], 'page');
assert.equal(diagnostics.hasSuccessLikeVisibleBadge, true);
assert.equal(diagnostics.hasExactSuccessVisibleBadge, false);
assert.equal(diagnostics.hasErrorStyledVisibleBadge, true);
});
test('step 9 keeps failure state dominant when success badge and error banner coexist', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '认证成功!',
className: 'status-badge',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[
{
visible: true,
text: '回调 URL 提交失败: oauth flow is not pending',
className: 'alert alert-danger',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
],
'page'
);
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
assert.equal(diagnostics.hasFailureVisibleBadge, true);
assert.equal(diagnostics.failureText, '回调 URL 提交失败: oauth flow is not pending');
});
+12 -5
View File
@@ -206,7 +206,9 @@
1. 解析本轮应使用的邮箱
2. 打开或复用注册页
3. 点击注册入口并提交邮箱
4. 等待跳转到密码
4. 等待邮箱提交后的真实落地
5. 如果进入密码页,则继续执行 Step 3
6. 如果直接进入邮箱验证码页,则自动跳过 Step 3 并进入 Step 4
### Step 3
@@ -254,7 +256,7 @@
1. 生成随机姓名和生日
2. 内容脚本填写资料并点击“完成帐户创建”
3. 点击后立即上报 Step 5 完成,不再等待页面结果
4. 如果提交瞬间页面跳转导致响应通道中断,后台仅在未收到完成信号时兜底判断是否跳到 ChatGPT
4. 自动运行在进入 Step 6 前仅额外等待当前页面加载完成,不再在 Step 5 阶段接管 ChatGPT 跳转或 onboarding 跳过
### Step 6
@@ -270,6 +272,7 @@
4. 登录
5. 确保真正进入验证码页
6. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次
7. 自动运行一旦进入步骤 6 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 6 重新开始授权流程
### Step 8
@@ -296,9 +299,11 @@
2. 判断是 CPA 还是 SUB2API
3. 打开相应后台
4. 提交回调地址
5. 完成平台侧验证
6. 追加账号运行历史成功记录
7. 做成功后的清理与标记
5. 仅当出现精确成功徽标,且该徽标不是红色/错误态、页面上也没有同时可见的失败提示时,才判定成功
6. 识别 `认证失败:*``认证失败: timeout of 30000ms exceeded``回调 URL 提交失败: oauth flow is not pending` 等失败提示并立即报错
7. 完成平台侧验证
8. 追加账号运行历史成功记录
9. 做成功后的清理与标记
## 7. 邮箱与 provider 链路
@@ -359,6 +364,8 @@
2. 计算是否从中断点继续
3. 每轮执行前重置必要运行态
4. 执行 `runAutoSequenceFromStep`
- 步骤 6 内部仍保留登录态恢复的有限重试
- 一旦进入步骤 6~9,遇到报错且认证流程未进入 `add-phone`,则自动回到步骤 6 无限重开,直到成功、手动停止或命中 `add-phone`
5. 如果失败,根据设置决定:
- 立即停止
- 当前轮重试
+9 -6
View File
@@ -55,16 +55,16 @@
- `background/steps/fetch-login-code.js`:步骤 7 实现,负责登录验证码阶段的邮箱轮询与回退控制。
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口。
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。
- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写与 ChatGPT onboarding 跳过流程
- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本
- `background/steps/oauth-login.js`:步骤 6 实现,负责刷新 OAuth 链接、登录和确保进入验证码页。
- `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
- `background/steps/platform-verify.js`:步骤 9 实现,负责 CPA / SUB2API 回调验证。
- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与密码页等待
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与提交后落地页分支判断
## `content/`
- `content/activation-utils.js`:内容脚本通用激活策略工具,负责按钮点击方式判断等轻量辅助逻辑
- `content/activation-utils.js`:内容脚本通用激活策略工具,负责按钮点击方式判断与 Step 9 可恢复失败文案判断
- `content/duck-mail.js`DuckDuckGo Email Protection 页面脚本,负责生成或读取 `@duck.com` 地址。
- `content/gmail-mail.js`:Gmail 邮箱轮询脚本,负责在 Gmail 页面中匹配验证码邮件。
- `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
@@ -75,7 +75,7 @@
- `content/signup-page.js`:注册/登录/授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行。
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调。
- `content/utils.js`:内容脚本公共工具层,负责日志、READY/COMPLETE/ERROR 上报、元素等待、输入与点击。
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址提交回调 URL。
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址提交回调 URL,并基于精确成功徽标与错误态做 Step 9 判定
## `data/`
@@ -118,6 +118,7 @@
- `tests/activation-utils.test.js`:测试内容脚本激活策略与 Step 9 可恢复错误判断。
- `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文。
- `tests/auto-run-step6-restart.test.js`:测试自动运行在步骤 6 之后遇错时会回到步骤 6 重开,并在命中 add-phone 时停止重开。
- `tests/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。
- `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。
- `tests/background-account-run-history-module.test.js`:测试账号运行历史模块已接入、导出工厂,并能在不启用本地 helper 时正确持久化记录。
@@ -128,10 +129,12 @@
- `tests/background-logging-status-module.test.js`:测试日志/状态模块已接入且导出工厂。
- `tests/background-luckmail.test.js`:测试 LuckMail 相关后台逻辑,如购买、复用、标记已用与重置。
- `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑。
- `tests/background-navigation-utils-module.test.js`:测试导航工具模块已接入且导出工厂。
- `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。
- `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。
- `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。
- `tests/background-step5-submit-short-circuit.test.js`:测试 Step 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾。
- `tests/background-step6-retry-limit.test.js`:测试 Step 6 的有限重试上限,以及 Step 7 回放时可跳过登录前 Cookie 清理。
- `tests/background-step7-recovery.test.js`:测试 Step 7 在 CPA 模式下会先回放 Step 6 再提交登录验证码,并为 2925 关闭重发间隔。
- `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。
@@ -153,8 +156,7 @@
- `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。
- `tests/step5-chatgpt-onboarding.test.js`:测试 Step 5 的 ChatGPT onboarding 判断与跳过逻辑
- `tests/step5-chatgpt-redirect-race.test.js`:测试 Step 5 跨域跳转 race 下的 chatgpt.com 兜底判断。
- `tests/step5-direct-complete.test.js`:测试 Step 5 在资料页点击提交后立即完成当前步骤
- `tests/step6-login-state.test.js`:测试 Step 6 登录状态判断逻辑。
- `tests/step8-callback-handling.test.js`:测试 Step 8 回调地址捕获逻辑。
- `tests/step8-debugger-stop.test.js`:测试 Step 8 调试器点击在 Stop 场景下的中止行为。
@@ -162,5 +164,6 @@
- `tests/step8-stop-cleanup.test.js`:测试 Step 8 在 Stop 后能正确清理监听器与挂起状态。
- `tests/step9-cpa-mode.test.js`:测试本地 CPA Step 9 策略判断。
- `tests/step9-localhost-cleanup-scope.test.js`:测试 Step 9 仅清理精确命中的 localhost callback 和路径前缀残留页。
- `tests/step9-status-diagnostics.test.js`:测试 Step 9 精确成功判定、红色错误态过滤,以及成功徽标与失败提示并存时的优先级。
- `tests/verification-stop-propagation.test.js`:测试验证码流程在 Stop 场景下的错误传播与不中途降级。
- `tests/verification-flow-polling.test.js`:测试 2925 长轮询参数,以及验证码提交流程中的 `beforeSubmit` 钩子执行顺序。