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:
@@ -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
@@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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,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', {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return { executeStep5 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 (isStep9SuccessStatus(statusText)) {
|
||||
return 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 (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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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` 钩子执行顺序。
|
||||
|
||||
Reference in New Issue
Block a user