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` 选择邮箱来源
|
2. 根据 `Mail` 选择邮箱来源
|
||||||
3. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号
|
3. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号
|
||||||
4. 如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取邮箱(Duck / Cloudflare / iCloud 等)
|
4. 如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取邮箱(Duck / Cloudflare / iCloud 等)
|
||||||
5. Step 2 点击注册、填写邮箱并继续到密码页
|
5. Step 2 点击注册、填写邮箱,并按真实落地页进入密码页或直接进入邮箱验证码页
|
||||||
6. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue`
|
6. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue`
|
||||||
7. 继续执行 Step 3 ~ Step 9
|
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
|
### Step 3: Fill Password
|
||||||
|
|
||||||
@@ -485,7 +485,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
|||||||
- 页面要求 `age`
|
- 页面要求 `age`
|
||||||
|
|
||||||
如果页面是生日模式,会填写年月日;如果页面上存在 `input[name='age']`,则直接填写年龄。
|
如果页面是生日模式,会填写年月日;如果页面上存在 `input[name='age']`,则直接填写年龄。
|
||||||
点击 `完成帐户创建` 后,Step 5 会立刻记为完成,不再等待页面跳转结果;只有在提交瞬间发生页面跳转、且后台还没收到完成信号时,才会兜底检查是否已跳到 ChatGPT。
|
点击 `完成帐户创建` 后,Step 5 会立刻记为完成,不再等待页面跳转结果;自动运行在进入 Step 6 前只会等待当前页面加载完成,不再接管 ChatGPT 跳转或 onboarding 跳过逻辑。
|
||||||
|
|
||||||
### Step 6: Login via OAuth
|
### Step 6: Login via OAuth
|
||||||
|
|
||||||
|
|||||||
+100
-107
@@ -3403,6 +3403,10 @@ async function waitForTabUrlMatch(tabId, matcher, options = {}) {
|
|||||||
return tabRuntime.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 = {}) {
|
async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
|
||||||
return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options);
|
return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options);
|
||||||
}
|
}
|
||||||
@@ -3631,7 +3635,7 @@ function isStep9RecoverableAuthError(error) {
|
|||||||
|
|
||||||
function isLegacyStep9RecoverableAuthError(error) {
|
function isLegacyStep9RecoverableAuthError(error) {
|
||||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
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) {
|
function isStepDoneStatus(status) {
|
||||||
@@ -4708,6 +4712,17 @@ async function executeStepAndWait(step, delayAfter = 2000) {
|
|||||||
await executeStep(step);
|
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
|
// Extra delay for page transitions / DOM updates
|
||||||
if (delayAfter > 0) {
|
if (delayAfter > 0) {
|
||||||
await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200));
|
await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200));
|
||||||
@@ -4791,7 +4806,7 @@ const AUTO_STEP_DELAYS = {
|
|||||||
2: 2000,
|
2: 2000,
|
||||||
3: 3000,
|
3: 3000,
|
||||||
4: 2000,
|
4: 2000,
|
||||||
5: 3000,
|
5: 0,
|
||||||
6: 3000,
|
6: 3000,
|
||||||
7: 2000,
|
7: 2000,
|
||||||
8: 2000,
|
8: 2000,
|
||||||
@@ -4996,8 +5011,7 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
|
|||||||
|
|
||||||
async function runAutoSequenceFromStep(startStep, context = {}) {
|
async function runAutoSequenceFromStep(startStep, context = {}) {
|
||||||
const { targetRun, totalRuns, attemptRuns, continued = false } = context;
|
const { targetRun, totalRuns, attemptRuns, continued = false } = context;
|
||||||
const maxStep9RestartAttempts = 5;
|
let postStep6RestartCount = 0;
|
||||||
let step9RestartAttempts = 0;
|
|
||||||
|
|
||||||
if (continued) {
|
if (continued) {
|
||||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info');
|
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info');
|
||||||
@@ -5048,27 +5062,31 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
|
|||||||
}
|
}
|
||||||
step += 1;
|
step += 1;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const latestState = await getState();
|
const restartDecision = await getPostStep6AutoRestartDecision(step, err);
|
||||||
const currentMail = getMailConfig(latestState);
|
if (restartDecision.shouldRestart) {
|
||||||
const shouldRetryStep9 = step === 9
|
postStep6RestartCount += 1;
|
||||||
&& (
|
const authState = restartDecision.authState;
|
||||||
isLegacyStep9RecoverableAuthError(err)
|
const authStateLabel = authState?.state ? getLoginAuthStateLabel(authState.state) : '未知页面';
|
||||||
|| (currentMail.provider === HOTMAIL_PROVIDER && isStep9RecoverableAuthError(err))
|
const authStateSuffix = authState?.url
|
||||||
)
|
? `当前认证页:${authStateLabel}(${authState.url})`
|
||||||
&& step9RestartAttempts < maxStep9RestartAttempts;
|
: authState?.state
|
||||||
|
? `当前认证页:${authStateLabel}`
|
||||||
if (shouldRetryStep9) {
|
: '未获取到认证页状态';
|
||||||
step9RestartAttempts += 1;
|
|
||||||
await addLog(
|
await addLog(
|
||||||
`步骤 9:检测到 CPA 认证失败,正在回到步骤 6 重新开始授权流程(${step9RestartAttempts}/${maxStep9RestartAttempts})...`,
|
`步骤 ${step}:检测到报错且当前未进入 add-phone,正在回到步骤 6 重新开始授权流程(第 ${postStep6RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`,
|
||||||
'warn'
|
'warn'
|
||||||
);
|
);
|
||||||
await invalidateDownstreamAfterStepRestart(6, {
|
await invalidateDownstreamAfterStepRestart(5, {
|
||||||
logLabel: `步骤 9 认证失败后准备回到步骤 6 重试(${step9RestartAttempts}/${maxStep9RestartAttempts})`,
|
logLabel: `步骤 ${step} 报错后准备回到步骤 6 重试(第 ${postStep6RestartCount} 次重开)`,
|
||||||
});
|
});
|
||||||
step = 6;
|
step = 6;
|
||||||
continue;
|
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;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5297,13 +5315,7 @@ const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({
|
|||||||
addLog,
|
addLog,
|
||||||
generateRandomBirthday,
|
generateRandomBirthday,
|
||||||
generateRandomName,
|
generateRandomName,
|
||||||
getState,
|
|
||||||
getTabId,
|
|
||||||
handleChatgptOnboardingSkip,
|
|
||||||
isRetryableContentScriptTransportError,
|
|
||||||
LOG_PREFIX,
|
|
||||||
sendToContentScript,
|
sendToContentScript,
|
||||||
waitForStep5ChatgptRedirect,
|
|
||||||
});
|
});
|
||||||
const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
|
const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
|
||||||
addLog,
|
addLog,
|
||||||
@@ -5651,91 +5663,10 @@ async function executeStep4(state) {
|
|||||||
// Step 5: Fill Name & Birthday (via signup-page.js)
|
// 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) {
|
async function executeStep5(state) {
|
||||||
return step5Executor.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
|
// Step 6 Cookie Cleanup
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -5880,7 +5811,69 @@ function isStep6RecoverableResult(result) {
|
|||||||
return result?.step6Outcome === 'recoverable';
|
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(
|
const result = await sendToContentScriptResilient(
|
||||||
'signup-page',
|
'signup-page',
|
||||||
{
|
{
|
||||||
@@ -5891,7 +5884,7 @@ async function getLoginAuthStateFromContent() {
|
|||||||
{
|
{
|
||||||
timeoutMs: 15000,
|
timeoutMs: 15000,
|
||||||
retryDelayMs: 600,
|
retryDelayMs: 600,
|
||||||
logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...',
|
logMessage,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
function isLegacyStep9RecoverableAuthError(error) {
|
function isLegacyStep9RecoverableAuthError(error) {
|
||||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
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) {
|
function isStepDoneStatus(status) {
|
||||||
|
|||||||
@@ -6,74 +6,21 @@
|
|||||||
addLog,
|
addLog,
|
||||||
generateRandomBirthday,
|
generateRandomBirthday,
|
||||||
generateRandomName,
|
generateRandomName,
|
||||||
getState,
|
|
||||||
getTabId,
|
|
||||||
handleChatgptOnboardingSkip,
|
|
||||||
isRetryableContentScriptTransportError,
|
|
||||||
LOG_PREFIX,
|
|
||||||
sendToContentScript,
|
sendToContentScript,
|
||||||
waitForStep5ChatgptRedirect,
|
|
||||||
} = deps;
|
} = 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() {
|
async function executeStep5() {
|
||||||
const { firstName, lastName } = generateRandomName();
|
const { firstName, lastName } = generateRandomName();
|
||||||
const { year, month, day } = generateRandomBirthday();
|
const { year, month, day } = generateRandomBirthday();
|
||||||
|
|
||||||
await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
|
await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
|
||||||
|
|
||||||
let step5Result = null;
|
await sendToContentScript('signup-page', {
|
||||||
let step5TransportError = null;
|
type: 'EXECUTE_STEP',
|
||||||
|
step: 5,
|
||||||
try {
|
source: 'background',
|
||||||
step5Result = await sendToContentScript('signup-page', {
|
payload: { firstName, lastName, year, month, day },
|
||||||
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 };
|
return { executeStep5 };
|
||||||
|
|||||||
@@ -202,6 +202,29 @@
|
|||||||
return null;
|
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 = {}) {
|
async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
|
||||||
const {
|
const {
|
||||||
inject = null,
|
inject = null,
|
||||||
@@ -619,6 +642,7 @@
|
|||||||
sendToContentScriptResilient,
|
sendToContentScriptResilient,
|
||||||
sendToMailContentScriptResilient,
|
sendToMailContentScriptResilient,
|
||||||
summarizeMessageResultForDebug,
|
summarizeMessageResultForDebug,
|
||||||
|
waitForTabComplete,
|
||||||
waitForTabUrlFamily,
|
waitForTabUrlFamily,
|
||||||
waitForTabUrlMatch,
|
waitForTabUrlMatch,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,12 +38,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isRecoverableStep9AuthFailure(statusText) {
|
function isRecoverableStep9AuthFailure(statusText) {
|
||||||
const text = String(statusText || '').trim();
|
const text = String(statusText || '').replace(/\s+/g, ' ').trim();
|
||||||
if (!/认证失败:\s*/i.test(text)) {
|
if (!text) {
|
||||||
return false;
|
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 {
|
return {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|
|||||||
|| message.type === 'RESEND_VERIFICATION_CODE'
|
|| message.type === 'RESEND_VERIFICATION_CODE'
|
||||||
|| message.type === 'ENSURE_SIGNUP_ENTRY_READY'
|
|| message.type === 'ENSURE_SIGNUP_ENTRY_READY'
|
||||||
|| message.type === 'ENSURE_SIGNUP_PASSWORD_PAGE_READY'
|
|| message.type === 'ENSURE_SIGNUP_PASSWORD_PAGE_READY'
|
||||||
|| message.type === 'CHATGPT_SKIP_ONBOARDING'
|
|
||||||
) {
|
) {
|
||||||
resetStopState();
|
resetStopState();
|
||||||
handleCommand(message).then((result) => {
|
handleCommand(message).then((result) => {
|
||||||
@@ -84,8 +83,6 @@ async function handleCommand(message) {
|
|||||||
return getStep8State();
|
return getStep8State();
|
||||||
case 'STEP8_TRIGGER_CONTINUE':
|
case 'STEP8_TRIGGER_CONTINUE':
|
||||||
return await step8_triggerContinue(message.payload);
|
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 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 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) {
|
function isVisibleElement(el) {
|
||||||
if (!el) return false;
|
if (!el) return false;
|
||||||
@@ -780,69 +775,6 @@ function getStep5ErrorText() {
|
|||||||
return messages.find((text) => STEP5_SUBMIT_ERROR_PATTERN.test(text)) || '';
|
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() {
|
function isSignupPasswordPage() {
|
||||||
return /\/create-account\/password(?:[/?#]|$)/i.test(location.pathname || '');
|
return /\/create-account\/password(?:[/?#]|$)/i.test(location.pathname || '');
|
||||||
@@ -2208,86 +2140,3 @@ async function step5_fillNameBirthday(payload) {
|
|||||||
return completionPayload;
|
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) {
|
for (const candidate of candidates) {
|
||||||
if (seen.has(candidate)) continue;
|
if (seen.has(candidate)) continue;
|
||||||
seen.add(candidate);
|
seen.add(candidate);
|
||||||
entries.push({
|
entries.push(createStep9Entry(candidate, selector));
|
||||||
element: candidate,
|
|
||||||
selector,
|
|
||||||
visible: isVisibleElement(candidate),
|
|
||||||
text: (candidate.textContent || '').replace(/\s+/g, ' ').trim(),
|
|
||||||
className: String(candidate.className || '').replace(/\s+/g, ' ').trim(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +221,8 @@ function summarizeStatusBadgeEntries(entries) {
|
|||||||
.map((entry, index) => {
|
.map((entry, index) => {
|
||||||
const text = entry.text || '(空文本)';
|
const text = entry.text || '(空文本)';
|
||||||
const className = entry.className ? ` class=${getInlineTextSnippet(entry.className, 80)}` : '';
|
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(' | ');
|
.join(' | ');
|
||||||
}
|
}
|
||||||
@@ -242,6 +237,20 @@ function normalizeStep9StatusText(statusText) {
|
|||||||
return String(statusText || '').replace(/\s+/g, ' ').trim();
|
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) {
|
function isStep9SuccessStatus(statusText) {
|
||||||
return STEP9_SUCCESS_STATUSES.has(normalizeStep9StatusText(statusText));
|
return STEP9_SUCCESS_STATUSES.has(normalizeStep9StatusText(statusText));
|
||||||
}
|
}
|
||||||
@@ -251,37 +260,188 @@ function isStep9SuccessLikeStatus(statusText) {
|
|||||||
return /authentication successful|аутентификац.*успеш|认证成功/i.test(text);
|
return /authentication successful|аутентификац.*успеш|认证成功/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusBadgeDiagnostics() {
|
function parseCssColorChannels(colorText) {
|
||||||
const entries = getStatusBadgeEntries();
|
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 visibleEntries = entries.filter((entry) => entry.visible);
|
||||||
const selectedEntry = visibleEntries[0] || null;
|
|
||||||
const selectedText = selectedEntry?.text || '';
|
|
||||||
const successLikeEntries = visibleEntries.filter((entry) => isStep9SuccessLikeStatus(entry.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 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 {
|
return {
|
||||||
selectedText,
|
selectedText,
|
||||||
|
exactSuccessText: exactSuccessEntries[0]?.text || '',
|
||||||
|
failureText: decisiveFailureEntry?.text || '',
|
||||||
visibleCount: visibleEntries.length,
|
visibleCount: visibleEntries.length,
|
||||||
visibleSummary,
|
visibleSummary,
|
||||||
hasSuccessLikeVisibleBadge: successLikeEntries.length > 0,
|
hasSuccessLikeVisibleBadge: successLikeEntries.length > 0,
|
||||||
hasExactSuccessVisibleBadge: exactSuccessEntries.length > 0,
|
hasExactSuccessVisibleBadge: exactSuccessEntries.length > 0,
|
||||||
successLikeSummary: summarizeStatusBadgeEntries(successLikeEntries),
|
hasFailureVisibleBadge: allFailureEntries.length > 0,
|
||||||
exactSuccessSummary: summarizeStatusBadgeEntries(exactSuccessEntries),
|
hasErrorStyledVisibleBadge: errorStyledEntries.length > 0,
|
||||||
|
successLikeSummary,
|
||||||
|
exactSuccessSummary,
|
||||||
|
failureSummary,
|
||||||
|
pageErrorSummary,
|
||||||
|
errorStyledSummary,
|
||||||
pageSnippet,
|
pageSnippet,
|
||||||
signature: JSON.stringify({
|
signature: JSON.stringify({
|
||||||
selectedText,
|
selectedText,
|
||||||
visibleCount: visibleEntries.length,
|
visibleCount: visibleEntries.length,
|
||||||
visibleSummary,
|
visibleSummary,
|
||||||
successLikeSummary: summarizeStatusBadgeEntries(successLikeEntries),
|
successLikeSummary,
|
||||||
|
exactSuccessSummary,
|
||||||
|
failureSummary,
|
||||||
|
pageErrorSummary,
|
||||||
|
errorStyledSummary,
|
||||||
}),
|
}),
|
||||||
summary: selectedText
|
summary: selectedText
|
||||||
? `当前选中徽标="${getInlineTextSnippet(selectedText, 80)}";可见徽标 ${visibleEntries.length} 个:${visibleSummary}`
|
? `当前聚焦状态="${getInlineTextSnippet(selectedText, 80)}";可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}`
|
||||||
: `当前未选中任何可见状态徽标;可见徽标 ${visibleEntries.length} 个:${visibleSummary};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
|
: `当前未选中任何可见状态徽标;可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStatusBadgeDiagnostics() {
|
||||||
|
return buildStep9StatusDiagnostics(
|
||||||
|
getStatusBadgeEntries(),
|
||||||
|
getStep9PageErrorEntries(),
|
||||||
|
getPageTextSnippet()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getStatusBadgeElement() {
|
function getStatusBadgeElement() {
|
||||||
const visibleEntry = getStatusBadgeEntries().find((entry) => entry.visible);
|
const visibleEntry = getStatusBadgeEntries().find((entry) => entry.visible);
|
||||||
return visibleEntry ? visibleEntry.element : null;
|
return visibleEntry ? visibleEntry.element : null;
|
||||||
@@ -292,20 +452,16 @@ function getStatusBadgeText() {
|
|||||||
return diagnostics.selectedText;
|
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) {
|
async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS) {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
let lastDiagnosticsSignature = '';
|
let lastDiagnosticsSignature = '';
|
||||||
let lastHeartbeatLoggedAt = 0;
|
let lastHeartbeatLoggedAt = 0;
|
||||||
let lastSuccessLikeMismatchSignature = '';
|
let lastSuccessLikeMismatchSignature = '';
|
||||||
|
let lastSuccessFailureConflictSignature = '';
|
||||||
|
|
||||||
while (Date.now() - start < timeout) {
|
while (Date.now() - start < timeout) {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
const diagnostics = getStatusBadgeDiagnostics();
|
const diagnostics = getStatusBadgeDiagnostics();
|
||||||
const statusText = diagnostics.selectedText;
|
|
||||||
const elapsed = Date.now() - start;
|
const elapsed = Date.now() - start;
|
||||||
|
|
||||||
if (diagnostics.signature !== lastDiagnosticsSignature) {
|
if (diagnostics.signature !== lastDiagnosticsSignature) {
|
||||||
@@ -324,36 +480,59 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
|
|||||||
selectedText: diagnostics.selectedText,
|
selectedText: diagnostics.selectedText,
|
||||||
successLikeSummary: diagnostics.successLikeSummary,
|
successLikeSummary: diagnostics.successLikeSummary,
|
||||||
visibleSummary: diagnostics.visibleSummary,
|
visibleSummary: diagnostics.visibleSummary,
|
||||||
|
errorStyledSummary: diagnostics.errorStyledSummary,
|
||||||
});
|
});
|
||||||
if (mismatchSignature !== lastSuccessLikeMismatchSignature) {
|
if (mismatchSignature !== lastSuccessLikeMismatchSignature) {
|
||||||
lastSuccessLikeMismatchSignature = mismatchSignature;
|
lastSuccessLikeMismatchSignature = mismatchSignature;
|
||||||
|
const errorStyledSuffix = diagnostics.hasErrorStyledVisibleBadge
|
||||||
|
? `;错误样式徽标:${diagnostics.errorStyledSummary}`
|
||||||
|
: '';
|
||||||
log(
|
log(
|
||||||
`步骤 9:检测到“认证成功”相关徽标,但未命中精确条件。当前选中="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}`,
|
`步骤 9:检测到“认证成功”相关徽标,但未命中精确成功条件。当前聚焦="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}${errorStyledSuffix}`,
|
||||||
'warn'
|
'warn'
|
||||||
);
|
);
|
||||||
console.warn(LOG_PREFIX, '[Step 9] success-like badge detected without exact match', diagnostics);
|
console.warn(LOG_PREFIX, '[Step 9] success-like badge detected without exact match', diagnostics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOAuthCallbackTimeoutFailure(statusText)) {
|
if (diagnostics.hasExactSuccessVisibleBadge && diagnostics.hasFailureVisibleBadge) {
|
||||||
throw new Error(`STEP9_OAUTH_TIMEOUT::${statusText}`);
|
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)) {
|
if (diagnostics.exactSuccessText) {
|
||||||
return statusText;
|
return diagnostics.exactSuccessText;
|
||||||
}
|
}
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalDiagnostics = getStatusBadgeDiagnostics();
|
const finalDiagnostics = getStatusBadgeDiagnostics();
|
||||||
const finalText = finalDiagnostics.selectedText;
|
const finalText = finalDiagnostics.failureText || finalDiagnostics.selectedText;
|
||||||
const diagnosticsSuffix = ` 当前诊断:${finalDiagnostics.summary}`;
|
const diagnosticsSuffix = ` 当前诊断:${finalDiagnostics.summary}`;
|
||||||
if (isOAuthCallbackTimeoutFailure(finalText)) {
|
if (isOAuthCallbackTimeoutFailure(finalText)) {
|
||||||
throw new Error(`STEP9_OAUTH_TIMEOUT::${finalText}${diagnosticsSuffix}`);
|
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(`STEP9_OAUTH_RETRY::${finalText}${diagnosticsSuffix}`);
|
||||||
}
|
}
|
||||||
throw new Error(finalText
|
throw new Error(finalText
|
||||||
|
|||||||
@@ -71,6 +71,16 @@ test('isRecoverableStep9AuthFailure matches timeout and CPA auth failure statuse
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
isRecoverableStep9AuthFailure('认证失败: timeout of 30000ms exceeded'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
isRecoverableStep9AuthFailure('回调 URL 提交失败: oauth flow is not pending'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
isRecoverableStep9AuthFailure('认证成功!'),
|
isRecoverableStep9AuthFailure('认证成功!'),
|
||||||
false
|
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 globalScope = {};
|
||||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep5;`)(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 = {
|
const events = {
|
||||||
redirectWaitCalls: 0,
|
|
||||||
onboardingCalls: 0,
|
|
||||||
logs: [],
|
logs: [],
|
||||||
|
messages: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const transportError = new Error('The message port closed before a response was received.');
|
|
||||||
|
|
||||||
const executor = api.createStep5Executor({
|
const executor = api.createStep5Executor({
|
||||||
addLog: async (message, level) => {
|
addLog: async (message, level) => {
|
||||||
events.logs.push({ message, level: level || 'info' });
|
events.logs.push({ message, level: level || 'info' });
|
||||||
},
|
},
|
||||||
generateRandomBirthday: () => ({ year: 2003, month: 6, day: 19 }),
|
generateRandomBirthday: () => ({ year: 2003, month: 6, day: 19 }),
|
||||||
generateRandomName: () => ({ firstName: 'Test', lastName: 'User' }),
|
generateRandomName: () => ({ firstName: 'Test', lastName: 'User' }),
|
||||||
getState: async () => ({
|
sendToContentScript: async (source, message) => {
|
||||||
stepStatuses: {
|
events.messages.push({ source, message });
|
||||||
5: 'completed',
|
return { accepted: true };
|
||||||
},
|
|
||||||
}),
|
|
||||||
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/' };
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executor.executeStep5();
|
await executor.executeStep5();
|
||||||
|
|
||||||
assert.equal(events.redirectWaitCalls, 0, '收到完成信号后不应再等待 ChatGPT 跳转');
|
assert.deepStrictEqual(events.messages, [
|
||||||
assert.equal(events.onboardingCalls, 0, '收到完成信号后不应再触发 onboarding 跳过');
|
{
|
||||||
assert.ok(
|
source: 'signup-page',
|
||||||
events.logs.some(({ message }) => /已收到完成信号,直接结束当前步骤/.test(message)),
|
message: {
|
||||||
'应记录 Step 5 已按完成信号直接结束'
|
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');
|
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. 解析本轮应使用的邮箱
|
1. 解析本轮应使用的邮箱
|
||||||
2. 打开或复用注册页
|
2. 打开或复用注册页
|
||||||
3. 点击注册入口并提交邮箱
|
3. 点击注册入口并提交邮箱
|
||||||
4. 等待跳转到密码页
|
4. 等待邮箱提交后的真实落地页
|
||||||
|
5. 如果进入密码页,则继续执行 Step 3
|
||||||
|
6. 如果直接进入邮箱验证码页,则自动跳过 Step 3 并进入 Step 4
|
||||||
|
|
||||||
### Step 3
|
### Step 3
|
||||||
|
|
||||||
@@ -254,7 +256,7 @@
|
|||||||
1. 生成随机姓名和生日
|
1. 生成随机姓名和生日
|
||||||
2. 内容脚本填写资料并点击“完成帐户创建”
|
2. 内容脚本填写资料并点击“完成帐户创建”
|
||||||
3. 点击后立即上报 Step 5 完成,不再等待页面结果
|
3. 点击后立即上报 Step 5 完成,不再等待页面结果
|
||||||
4. 如果提交瞬间页面跳转导致响应通道中断,后台仅在未收到完成信号时兜底判断是否跳到 ChatGPT
|
4. 自动运行在进入 Step 6 前仅额外等待当前页面加载完成,不再在 Step 5 阶段接管 ChatGPT 跳转或 onboarding 跳过
|
||||||
|
|
||||||
### Step 6
|
### Step 6
|
||||||
|
|
||||||
@@ -270,6 +272,7 @@
|
|||||||
4. 登录
|
4. 登录
|
||||||
5. 确保真正进入验证码页
|
5. 确保真正进入验证码页
|
||||||
6. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次
|
6. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次
|
||||||
|
7. 自动运行一旦进入步骤 6 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 6 重新开始授权流程
|
||||||
|
|
||||||
### Step 8
|
### Step 8
|
||||||
|
|
||||||
@@ -296,9 +299,11 @@
|
|||||||
2. 判断是 CPA 还是 SUB2API
|
2. 判断是 CPA 还是 SUB2API
|
||||||
3. 打开相应后台
|
3. 打开相应后台
|
||||||
4. 提交回调地址
|
4. 提交回调地址
|
||||||
5. 完成平台侧验证
|
5. 仅当出现精确成功徽标,且该徽标不是红色/错误态、页面上也没有同时可见的失败提示时,才判定成功
|
||||||
6. 追加账号运行历史成功记录
|
6. 识别 `认证失败:*`、`认证失败: timeout of 30000ms exceeded`、`回调 URL 提交失败: oauth flow is not pending` 等失败提示并立即报错
|
||||||
7. 做成功后的清理与标记
|
7. 完成平台侧验证
|
||||||
|
8. 追加账号运行历史成功记录
|
||||||
|
9. 做成功后的清理与标记
|
||||||
|
|
||||||
## 7. 邮箱与 provider 链路
|
## 7. 邮箱与 provider 链路
|
||||||
|
|
||||||
@@ -359,6 +364,8 @@
|
|||||||
2. 计算是否从中断点继续
|
2. 计算是否从中断点继续
|
||||||
3. 每轮执行前重置必要运行态
|
3. 每轮执行前重置必要运行态
|
||||||
4. 执行 `runAutoSequenceFromStep`
|
4. 执行 `runAutoSequenceFromStep`
|
||||||
|
- 步骤 6 内部仍保留登录态恢复的有限重试
|
||||||
|
- 一旦进入步骤 6~9,遇到报错且认证流程未进入 `add-phone`,则自动回到步骤 6 无限重开,直到成功、手动停止或命中 `add-phone`
|
||||||
5. 如果失败,根据设置决定:
|
5. 如果失败,根据设置决定:
|
||||||
- 立即停止
|
- 立即停止
|
||||||
- 当前轮重试
|
- 当前轮重试
|
||||||
|
|||||||
+9
-6
@@ -55,16 +55,16 @@
|
|||||||
- `background/steps/fetch-login-code.js`:步骤 7 实现,负责登录验证码阶段的邮箱轮询与回退控制。
|
- `background/steps/fetch-login-code.js`:步骤 7 实现,负责登录验证码阶段的邮箱轮询与回退控制。
|
||||||
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口。
|
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口。
|
||||||
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。
|
- `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/oauth-login.js`:步骤 6 实现,负责刷新 OAuth 链接、登录和确保进入验证码页。
|
||||||
- `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
|
- `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
|
||||||
- `background/steps/platform-verify.js`:步骤 9 实现,负责 CPA / SUB2API 回调验证。
|
- `background/steps/platform-verify.js`:步骤 9 实现,负责 CPA / SUB2API 回调验证。
|
||||||
- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
|
- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
|
||||||
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与密码页等待。
|
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与提交后落地页分支判断。
|
||||||
|
|
||||||
## `content/`
|
## `content/`
|
||||||
|
|
||||||
- `content/activation-utils.js`:内容脚本通用激活策略工具,负责按钮点击方式判断等轻量辅助逻辑。
|
- `content/activation-utils.js`:内容脚本通用激活策略工具,负责按钮点击方式判断与 Step 9 可恢复失败文案判断。
|
||||||
- `content/duck-mail.js`:DuckDuckGo Email Protection 页面脚本,负责生成或读取 `@duck.com` 地址。
|
- `content/duck-mail.js`:DuckDuckGo Email Protection 页面脚本,负责生成或读取 `@duck.com` 地址。
|
||||||
- `content/gmail-mail.js`:Gmail 邮箱轮询脚本,负责在 Gmail 页面中匹配验证码邮件。
|
- `content/gmail-mail.js`:Gmail 邮箱轮询脚本,负责在 Gmail 页面中匹配验证码邮件。
|
||||||
- `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
|
- `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
- `content/signup-page.js`:注册/登录/授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行。
|
- `content/signup-page.js`:注册/登录/授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行。
|
||||||
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调。
|
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调。
|
||||||
- `content/utils.js`:内容脚本公共工具层,负责日志、READY/COMPLETE/ERROR 上报、元素等待、输入与点击。
|
- `content/utils.js`:内容脚本公共工具层,负责日志、READY/COMPLETE/ERROR 上报、元素等待、输入与点击。
|
||||||
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址和提交回调 URL。
|
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做 Step 9 判定。
|
||||||
|
|
||||||
## `data/`
|
## `data/`
|
||||||
|
|
||||||
@@ -118,6 +118,7 @@
|
|||||||
|
|
||||||
- `tests/activation-utils.test.js`:测试内容脚本激活策略与 Step 9 可恢复错误判断。
|
- `tests/activation-utils.test.js`:测试内容脚本激活策略与 Step 9 可恢复错误判断。
|
||||||
- `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文。
|
- `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/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。
|
||||||
- `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。
|
- `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。
|
||||||
- `tests/background-account-run-history-module.test.js`:测试账号运行历史模块已接入、导出工厂,并能在不启用本地 helper 时正确持久化记录。
|
- `tests/background-account-run-history-module.test.js`:测试账号运行历史模块已接入、导出工厂,并能在不启用本地 helper 时正确持久化记录。
|
||||||
@@ -128,10 +129,12 @@
|
|||||||
- `tests/background-logging-status-module.test.js`:测试日志/状态模块已接入且导出工厂。
|
- `tests/background-logging-status-module.test.js`:测试日志/状态模块已接入且导出工厂。
|
||||||
- `tests/background-luckmail.test.js`:测试 LuckMail 相关后台逻辑,如购买、复用、标记已用与重置。
|
- `tests/background-luckmail.test.js`:测试 LuckMail 相关后台逻辑,如购买、复用、标记已用与重置。
|
||||||
- `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。
|
- `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-navigation-utils-module.test.js`:测试导航工具模块已接入且导出工厂。
|
||||||
- `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。
|
- `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。
|
||||||
- `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。
|
- `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂。
|
||||||
- `tests/background-step-modules.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-step6-retry-limit.test.js`:测试 Step 6 的有限重试上限,以及 Step 7 回放时可跳过登录前 Cookie 清理。
|
||||||
- `tests/background-step7-recovery.test.js`:测试 Step 7 在 CPA 模式下会先回放 Step 6 再提交登录验证码,并为 2925 关闭重发间隔。
|
- `tests/background-step7-recovery.test.js`:测试 Step 7 在 CPA 模式下会先回放 Step 6 再提交登录验证码,并为 2925 关闭重发间隔。
|
||||||
- `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。
|
- `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入。
|
||||||
@@ -153,8 +156,7 @@
|
|||||||
- `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
|
- `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
|
||||||
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
|
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
|
||||||
- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。
|
- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。
|
||||||
- `tests/step5-chatgpt-onboarding.test.js`:测试 Step 5 的 ChatGPT onboarding 判断与跳过逻辑。
|
- `tests/step5-direct-complete.test.js`:测试 Step 5 在资料页点击提交后立即完成当前步骤。
|
||||||
- `tests/step5-chatgpt-redirect-race.test.js`:测试 Step 5 跨域跳转 race 下的 chatgpt.com 兜底判断。
|
|
||||||
- `tests/step6-login-state.test.js`:测试 Step 6 登录状态判断逻辑。
|
- `tests/step6-login-state.test.js`:测试 Step 6 登录状态判断逻辑。
|
||||||
- `tests/step8-callback-handling.test.js`:测试 Step 8 回调地址捕获逻辑。
|
- `tests/step8-callback-handling.test.js`:测试 Step 8 回调地址捕获逻辑。
|
||||||
- `tests/step8-debugger-stop.test.js`:测试 Step 8 调试器点击在 Stop 场景下的中止行为。
|
- `tests/step8-debugger-stop.test.js`:测试 Step 8 调试器点击在 Stop 场景下的中止行为。
|
||||||
@@ -162,5 +164,6 @@
|
|||||||
- `tests/step8-stop-cleanup.test.js`:测试 Step 8 在 Stop 后能正确清理监听器与挂起状态。
|
- `tests/step8-stop-cleanup.test.js`:测试 Step 8 在 Stop 后能正确清理监听器与挂起状态。
|
||||||
- `tests/step9-cpa-mode.test.js`:测试本地 CPA Step 9 策略判断。
|
- `tests/step9-cpa-mode.test.js`:测试本地 CPA Step 9 策略判断。
|
||||||
- `tests/step9-localhost-cleanup-scope.test.js`:测试 Step 9 仅清理精确命中的 localhost callback 和路径前缀残留页。
|
- `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-stop-propagation.test.js`:测试验证码流程在 Stop 场景下的错误传播与不中途降级。
|
||||||
- `tests/verification-flow-polling.test.js`:测试 2925 长轮询参数,以及验证码提交流程中的 `beforeSubmit` 钩子执行顺序。
|
- `tests/verification-flow-polling.test.js`:测试 2925 长轮询参数,以及验证码提交流程中的 `beforeSubmit` 钩子执行顺序。
|
||||||
|
|||||||
Reference in New Issue
Block a user