抽取cookie清理逻辑为新的第六步

This commit is contained in:
QLHazyCoder
2026-04-17 15:56:19 +08:00
parent b387ebfa91
commit cbe9b1fabf
30 changed files with 630 additions and 603 deletions
+35 -26
View File
@@ -126,7 +126,7 @@
2. 填好 `SUB2API` 地址、登录邮箱、登录密码、分组名 2. 填好 `SUB2API` 地址、登录邮箱、登录密码、分组名
3. `Mail``邮箱生成` 的配置方式同方案 A 3. `Mail``邮箱生成` 的配置方式同方案 A
4. Step 1 会直接在 SUB2API 后台生成 OAuth 链接 4. Step 1 会直接在 SUB2API 后台生成 OAuth 链接
5. Step 9 会把 localhost 回调提交回 SUB2API,并直接创建 OpenAI 账号 5. Step 10 会把 localhost 回调提交回 SUB2API,并直接创建 OpenAI 账号
### 方案 C`Hotmail 账号池` ### 方案 C`Hotmail 账号池`
@@ -146,7 +146,7 @@
http(s)://<your-host>/management.html#/oauth http(s)://<your-host>/management.html#/oauth
``` ```
Step 1 和 Step 9 都依赖这个地址。 Step 1 和 Step 10 都依赖这个地址。
### `Mail` ### `Mail`
@@ -416,17 +416,18 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
### 单步模式 ### 单步模式
侧边栏共有 9 个步骤按钮,可逐步执行: 侧边栏共有 10 个步骤按钮,可逐步执行:
1. `Open ChatGPT` 1. `Open ChatGPT`
2. `Signup + Email` 2. `Signup + Email`
3. `Fill Password` 3. `Fill Password`
4. `Get Signup Code` 4. `Get Signup Code`
5. `Fill Name / Birthday` 5. `Fill Name / Birthday`
6. `Login via OAuth` 6. `Clear Login Cookies`
7. `Get Login Code` 7. `Login via OAuth`
8. `Manual OAuth Confirm` 8. `Get Login Code`
9. `CPA Verify` 9. `Manual OAuth Confirm`
10. `CPA Verify`
### Auto 模式 ### Auto 模式
@@ -440,7 +441,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
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 10
也就是说: 也就是说:
@@ -511,18 +512,26 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
如果页面是生日模式,会填写年月日;如果页面上存在 `input[name='age']`,则直接填写年龄。 如果页面是生日模式,会填写年月日;如果页面上存在 `input[name='age']`,则直接填写年龄。
点击 `完成帐户创建` 后,Step 5 会立刻记为完成,不再等待页面跳转结果;自动运行在进入 Step 6 前只会等待当前页面加载完成,不再接管 ChatGPT 跳转或 onboarding 跳过逻辑。 点击 `完成帐户创建` 后,Step 5 会立刻记为完成,不再等待页面跳转结果;自动运行在进入 Step 6 前只会等待当前页面加载完成,不再接管 ChatGPT 跳转或 onboarding 跳过逻辑。
### Step 6: Login via OAuth ### Step 6: Clear Login Cookies
在登录前会先重新获取一遍最新的 CPA OAuth 链接,再使用刚注册的账号登录。 这一步只负责登录前清理环境:
当前 Step 6 的完成标准不是“邮箱/密码已提交”,而是:
- 开始前先等待 10 秒 - 开始前先等待 10 秒
- 直接删除 `chatgpt.com / openai.com` 相关 cookies - 直接删除 `chatgpt.com / openai.com` 相关 cookies
- 再去 CPA / SUB2API 刷新最新 OAuth 链接 - 必要时再用 `browsingData` 补扫一次
把 cookies 清理独立成单独步骤后,后续登录链路的重开锚点就不再落在这里。
### Step 7: Login via OAuth
这一步会重新获取一遍最新的 CPA OAuth 链接,再使用刚注册的账号登录。
当前 Step 7 的完成标准不是“邮箱/密码已提交”,而是:
- 已刷新到最新 OAuth 链接
- 认证页已经真正进入登录验证码页面 - 认证页已经真正进入登录验证码页面
- 如遇登录超时报错,会先尝试自动点击认证页上的 `重试` 恢复当前页面,再由后台按既有逻辑重跑整个 Step 6 - 如遇登录超时报错,会先尝试自动点击认证页上的 `重试` 恢复当前页面,再由后台按既有逻辑重跑整个 Step 7
- 如遇登录页长时间停滞,会由后台刷新 OAuth 后重跑整个 Step 6 - 如遇登录页长时间停滞,会由后台刷新 OAuth 后重跑整个 Step 7
支持: 支持:
@@ -530,23 +539,23 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
- 必要时切换到一次性验证码登录 - 必要时切换到一次性验证码登录
- 直到登录验证码页就绪才算步骤完成 - 直到登录验证码页就绪才算步骤完成
### Step 7: Get Login Code ### Step 8: Get Login Code
Step 7 默认要求当前认证页已经处于登录验证码页。 Step 8 默认要求当前认证页已经处于登录验证码页。
它只负责: 它只负责:
- 打开邮箱并轮询登录验证码 - 打开邮箱并轮询登录验证码
- 填写并提交登录验证码 - 填写并提交登录验证码
- 验证码链路失败后按有限次数回退到 Step 6 - 验证码链路失败后按有限次数回退到 Step 7
与 Step 4 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。 与 Step 4 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。
### Step 8: Manual OAuth Confirm ### Step 9: Manual OAuth Confirm
严格回调捕获规则: 严格回调捕获规则:
- 步骤 8 现在只接受 `http(s)://localhost:<port>/auth/callback?code=...&state=...``http(s)://127.0.0.1:<port>/auth/callback?code=...&state=...` - 步骤 9 现在只接受 `http(s)://localhost:<port>/auth/callback?code=...&state=...``http(s)://127.0.0.1:<port>/auth/callback?code=...&state=...`
- 监听范围只限于当前 OAuth 认证标签页的主 frame 跳转 - 监听范围只限于当前 OAuth 认证标签页的主 frame 跳转
- 普通 `localhost` 页面,包括本地部署的 CPA 面板,不会再被误判为回调地址 - 普通 `localhost` 页面,包括本地部署的 CPA 面板,不会再被误判为回调地址
@@ -566,15 +575,15 @@ Step 7 默认要求当前认证页已经处于登录验证码页。
- 如果 120 秒内没有捕获到 localhost 回调,会报错超时 - 如果 120 秒内没有捕获到 localhost 回调,会报错超时
- README 中的按钮名称沿用了旧文案,但代码行为是“自动尝试点击” - README 中的按钮名称沿用了旧文案,但代码行为是“自动尝试点击”
### Step 9: CPA Verify ### Step 10: CPA Verify
校验规则: 校验规则:
- 步骤 9 会拒绝任何不是真实 `/auth/callback`,或缺少 `code` / `state` 的本地回调地址 - 步骤 10 会拒绝任何不是真实 `/auth/callback`,或缺少 `code` / `state` 的本地回调地址
- 成功后的清理只会针对 `/auth` 这一类真实回调标签页,不会再泛化清理任意 localhost 路径 - 成功后的清理只会针对 `/auth` 这一类真实回调标签页,不会再泛化清理任意 localhost 路径
- 侧边栏可切换“本地 CPA”策略,默认是 `全部回调` - 侧边栏可切换“本地 CPA”策略,默认是 `全部回调`
- 选择 `全部回调` 时,即使 CPA 部署在本地,也会执行步骤 9 - 选择 `全部回调` 时,即使 CPA 部署在本地,也会执行步骤 10
- 选择 `跳过第9步` 时,仅当本地 CPA 且步骤 8 已拿到回调地址时,才会直接跳过步骤 9 - 选择 `跳过第10步` 时,仅当本地 CPA 且步骤 9 已拿到回调地址时,才会直接跳过步骤 10
回到 CPA 面板: 回到 CPA 面板:
@@ -653,8 +662,8 @@ background.js 后台主控,编排 1~9 步、Tab 复用、状态
manifest.json 扩展清单 manifest.json 扩展清单
data/names.js 随机姓名、生日数据 data/names.js 随机姓名、生日数据
content/utils.js 通用工具:等待元素、点击、日志、停止控制 content/utils.js 通用工具:等待元素、点击、日志、停止控制
content/vps-panel.js CPA 面板步骤:内部 OAuth 刷新 / Step 9 content/vps-panel.js CPA 面板步骤:内部 OAuth 刷新 / Step 10
content/signup-page.js ChatGPT 官网 + OpenAI 注册/登录页步骤:Step 1 / 2 / 3 / 5 / 6 / 8 content/signup-page.js ChatGPT 官网 + OpenAI 注册/登录页步骤:Step 1 / 2 / 3 / 5 / 7 / 9
hotmail-utils.js Hotmail 收信相关通用辅助 hotmail-utils.js Hotmail 收信相关通用辅助
content/duck-mail.js Duck 邮箱自动获取 content/duck-mail.js Duck 邮箱自动获取
content/qq-mail.js QQ 邮箱验证码轮询 content/qq-mail.js QQ 邮箱验证码轮询
+109 -86
View File
@@ -19,6 +19,7 @@ importScripts(
'background/steps/fill-password.js', 'background/steps/fill-password.js',
'background/steps/fetch-signup-code.js', 'background/steps/fetch-signup-code.js',
'background/steps/fill-profile.js', 'background/steps/fill-profile.js',
'background/steps/clear-login-cookies.js',
'background/steps/oauth-login.js', 'background/steps/oauth-login.js',
'background/steps/fetch-login-code.js', 'background/steps/fetch-login-code.js',
'background/steps/confirm-oauth.js', 'background/steps/confirm-oauth.js',
@@ -32,6 +33,14 @@ importScripts(
'content/activation-utils.js' 'content/activation-utils.js'
); );
const SHARED_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.() || [];
const STEP_IDS = SHARED_STEP_DEFINITIONS
.map((definition) => Number(definition?.id))
.filter(Number.isFinite)
.sort((left, right) => left - right);
const LAST_STEP_ID = STEP_IDS[STEP_IDS.length - 1] || 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const { const {
extractVerificationCodeFromMessage, extractVerificationCodeFromMessage,
filterHotmailAccountsByUsage, filterHotmailAccountsByUsage,
@@ -144,7 +153,7 @@ const AUTO_STEP_DELAY_MIN_ALLOWED_SECONDS = 0;
const AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS = 600; const AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS = 600;
const LEGACY_AUTO_STEP_DELAY_KEYS = ['autoStepRandomDelayMinSeconds', 'autoStepRandomDelayMaxSeconds']; const LEGACY_AUTO_STEP_DELAY_KEYS = ['autoStepRandomDelayMinSeconds', 'autoStepRandomDelayMaxSeconds'];
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit'; const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
const DEFAULT_CPA_CALLBACK_MODE = 'step8'; const DEFAULT_CPA_CALLBACK_MODE = 'step9';
const MAIL_2925_MODE_PROVIDE = 'provide'; const MAIL_2925_MODE_PROVIDE = 'provide';
const MAIL_2925_MODE_RECEIVE = 'receive'; const MAIL_2925_MODE_RECEIVE = 'receive';
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE; const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
@@ -258,10 +267,7 @@ const PRE_LOGIN_COOKIE_CLEAR_ORIGINS = [
const DEFAULT_STATE = { const DEFAULT_STATE = {
currentStep: 0, // 当前流程执行到的步骤编号。 currentStep: 0, // 当前流程执行到的步骤编号。
stepStatuses: { stepStatuses: Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending'])),
1: 'pending', 2: 'pending', 3: 'pending', 4: 'pending', 5: 'pending', // 运行时步骤状态映射,不要手动预填。
6: 'pending', 7: 'pending', 8: 'pending', 9: 'pending',
},
oauthUrl: null, // 运行时抓取到的 OAuth 地址,不要手动预填。 oauthUrl: null, // 运行时抓取到的 OAuth 地址,不要手动预填。
email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。 email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。
password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。 password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。
@@ -610,9 +616,14 @@ function normalizeLocalCpaStep9Mode(value = '') {
} }
function normalizeCpaCallbackMode(value = '') { function normalizeCpaCallbackMode(value = '') {
return String(value || '').trim().toLowerCase() === 'step6' const normalized = String(value || '').trim().toLowerCase();
? 'step6' if (normalized === 'step7' || normalized === 'step6') {
: DEFAULT_CPA_CALLBACK_MODE; return 'step7';
}
if (normalized === 'step9' || normalized === 'step8') {
return 'step9';
}
return DEFAULT_CPA_CALLBACK_MODE;
} }
function normalizeCloudflareDomain(rawValue = '') { function normalizeCloudflareDomain(rawValue = '') {
@@ -3491,7 +3502,7 @@ function shouldSkipLoginVerificationForCpaCallback(state) {
return navigationUtils.shouldSkipLoginVerificationForCpaCallback(state); return navigationUtils.shouldSkipLoginVerificationForCpaCallback(state);
} }
return getPanelMode(state) === 'cpa' return getPanelMode(state) === 'cpa'
&& normalizeCpaCallbackMode(state?.cpaCallbackMode) === 'step6'; && normalizeCpaCallbackMode(state?.cpaCallbackMode) === 'step7';
} }
function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) { function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
@@ -3806,7 +3817,7 @@ function getFirstUnfinishedStep(statuses = {}) {
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getFirstUnfinishedStep) { if (typeof loggingStatus !== 'undefined' && loggingStatus?.getFirstUnfinishedStep) {
return loggingStatus.getFirstUnfinishedStep(statuses); return loggingStatus.getFirstUnfinishedStep(statuses);
} }
for (let step = 1; step <= 9; step++) { for (const step of STEP_IDS) {
if (!isStepDoneStatus(statuses[step] || 'pending')) return step; if (!isStepDoneStatus(statuses[step] || 'pending')) return step;
} }
return null; return null;
@@ -3858,14 +3869,14 @@ function getDownstreamStateResets(step) {
localhostUrl: null, localhostUrl: null,
}; };
} }
if (step === 5 || step === 6 || step === 7) { if (step === 5 || step === 6 || step === 7 || step === 8) {
return { return {
lastLoginCode: null, lastLoginCode: null,
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
localhostUrl: null, localhostUrl: null,
}; };
} }
if (step === 8) { if (step === 9) {
return { return {
localhostUrl: null, localhostUrl: null,
}; };
@@ -3879,7 +3890,7 @@ async function invalidateDownstreamAfterStepRestart(step, options = {}) {
const statuses = { ...(state.stepStatuses || {}) }; const statuses = { ...(state.stepStatuses || {}) };
const changedSteps = []; const changedSteps = [];
for (let downstream = step + 1; downstream <= 9; downstream++) { for (let downstream = step + 1; downstream <= LAST_STEP_ID; downstream++) {
if (statuses[downstream] !== 'pending') { if (statuses[downstream] !== 'pending') {
statuses[downstream] = 'pending'; statuses[downstream] = 'pending';
changedSteps.push(downstream); changedSteps.push(downstream);
@@ -4307,7 +4318,7 @@ async function ensureManualInteractionAllowed(actionLabel) {
async function skipStep(step) { async function skipStep(step) {
const state = await ensureManualInteractionAllowed('跳过步骤'); const state = await ensureManualInteractionAllowed('跳过步骤');
if (!Number.isInteger(step) || step < 1 || step > 9) { if (!Number.isInteger(step) || !STEP_IDS.includes(step)) {
throw new Error(`无效步骤:${step}`); throw new Error(`无效步骤:${step}`);
} }
@@ -4367,7 +4378,7 @@ async function clickWithDebugger(tabId, rect) {
throw new Error('未找到用于调试点击的认证页面标签页。'); throw new Error('未找到用于调试点击的认证页面标签页。');
} }
if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) { if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) {
throw new Error('步骤 8 的调试器兜底点击需要有效的按钮坐标。'); throw new Error('步骤 9 的调试器兜底点击需要有效的按钮坐标。');
} }
const target = { tabId }; const target = { tabId };
@@ -4375,7 +4386,7 @@ async function clickWithDebugger(tabId, rect) {
await chrome.debugger.attach(target, '1.3'); await chrome.debugger.attach(target, '1.3');
} catch (err) { } catch (err) {
throw new Error( throw new Error(
`步骤 8 的调试器兜底点击附加失败:${err.message}` + `步骤 9 的调试器兜底点击附加失败:${err.message}` +
'如果认证页标签已打开 DevTools,请先关闭后重试。' '如果认证页标签已打开 DevTools,请先关闭后重试。'
); );
} }
@@ -4500,7 +4511,7 @@ async function handleStepData(step, payload) {
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
} }
break; break;
case 6: case 7:
if (payload.loginVerificationRequestedAt) { if (payload.loginVerificationRequestedAt) {
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
} }
@@ -4511,22 +4522,22 @@ async function handleStepData(step, payload) {
signupVerificationRequestedAt: null, signupVerificationRequestedAt: null,
}); });
break; break;
case 7: case 8:
await setState({ await setState({
lastEmailTimestamp: payload.emailTimestamp || null, lastEmailTimestamp: payload.emailTimestamp || null,
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
}); });
break; break;
case 8: case 9:
if (payload.localhostUrl) { if (payload.localhostUrl) {
if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) { if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) {
throw new Error('步骤 8 返回了无效的 localhost OAuth 回调地址。'); throw new Error('步骤 9 返回了无效的 localhost OAuth 回调地址。');
} }
await setState({ localhostUrl: payload.localhostUrl }); await setState({ localhostUrl: payload.localhostUrl });
broadcastDataUpdate({ localhostUrl: payload.localhostUrl }); broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
} }
break; break;
case 9: { case 10: {
if (payload.localhostUrl) { if (payload.localhostUrl) {
await closeLocalhostCallbackTabs(payload.localhostUrl); await closeLocalhostCallbackTabs(payload.localhostUrl);
} }
@@ -4571,8 +4582,8 @@ async function handleStepData(step, payload) {
const stepWaiters = new Map(); const stepWaiters = new Map();
let resumeWaiter = null; let resumeWaiter = null;
const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000; const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000;
const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8]); const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8, 9]);
const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 9]); const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 10]);
function waitForStepComplete(step, timeoutMs = 120000) { function waitForStepComplete(step, timeoutMs = 120000) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -4618,11 +4629,11 @@ async function completeStepFromBackground(step, payload = {}) {
return; return;
} }
const completionState = step === 9 ? await getState() : null; const completionState = step === LAST_STEP_ID ? await getState() : null;
await setStepStatus(step, 'completed'); await setStepStatus(step, 'completed');
await addLog(`步骤 ${step} 已完成`, 'ok'); await addLog(`步骤 ${step} 已完成`, 'ok');
await handleStepData(step, payload); await handleStepData(step, payload);
if (step === 9) { if (step === LAST_STEP_ID) {
await appendAndBroadcastAccountRunRecord('success', completionState); await appendAndBroadcastAccountRunRecord('success', completionState);
} }
notifyStepComplete(step, payload); notifyStepComplete(step, payload);
@@ -5274,7 +5285,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;
let postStep6RestartCount = 0; let postStep7RestartCount = 0;
if (continued) { if (continued) {
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info'); await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info');
@@ -5315,12 +5326,12 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
} }
let step = Math.max(startStep, 4); let step = Math.max(startStep, 4);
while (step <= 9) { while (step <= LAST_STEP_ID) {
try { try {
await executeStepAndWait(step, AUTO_STEP_DELAYS[step]); await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
const latestState = await getState(); const latestState = await getState();
if (step === 6 && shouldSkipLoginVerificationForCpaCallback(latestState)) { if (step === FINAL_OAUTH_CHAIN_START_STEP && shouldSkipLoginVerificationForCpaCallback(latestState)) {
step = 8; step = 9;
continue; continue;
} }
step += 1; step += 1;
@@ -5331,7 +5342,7 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
const restartDecision = await getPostStep6AutoRestartDecision(step, err); const restartDecision = await getPostStep6AutoRestartDecision(step, err);
if (restartDecision.shouldRestart) { if (restartDecision.shouldRestart) {
postStep6RestartCount += 1; postStep7RestartCount += 1;
const authState = restartDecision.authState; const authState = restartDecision.authState;
const authStateLabel = authState?.state ? getLoginAuthStateLabel(authState.state) : '未知页面'; const authStateLabel = authState?.state ? getLoginAuthStateLabel(authState.state) : '未知页面';
const authStateSuffix = authState?.url const authStateSuffix = authState?.url
@@ -5340,19 +5351,19 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
? `当前认证页:${authStateLabel}` ? `当前认证页:${authStateLabel}`
: '未获取到认证页状态'; : '未获取到认证页状态';
await addLog( await addLog(
`步骤 ${step}:检测到报错且当前未进入 add-phone,正在回到步骤 6 重新开始授权流程(第 ${postStep6RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`, `步骤 ${step}:检测到报错且当前未进入 add-phone,正在回到步骤 7 重新开始授权流程(第 ${postStep7RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`,
'warn' 'warn'
); );
await invalidateDownstreamAfterStepRestart(5, { await invalidateDownstreamAfterStepRestart(6, {
logLabel: `步骤 ${step} 报错后准备回到步骤 6 重试(第 ${postStep6RestartCount} 次重开)`, logLabel: `步骤 ${step} 报错后准备回到步骤 7 重试(第 ${postStep7RestartCount} 次重开)`,
}); });
step = 6; step = 7;
continue; continue;
} }
if (restartDecision.blockedByAddPhone) { if (restartDecision.blockedByAddPhone) {
const addPhoneUrl = restartDecision.authState?.url || 'https://auth.openai.com/add-phone'; const addPhoneUrl = restartDecision.authState?.url || 'https://auth.openai.com/add-phone';
await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 6 重开。`, 'warn'); await addLog(`步骤 ${step}:检测到认证流程进入 add-phone(${addPhoneUrl}),停止自动回到步骤 7 重开。`, 'warn');
} }
throw err; throw err;
} }
@@ -5586,6 +5597,10 @@ const step5Executor = self.MultiPageBackgroundStep5?.createStep5Executor({
sendToContentScript, sendToContentScript,
}); });
const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({ const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
completeStepFromBackground,
runPreStep6CookieCleanup,
});
const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
addLog, addLog,
completeStepFromBackground, completeStepFromBackground,
getErrorMessage, getErrorMessage,
@@ -5595,20 +5610,19 @@ const step6Executor = self.MultiPageBackgroundStep6?.createStep6Executor({
isStep6SuccessResult, isStep6SuccessResult,
refreshOAuthUrlBeforeStep6, refreshOAuthUrlBeforeStep6,
reuseOrCreateTab, reuseOrCreateTab,
runPreStep6CookieCleanup,
sendToContentScriptResilient, sendToContentScriptResilient,
shouldSkipLoginVerificationForCpaCallback, shouldSkipLoginVerificationForCpaCallback,
skipLoginVerificationStepsForCpaCallback, skipLoginVerificationStepsForCpaCallback,
STEP6_MAX_ATTEMPTS, STEP6_MAX_ATTEMPTS,
throwIfStopped, throwIfStopped,
}); });
const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
addLog, addLog,
chrome, chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER,
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
ensureStep7VerificationPageReady, ensureStep8VerificationPageReady,
executeStep6: (...args) => executeStep6(...args), executeStep7: (...args) => executeStep7(...args),
getPanelMode, getPanelMode,
getMailConfig, getMailConfig,
getState, getState,
@@ -5628,7 +5642,7 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS, STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
throwIfStopped, throwIfStopped,
}); });
const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({ const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({
addLog, addLog,
chrome, chrome,
closeConflictingTabsForSource, closeConflictingTabsForSource,
@@ -5646,17 +5660,18 @@ const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
shouldBypassStep9ForLocalCpa, shouldBypassStep9ForLocalCpa,
SUB2API_STEP9_RESPONSE_TIMEOUT_MS, SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
}); });
const stepDefinitions = self.MultiPageStepDefinitions?.getSteps?.() || []; const stepDefinitions = SHARED_STEP_DEFINITIONS;
const stepExecutorsByKey = { const stepExecutorsByKey = {
'open-chatgpt': () => step1Executor.executeStep1(), 'open-chatgpt': () => step1Executor.executeStep1(),
'submit-signup-email': (state) => step2Executor.executeStep2(state), 'submit-signup-email': (state) => step2Executor.executeStep2(state),
'fill-password': (state) => step3Executor.executeStep3(state), 'fill-password': (state) => step3Executor.executeStep3(state),
'fetch-signup-code': (state) => step4Executor.executeStep4(state), 'fetch-signup-code': (state) => step4Executor.executeStep4(state),
'fill-profile': (state) => step5Executor.executeStep5(state), 'fill-profile': (state) => step5Executor.executeStep5(state),
'oauth-login': (state) => step6Executor.executeStep6(state), 'clear-login-cookies': () => step6Executor.executeStep6(),
'fetch-login-code': (state) => step7Executor.executeStep7(state), 'oauth-login': (state) => step7Executor.executeStep7(state),
'confirm-oauth': (state) => step8Executor.executeStep8(state), 'fetch-login-code': (state) => step8Executor.executeStep8(state),
'platform-verify': (state) => step9Executor.executeStep9(state), 'confirm-oauth': (state) => step9Executor.executeStep9(state),
'platform-verify': (state) => step10Executor.executeStep10(state),
}; };
const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter({
addLog, addLog,
@@ -6064,13 +6079,13 @@ async function runPreStep6CookieCleanup() {
} }
// ============================================================ // ============================================================
// Step 6: Login and ensure the auth page reaches the login verification page // Step 7: Login and ensure the auth page reaches the login verification page
// ============================================================ // ============================================================
async function refreshOAuthUrlBeforeStep6(state) { async function refreshOAuthUrlBeforeStep6(state) {
await addLog(`步骤 6:正在刷新登录用的 ${getPanelModeLabel(state)} OAuth 链接...`); await addLog(`步骤 7:正在刷新登录用的 ${getPanelModeLabel(state)} OAuth 链接...`);
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel'); console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel');
const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 6' }); const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 7' });
await handleStepData(1, refreshResult); await handleStepData(1, refreshResult);
if (!refreshResult?.oauthUrl) { if (!refreshResult?.oauthUrl) {
@@ -6101,7 +6116,7 @@ function isAddPhoneAuthState(authState = {}) {
async function getPostStep6AutoRestartDecision(step, error) { async function getPostStep6AutoRestartDecision(step, error) {
const normalizedStep = Number(step); const normalizedStep = Number(step);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
if (!Number.isFinite(normalizedStep) || normalizedStep < 6 || normalizedStep > 9) { if (!Number.isFinite(normalizedStep) || normalizedStep < 7 || normalizedStep > LAST_STEP_ID) {
return { return {
shouldRestart: false, shouldRestart: false,
blockedByAddPhone: false, blockedByAddPhone: false,
@@ -6122,7 +6137,7 @@ async function getPostStep6AutoRestartDecision(step, error) {
let authState = null; let authState = null;
try { try {
authState = await getLoginAuthStateFromContent({ authState = await getLoginAuthStateFromContent({
logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 6 重开...`, logMessage: `步骤 ${normalizedStep}:正在确认当前认证页状态,以决定是否回到步骤 7 重开...`,
}); });
} catch (inspectError) { } catch (inspectError) {
console.warn(LOG_PREFIX, '[AutoRun] failed to inspect login auth state after post-step6 error', { console.warn(LOG_PREFIX, '[AutoRun] failed to inspect login auth state after post-step6 error', {
@@ -6150,7 +6165,7 @@ async function getPostStep6AutoRestartDecision(step, error) {
} }
async function getLoginAuthStateFromContent(options = {}) { async function getLoginAuthStateFromContent(options = {}) {
const { logMessage = '步骤 7:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...' } = options; const { logMessage = '步骤 8:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...' } = options;
const result = await sendToContentScriptResilient( const result = await sendToContentScriptResilient(
'signup-page', 'signup-page',
{ {
@@ -6172,7 +6187,7 @@ async function getLoginAuthStateFromContent(options = {}) {
return result || {}; return result || {};
} }
async function ensureStep7VerificationPageReady() { async function ensureStep8VerificationPageReady() {
const pageState = await getLoginAuthStateFromContent(); const pageState = await getLoginAuthStateFromContent();
if (pageState.state === 'verification_page') { if (pageState.state === 'verification_page') {
return pageState; return pageState;
@@ -6180,7 +6195,7 @@ async function ensureStep7VerificationPageReady() {
const stateLabel = getLoginAuthStateLabel(pageState.state); const stateLabel = getLoginAuthStateLabel(pageState.state);
const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; const urlPart = pageState.url ? ` URL: ${pageState.url}` : '';
throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 6。当前状态:${stateLabel}.${urlPart}`.trim()); throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:${stateLabel}.${urlPart}`.trim());
} }
async function skipLoginVerificationStepsForCpaCallback() { async function skipLoginVerificationStepsForCpaCallback() {
@@ -6188,21 +6203,21 @@ async function skipLoginVerificationStepsForCpaCallback() {
lastLoginCode: null, lastLoginCode: null,
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
}); });
await setStepStatus(6, 'skipped'); await setStepStatus(7, 'skipped');
await addLog('步骤 6:当前已选择“第步回调”,直接跳过步骤 6、7。', 'warn'); await addLog('步骤 7:当前已选择“第步回调”,直接跳过步骤 7、8。', 'warn');
const latestState = await getState(); const latestState = await getState();
if (!isStepDoneStatus(latestState.stepStatuses?.[7])) { if (!isStepDoneStatus(latestState.stepStatuses?.[8])) {
await setStepStatus(7, 'skipped'); await setStepStatus(8, 'skipped');
await addLog('步骤 7:当前已选择“第步回调”,本轮无需获取登录验证码。', 'warn'); await addLog('步骤 8:当前已选择“第步回调”,本轮无需获取登录验证码。', 'warn');
} }
} }
async function executeStep6(state, options = {}) { async function executeStep6() {
return step6Executor.executeStep6(state, options); return step6Executor.executeStep6();
} }
// ============================================================ // ============================================================
// Step 7: Poll login verification mail and submit the login code // Step 7: Refresh OAuth and log in
// ============================================================ // ============================================================
async function executeStep7(state) { async function executeStep7(state) {
@@ -6210,7 +6225,15 @@ async function executeStep7(state) {
} }
// ============================================================ // ============================================================
// Step 8: 完成 OAuth(自动点击 + localhost 回调监听) // Step 8: Poll login verification mail and submit the login code
// ============================================================
async function executeStep8(state) {
return step8Executor.executeStep8(state);
}
// ============================================================
// Step 9: 完成 OAuth(自动点击 + localhost 回调监听)
// ============================================================ // ============================================================
let webNavListener = null; let webNavListener = null;
@@ -6323,12 +6346,12 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS)
throwIfStopped(); throwIfStopped();
const pageState = await getStep8PageState(tabId); const pageState = await getStep8PageState(tabId);
if (pageState?.addPhonePage) { if (pageState?.addPhonePage) {
throw new Error('步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。'); throw new Error('步骤 9:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
} }
if (pageState?.retryPage) { if (pageState?.retryPage) {
await recoverAuthRetryPageOnTab(tabId, { await recoverAuthRetryPageOnTab(tabId, {
flow: 'auth', flow: 'auth',
logLabel: '步骤 8:检测到认证页重试页,正在点击“重试”恢复', logLabel: '步骤 9:检测到认证页重试页,正在点击“重试”恢复',
step: 8, step: 8,
timeoutMs: Math.max(5000, Math.min(12000, timeoutMs)), timeoutMs: Math.max(5000, Math.min(12000, timeoutMs)),
}); });
@@ -6338,7 +6361,7 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS)
} }
if (pageState?.consentReady) { if (pageState?.consentReady) {
if (retryRecovered) { if (retryRecovered) {
await addLog('步骤 8:认证页重试页已恢复,准备重新定位“继续”按钮...', 'info'); await addLog('步骤 9:认证页重试页已恢复,准备重新定位“继续”按钮...', 'info');
} }
return pageState; return pageState;
} }
@@ -6346,7 +6369,7 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS)
recovered = true; recovered = true;
await ensureStep8SignupPageReady(tabId, { await ensureStep8SignupPageReady(tabId, {
timeoutMs: Math.min(10000, timeoutMs), timeoutMs: Math.min(10000, timeoutMs),
logMessage: '步骤 8:认证页内容脚本已失联,正在等待页面重新就绪...', logMessage: '步骤 9:认证页内容脚本已失联,正在等待页面重新就绪...',
}); });
continue; continue;
} }
@@ -6354,13 +6377,13 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS)
await sleepWithStop(250); await sleepWithStop(250);
} }
throw new Error('步骤 8:长时间未进入 OAuth 同意页,无法定位“继续”按钮。'); throw new Error('步骤 9:长时间未进入 OAuth 同意页,无法定位“继续”按钮。');
} }
async function prepareStep8DebuggerClick(tabId) { async function prepareStep8DebuggerClick(tabId) {
await ensureStep8SignupPageReady(tabId, { await ensureStep8SignupPageReady(tabId, {
timeoutMs: 15000, timeoutMs: 15000,
logMessage: '步骤 8:认证页内容脚本已失联,正在恢复后继续定位按钮...', logMessage: '步骤 9:认证页内容脚本已失联,正在恢复后继续定位按钮...',
}); });
const result = await sendToContentScriptResilient('signup-page', { const result = await sendToContentScriptResilient('signup-page', {
type: 'STEP8_FIND_AND_CLICK', type: 'STEP8_FIND_AND_CLICK',
@@ -6369,7 +6392,7 @@ async function prepareStep8DebuggerClick(tabId) {
}, { }, {
timeoutMs: 15000, timeoutMs: 15000,
retryDelayMs: 600, retryDelayMs: 600,
logMessage: '步骤 8:认证页正在切换,等待 OAuth 同意页按钮重新就绪...', logMessage: '步骤 9:认证页正在切换,等待 OAuth 同意页按钮重新就绪...',
}); });
if (result?.error) { if (result?.error) {
@@ -6382,7 +6405,7 @@ async function prepareStep8DebuggerClick(tabId) {
async function triggerStep8ContentStrategy(tabId, strategy) { async function triggerStep8ContentStrategy(tabId, strategy) {
await ensureStep8SignupPageReady(tabId, { await ensureStep8SignupPageReady(tabId, {
timeoutMs: 15000, timeoutMs: 15000,
logMessage: '步骤 8:认证页内容脚本已失联,正在恢复后继续点击“继续”按钮...', logMessage: '步骤 9:认证页内容脚本已失联,正在恢复后继续点击“继续”按钮...',
}); });
const result = await sendToContentScriptResilient('signup-page', { const result = await sendToContentScriptResilient('signup-page', {
type: 'STEP8_TRIGGER_CONTINUE', type: 'STEP8_TRIGGER_CONTINUE',
@@ -6395,7 +6418,7 @@ async function triggerStep8ContentStrategy(tabId, strategy) {
}, { }, {
timeoutMs: 15000, timeoutMs: 15000,
retryDelayMs: 600, retryDelayMs: 600,
logMessage: '步骤 8:认证页正在切换,等待“继续”按钮重新就绪...', logMessage: '步骤 9:认证页正在切换,等待“继续”按钮重新就绪...',
}); });
if (result?.error) { if (result?.error) {
@@ -6409,7 +6432,7 @@ async function recoverAuthRetryPageOnTab(tabId, payload = {}, options = {}) {
await ensureStep8SignupPageReady(tabId, { await ensureStep8SignupPageReady(tabId, {
timeoutMs: options.readyTimeoutMs ?? 15000, timeoutMs: options.readyTimeoutMs ?? 15000,
retryDelayMs: options.retryDelayMs ?? 600, retryDelayMs: options.retryDelayMs ?? 600,
logMessage: options.readyLogMessage || '步骤 8:认证页内容脚本已失联,正在恢复后继续处理重试页...', logMessage: options.readyLogMessage || '步骤 9:认证页内容脚本已失联,正在恢复后继续处理重试页...',
}); });
const result = await sendToContentScriptResilient('signup-page', { const result = await sendToContentScriptResilient('signup-page', {
type: 'RECOVER_AUTH_RETRY_PAGE', type: 'RECOVER_AUTH_RETRY_PAGE',
@@ -6418,7 +6441,7 @@ async function recoverAuthRetryPageOnTab(tabId, payload = {}, options = {}) {
}, { }, {
timeoutMs: options.timeoutMs ?? 15000, timeoutMs: options.timeoutMs ?? 15000,
retryDelayMs: options.retryDelayMs ?? 600, retryDelayMs: options.retryDelayMs ?? 600,
logMessage: options.logMessage || '步骤 8:认证页正在切换,等待“重试”按钮重新就绪...', logMessage: options.logMessage || '步骤 9:认证页正在切换,等待“重试”按钮重新就绪...',
}); });
if (result?.error) { if (result?.error) {
@@ -6430,7 +6453,7 @@ async function recoverAuthRetryPageOnTab(tabId, payload = {}, options = {}) {
async function reloadStep8ConsentPage(tabId, timeoutMs = 30000) { async function reloadStep8ConsentPage(tabId, timeoutMs = 30000) {
if (!Number.isInteger(tabId)) { if (!Number.isInteger(tabId)) {
throw new Error('步骤 8:缺少有效的认证页标签页,无法刷新后重试。'); throw new Error('步骤 9:缺少有效的认证页标签页,无法刷新后重试。');
} }
await chrome.tabs.update(tabId, { active: true }).catch(() => { }); await chrome.tabs.update(tabId, { active: true }).catch(() => { });
@@ -6441,7 +6464,7 @@ async function reloadStep8ConsentPage(tabId, timeoutMs = 30000) {
if (settled) return; if (settled) return;
settled = true; settled = true;
chrome.tabs.onUpdated.removeListener(listener); chrome.tabs.onUpdated.removeListener(listener);
reject(new Error('步骤 8:刷新认证页后等待页面完成加载超时。')); reject(new Error('步骤 9:刷新认证页后等待页面完成加载超时。'));
}, timeoutMs); }, timeoutMs);
const listener = (updatedTabId, changeInfo) => { const listener = (updatedTabId, changeInfo) => {
@@ -6466,7 +6489,7 @@ async function reloadStep8ConsentPage(tabId, timeoutMs = 30000) {
await ensureStep8SignupPageReady(tabId, { await ensureStep8SignupPageReady(tabId, {
timeoutMs: Math.min(15000, timeoutMs), timeoutMs: Math.min(15000, timeoutMs),
logMessage: '步骤 8:认证页刷新后内容脚本尚未就绪,正在等待页面恢复...', logMessage: '步骤 9:认证页刷新后内容脚本尚未就绪,正在等待页面恢复...',
}); });
} }
@@ -6479,7 +6502,7 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI
const tab = await chrome.tabs.get(tabId).catch(() => null); const tab = await chrome.tabs.get(tabId).catch(() => null);
if (!tab) { if (!tab) {
throw new Error('步骤 8:认证页面标签页已关闭,无法继续自动授权。'); throw new Error('步骤 9:认证页面标签页已关闭,无法继续自动授权。');
} }
if (baselineUrl && typeof tab.url === 'string' && tab.url !== baselineUrl) { if (baselineUrl && typeof tab.url === 'string' && tab.url !== baselineUrl) {
@@ -6488,12 +6511,12 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI
const pageState = await getStep8PageState(tabId); const pageState = await getStep8PageState(tabId);
if (pageState?.addPhonePage) { if (pageState?.addPhonePage) {
throw new Error('步骤 8:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。'); throw new Error('步骤 9:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。');
} }
if (pageState?.retryPage) { if (pageState?.retryPage) {
await recoverAuthRetryPageOnTab(tabId, { await recoverAuthRetryPageOnTab(tabId, {
flow: 'auth', flow: 'auth',
logLabel: '步骤 8:点击“继续”后进入重试页,正在点击“重试”恢复', logLabel: '步骤 9:点击“继续”后进入重试页,正在点击“重试”恢复',
step: 8, step: 8,
timeoutMs: Math.max(5000, Math.min(12000, timeoutMs)), timeoutMs: Math.max(5000, Math.min(12000, timeoutMs)),
}); });
@@ -6509,7 +6532,7 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI
recovered = true; recovered = true;
await ensureStep8SignupPageReady(tabId, { await ensureStep8SignupPageReady(tabId, {
timeoutMs: Math.max(3000, Math.min(8000, timeoutMs)), timeoutMs: Math.max(3000, Math.min(8000, timeoutMs)),
logMessage: '步骤 8:点击后认证页正在重载,正在等待内容脚本重新就绪...', logMessage: '步骤 9:点击后认证页正在重载,正在等待内容脚本重新就绪...',
}).catch(() => null); }).catch(() => null);
continue; continue;
} }
@@ -6547,7 +6570,7 @@ function getStep8EffectLabel(effect) {
} }
} }
const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
addLog, addLog,
chrome, chrome,
cleanupStep8NavigationListeners, cleanupStep8NavigationListeners,
@@ -6580,16 +6603,16 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
waitForStep8Ready, waitForStep8Ready,
}); });
async function executeStep8(state) { async function executeStep9(state) {
return step8Executor.executeStep8(state); return step9Executor.executeStep9(state);
} }
// ============================================================ // ============================================================
// Step 9: 平台回调验证 // Step 10: 平台回调验证
// ============================================================ // ============================================================
async function executeStep9(state) { async function executeStep10(state) {
return step9Executor.executeStep9(state); return step10Executor.executeStep10(state);
} }
// ============================================================ // ============================================================
+5 -1
View File
@@ -102,7 +102,11 @@
} }
function getFirstUnfinishedStep(statuses = {}) { function getFirstUnfinishedStep(statuses = {}) {
for (let step = 1; step <= 9; step++) { const stepIds = Object.keys(DEFAULT_STATE.stepStatuses || {})
.map((step) => Number(step))
.filter(Number.isFinite)
.sort((left, right) => left - right);
for (const step of stepIds) {
if (!isStepDoneStatus(statuses[step] || 'pending')) { if (!isStepDoneStatus(statuses[step] || 'pending')) {
return step; return step;
} }
+7 -7
View File
@@ -132,7 +132,7 @@
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
} }
break; break;
case 6: case 7:
if (payload.loginVerificationRequestedAt) { if (payload.loginVerificationRequestedAt) {
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
} }
@@ -143,22 +143,22 @@
signupVerificationRequestedAt: null, signupVerificationRequestedAt: null,
}); });
break; break;
case 7: case 8:
await setState({ await setState({
lastEmailTimestamp: payload.emailTimestamp || null, lastEmailTimestamp: payload.emailTimestamp || null,
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
}); });
break; break;
case 8: case 9:
if (payload.localhostUrl) { if (payload.localhostUrl) {
if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) { if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) {
throw new Error('步骤 8 返回了无效的 localhost OAuth 回调地址。'); throw new Error('步骤 9 返回了无效的 localhost OAuth 回调地址。');
} }
await setState({ localhostUrl: payload.localhostUrl }); await setState({ localhostUrl: payload.localhostUrl });
broadcastDataUpdate({ localhostUrl: payload.localhostUrl }); broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
} }
break; break;
case 9: { case 10: {
if (payload.localhostUrl) { if (payload.localhostUrl) {
await closeLocalhostCallbackTabs(payload.localhostUrl); await closeLocalhostCallbackTabs(payload.localhostUrl);
} }
@@ -232,11 +232,11 @@
return { ok: true, error: errorMessage }; return { ok: true, error: errorMessage };
} }
const completionState = message.step === 9 ? await getState() : null; const completionState = message.step === 10 ? await getState() : null;
await setStepStatus(message.step, 'completed'); await setStepStatus(message.step, 'completed');
await addLog(`步骤 ${message.step} 已完成`, 'ok'); await addLog(`步骤 ${message.step} 已完成`, 'ok');
await handleStepData(message.step, message.payload); await handleStepData(message.step, message.payload);
if (message.step === 9 && typeof appendAccountRunRecord === 'function') { if (message.step === 10 && typeof appendAccountRunRecord === 'function') {
await appendAccountRunRecord('success', completionState); await appendAccountRunRecord('success', completionState);
} }
notifyStepComplete(message.step, message.payload); notifyStepComplete(message.step, message.payload);
+1 -1
View File
@@ -92,7 +92,7 @@
function shouldSkipLoginVerificationForCpaCallback(state) { function shouldSkipLoginVerificationForCpaCallback(state) {
return getPanelMode(state) === 'cpa' return getPanelMode(state) === 'cpa'
&& normalizeCpaCallbackMode(state?.cpaCallbackMode) === 'step6'; && normalizeCpaCallbackMode(state?.cpaCallbackMode) === 'step7';
} }
function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) { function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
+19
View File
@@ -0,0 +1,19 @@
(function attachBackgroundStep6(root, factory) {
root.MultiPageBackgroundStep6 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep6Module() {
function createStep6Executor(deps = {}) {
const {
completeStepFromBackground,
runPreStep6CookieCleanup,
} = deps;
async function executeStep6() {
await runPreStep6CookieCleanup();
await completeStepFromBackground(6);
}
return { executeStep6 };
}
return { createStep6Executor };
});
+27 -27
View File
@@ -1,7 +1,7 @@
(function attachBackgroundStep8(root, factory) { (function attachBackgroundStep9(root, factory) {
root.MultiPageBackgroundStep8 = factory(); root.MultiPageBackgroundStep9 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() { })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep9Module() {
function createStep8Executor(deps = {}) { function createStep9Executor(deps = {}) {
const { const {
addLog, addLog,
chrome, chrome,
@@ -32,12 +32,12 @@
setStep8TabUpdatedListener, setStep8TabUpdatedListener,
} = deps; } = deps;
async function executeStep8(state) { async function executeStep9(state) {
if (!state.oauthUrl) { if (!state.oauthUrl) {
throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。'); throw new Error('缺少登录用 OAuth 链接,请先完成步骤 7。');
} }
await addLog('步骤 8:正在监听 localhost 回调地址...'); await addLog('步骤 9:正在监听 localhost 回调地址...');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let resolved = false; let resolved = false;
@@ -48,7 +48,7 @@
setStep8PendingReject(null); setStep8PendingReject(null);
}; };
const rejectStep8 = (error) => { const rejectStep9 = (error) => {
if (resolved) return; if (resolved) return;
resolved = true; resolved = true;
clearTimeout(timeout); clearTimeout(timeout);
@@ -56,15 +56,15 @@
reject(error); reject(error);
}; };
const finalizeStep8Callback = (callbackUrl) => { const finalizeStep9Callback = (callbackUrl) => {
if (resolved || !callbackUrl) return; if (resolved || !callbackUrl) return;
resolved = true; resolved = true;
cleanupListener(); cleanupListener();
clearTimeout(timeout); clearTimeout(timeout);
addLog(`步骤 8:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => { addLog(`步骤 9:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => {
return completeStepFromBackground(8, { localhostUrl: callbackUrl }); return completeStepFromBackground(9, { localhostUrl: callbackUrl });
}).then(() => { }).then(() => {
resolve(); resolve();
}).catch((err) => { }).catch((err) => {
@@ -73,26 +73,26 @@
}; };
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
rejectStep8(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 的点击可能被拦截了。')); rejectStep9(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 9 的点击可能被拦截了。'));
}, 120000); }, 120000);
setStep8PendingReject((error) => { setStep8PendingReject((error) => {
rejectStep8(error); rejectStep9(error);
}); });
setWebNavListener((details) => { setWebNavListener((details) => {
const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId); const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId);
finalizeStep8Callback(callbackUrl); finalizeStep9Callback(callbackUrl);
}); });
setWebNavCommittedListener((details) => { setWebNavCommittedListener((details) => {
const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId); const callbackUrl = getStep8CallbackUrlFromNavigation(details, signupTabId);
finalizeStep8Callback(callbackUrl); finalizeStep9Callback(callbackUrl);
}); });
setStep8TabUpdatedListener((tabId, changeInfo, tab) => { setStep8TabUpdatedListener((tabId, changeInfo, tab) => {
const callbackUrl = getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId); const callbackUrl = getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId);
finalizeStep8Callback(callbackUrl); finalizeStep9Callback(callbackUrl);
}); });
(async () => { (async () => {
@@ -103,10 +103,10 @@
if (signupTabId && await isTabAlive('signup-page')) { if (signupTabId && await isTabAlive('signup-page')) {
await chrome.tabs.update(signupTabId, { active: true }); await chrome.tabs.update(signupTabId, { active: true });
await addLog('步骤 8:已切回认证页,正在准备调试器点击...'); await addLog('步骤 9:已切回认证页,正在准备调试器点击...');
} else { } else {
signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl); signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...'); await addLog('步骤 9:已重新打开认证页,正在准备调试器点击...');
} }
throwIfStep8SettledOrStopped(resolved); throwIfStep8SettledOrStopped(resolved);
@@ -115,7 +115,7 @@
chrome.tabs.onUpdated.addListener(deps.getStep8TabUpdatedListener()); chrome.tabs.onUpdated.addListener(deps.getStep8TabUpdatedListener());
await ensureStep8SignupPageReady(signupTabId, { await ensureStep8SignupPageReady(signupTabId, {
timeoutMs: 15000, timeoutMs: 15000,
logMessage: '步骤 8:认证页内容脚本尚未就绪,正在等待页面恢复...', logMessage: '步骤 9:认证页内容脚本尚未就绪,正在等待页面恢复...',
}); });
for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) { for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) {
@@ -128,7 +128,7 @@
const strategy = STEP8_STRATEGIES[Math.min(round - 1, STEP8_STRATEGIES.length - 1)]; const strategy = STEP8_STRATEGIES[Math.min(round - 1, STEP8_STRATEGIES.length - 1)];
await addLog(`步骤 8:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label}...`); await addLog(`步骤 9:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label}...`);
if (strategy.mode === 'debugger') { if (strategy.mode === 'debugger') {
const clickTarget = await prepareStep8DebuggerClick(signupTabId); const clickTarget = await prepareStep8DebuggerClick(signupTabId);
@@ -148,33 +148,33 @@
} }
if (effect.progressed) { if (effect.progressed) {
await addLog(`步骤 8:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info'); await addLog(`步骤 9:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info');
break; break;
} }
if (effect.restartCurrentStep) { if (effect.restartCurrentStep) {
await addLog(`步骤 8${getStep8EffectLabel(effect)},准备重新定位“继续”按钮并重试...`, 'warn'); await addLog(`步骤 9${getStep8EffectLabel(effect)},准备重新定位“继续”按钮并重试...`, 'warn');
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
continue; continue;
} }
if (round >= STEP8_MAX_ROUNDS) { if (round >= STEP8_MAX_ROUNDS) {
throw new Error(`步骤 8:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`); throw new Error(`步骤 9:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
} }
await addLog(`步骤 8${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS}...`, 'warn'); await addLog(`步骤 9${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS}...`, 'warn');
await reloadStep8ConsentPage(signupTabId); await reloadStep8ConsentPage(signupTabId);
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
} }
} catch (err) { } catch (err) {
rejectStep8(err); rejectStep9(err);
} }
})(); })();
}); });
} }
return { executeStep8 }; return { executeStep9 };
} }
return { createStep8Executor }; return { createStep9Executor };
}); });
+35 -37
View File
@@ -1,14 +1,14 @@
(function attachBackgroundStep7(root, factory) { (function attachBackgroundStep8(root, factory) {
root.MultiPageBackgroundStep7 = factory(); root.MultiPageBackgroundStep8 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep7Module() { })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() {
function createStep7Executor(deps = {}) { function createStep8Executor(deps = {}) {
const { const {
addLog, addLog,
chrome, chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER,
confirmCustomVerificationStepBypass, confirmCustomVerificationStepBypass,
ensureStep7VerificationPageReady, ensureStep8VerificationPageReady,
executeStep6, executeStep7,
getPanelMode, getPanelMode,
getMailConfig, getMailConfig,
getState, getState,
@@ -29,7 +29,7 @@
throwIfStopped, throwIfStopped,
} = deps; } = deps;
async function runStep7Attempt(state) { async function runStep8Attempt(state) {
const mail = getMailConfig(state); const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error); if (mail.error) throw new Error(mail.error);
const stepStartedAt = Date.now(); const stepStartedAt = Date.now();
@@ -39,25 +39,25 @@
await chrome.tabs.update(authTabId, { active: true }); await chrome.tabs.update(authTabId, { active: true });
} else { } else {
if (!state.oauthUrl) { if (!state.oauthUrl) {
throw new Error('缺少登录用 OAuth 链接,请先完成步骤 6。'); throw new Error('缺少登录用 OAuth 链接,请先完成步骤 7。');
} }
await reuseOrCreateTab('signup-page', state.oauthUrl); await reuseOrCreateTab('signup-page', state.oauthUrl);
} }
throwIfStopped(); throwIfStopped();
await ensureStep7VerificationPageReady(); await ensureStep8VerificationPageReady();
await addLog('步骤 7:登录验证码页面已就绪,开始获取验证码。', 'info'); await addLog('步骤 8:登录验证码页面已就绪,开始获取验证码。', 'info');
if (shouldUseCustomRegistrationEmail(state)) { if (shouldUseCustomRegistrationEmail(state)) {
await confirmCustomVerificationStepBypass(7); await confirmCustomVerificationStepBypass(8);
return; return;
} }
throwIfStopped(); throwIfStopped();
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`); await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`);
} else { } else {
await addLog(`步骤 7:正在打开${mail.label}...`); await addLog(`步骤 8:正在打开${mail.label}...`);
const alive = await isTabAlive(mail.source); const alive = await isTabAlive(mail.source);
if (alive) { if (alive) {
@@ -79,54 +79,52 @@
} }
const shouldRefreshOAuthBeforeSubmit = getPanelMode(state) === 'cpa'; const shouldRefreshOAuthBeforeSubmit = getPanelMode(state) === 'cpa';
let step6ReplayCompleted = false; let step7ReplayCompleted = false;
await resolveVerificationStep(7, state, mail, { await resolveVerificationStep(8, state, mail, {
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000), filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000),
requestFreshCodeFirst: false, requestFreshCodeFirst: false,
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0 ? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
beforeSubmit: shouldRefreshOAuthBeforeSubmit ? async (result) => { beforeSubmit: shouldRefreshOAuthBeforeSubmit ? async (result) => {
if (step6ReplayCompleted) { if (step7ReplayCompleted) {
return; return;
} }
step6ReplayCompleted = true; step7ReplayCompleted = true;
await addLog(`步骤 7:已拿到登录验证码 ${result.code},先刷新 CPA OAuth 链接并重走步骤 6,再回填验证码。`, 'warn'); await addLog(`步骤 8:已拿到登录验证码 ${result.code},先刷新 CPA OAuth 链接并重走步骤 7,再回填验证码。`, 'warn');
await rerunStep6ForStep7Recovery({ await rerunStep7ForStep8Recovery({
logMessage: '步骤 7:正在重新获取最新 CPA OAuth 链接,并快速重走步骤 6...', logMessage: '步骤 8:正在重新获取最新 CPA OAuth 链接,并快速重走步骤 7...',
skipPreLoginCleanup: true,
postStepDelayMs: 1200, postStepDelayMs: 1200,
}); });
await ensureStep7VerificationPageReady(); await ensureStep8VerificationPageReady();
await addLog('步骤 7:登录验证码页面已重新就绪,开始回填刚才获取到的验证码。', 'info'); await addLog('步骤 8:登录验证码页面已重新就绪,开始回填刚才获取到的验证码。', 'info');
} : undefined, } : undefined,
}); });
} }
async function rerunStep6ForStep7Recovery(options = {}) { async function rerunStep7ForStep8Recovery(options = {}) {
const { const {
logMessage = '步骤 7:正在回到步骤 6,重新发起登录验证码流程...', logMessage = '步骤 8:正在回到步骤 7,重新发起登录验证码流程...',
skipPreLoginCleanup = false,
postStepDelayMs = 3000, postStepDelayMs = 3000,
} = options; } = options;
const currentState = await getState(); const currentState = await getState();
await addLog(logMessage, 'warn'); await addLog(logMessage, 'warn');
await executeStep6(currentState, { skipPreLoginCleanup }); await executeStep7(currentState);
if (postStepDelayMs > 0) { if (postStepDelayMs > 0) {
await sleepWithStop(postStepDelayMs); await sleepWithStop(postStepDelayMs);
} }
} }
async function executeStep7(state) { async function executeStep8(state) {
if (shouldSkipLoginVerificationForCpaCallback(state)) { if (shouldSkipLoginVerificationForCpaCallback(state)) {
await setState({ await setState({
lastLoginCode: null, lastLoginCode: null,
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
}); });
await setStepStatus(7, 'skipped'); await setStepStatus(8, 'skipped');
await addLog('步骤 7:当前已选择“第步回调”,本轮无需获取登录验证码。', 'warn'); await addLog('步骤 8:当前已选择“第步回调”,本轮无需获取登录验证码。', 'warn');
return; return;
} }
@@ -136,7 +134,7 @@
while (true) { while (true) {
try { try {
await runStep7Attempt(currentState); await runStep8Attempt(currentState);
return; return;
} catch (err) { } catch (err) {
if (!isVerificationMailPollingError(err)) { if (!isVerificationMailPollingError(err)) {
@@ -150,25 +148,25 @@
mailPollingAttempt += 1; mailPollingAttempt += 1;
await addLog( await addLog(
`步骤 7:检测到邮箱轮询类失败,准备从步骤 6 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}...`, `步骤 8:检测到邮箱轮询类失败,准备从步骤 7 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}...`,
'warn' 'warn'
); );
await rerunStep6ForStep7Recovery(); await rerunStep7ForStep8Recovery();
currentState = await getState(); currentState = await getState();
} }
} }
if (lastMailPollingError) { if (lastMailPollingError) {
throw new Error( throw new Error(
`步骤 7:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}` `步骤 8:登录验证码流程在 ${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS} 轮邮箱轮询恢复后仍未成功。最后一次原因:${lastMailPollingError.message}`
); );
} }
throw new Error('步骤 7:登录验证码流程未成功完成。'); throw new Error('步骤 8:登录验证码流程未成功完成。');
} }
return { executeStep7 }; return { executeStep8 };
} }
return { createStep7Executor }; return { createStep8Executor };
}); });
+16 -22
View File
@@ -1,7 +1,7 @@
(function attachBackgroundStep6(root, factory) { (function attachBackgroundStep7(root, factory) {
root.MultiPageBackgroundStep6 = factory(); root.MultiPageBackgroundStep7 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep6Module() { })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep7Module() {
function createStep6Executor(deps = {}) { function createStep7Executor(deps = {}) {
const { const {
addLog, addLog,
completeStepFromBackground, completeStepFromBackground,
@@ -12,7 +12,6 @@
isStep6SuccessResult, isStep6SuccessResult,
refreshOAuthUrlBeforeStep6, refreshOAuthUrlBeforeStep6,
reuseOrCreateTab, reuseOrCreateTab,
runPreStep6CookieCleanup,
sendToContentScriptResilient, sendToContentScriptResilient,
shouldSkipLoginVerificationForCpaCallback, shouldSkipLoginVerificationForCpaCallback,
skipLoginVerificationStepsForCpaCallback, skipLoginVerificationStepsForCpaCallback,
@@ -20,8 +19,7 @@
throwIfStopped, throwIfStopped,
} = deps; } = deps;
async function executeStep6(state, options = {}) { async function executeStep7(state) {
const { skipPreLoginCleanup = false } = options;
if (shouldSkipLoginVerificationForCpaCallback(state)) { if (shouldSkipLoginVerificationForCpaCallback(state)) {
await skipLoginVerificationStepsForCpaCallback(); await skipLoginVerificationStepsForCpaCallback();
return; return;
@@ -30,10 +28,6 @@
throw new Error('缺少邮箱地址,请先完成步骤 3。'); throw new Error('缺少邮箱地址,请先完成步骤 3。');
} }
if (!skipPreLoginCleanup) {
await runPreStep6CookieCleanup();
}
let attempt = 0; let attempt = 0;
let lastError = null; let lastError = null;
@@ -46,9 +40,9 @@
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState); const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
if (attempt === 1) { if (attempt === 1) {
await addLog('步骤 6:正在打开最新 OAuth 链接并登录...'); await addLog('步骤 7:正在打开最新 OAuth 链接并登录...');
} else { } else {
await addLog(`步骤 6:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn'); await addLog(`步骤 7:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn');
} }
await reuseOrCreateTab('signup-page', oauthUrl); await reuseOrCreateTab('signup-page', oauthUrl);
@@ -57,7 +51,7 @@
'signup-page', 'signup-page',
{ {
type: 'EXECUTE_STEP', type: 'EXECUTE_STEP',
step: 6, step: 7,
source: 'background', source: 'background',
payload: { payload: {
email: currentState.email, email: currentState.email,
@@ -67,7 +61,7 @@
{ {
timeoutMs: 180000, timeoutMs: 180000,
retryDelayMs: 700, retryDelayMs: 700,
logMessage: '步骤 6:认证页正在切换,等待页面重新就绪后继续登录...', logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续登录...',
} }
); );
@@ -76,7 +70,7 @@
} }
if (isStep6SuccessResult(result)) { if (isStep6SuccessResult(result)) {
await completeStepFromBackground(6, { await completeStepFromBackground(7, {
loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, loginVerificationRequestedAt: result.loginVerificationRequestedAt || null,
}); });
return; return;
@@ -84,11 +78,11 @@
if (isStep6RecoverableResult(result)) { if (isStep6RecoverableResult(result)) {
const reasonMessage = result.message const reasonMessage = result.message
|| `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 6`; || `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 7`;
throw new Error(reasonMessage); throw new Error(reasonMessage);
} }
throw new Error('步骤 6:认证页未返回可识别的登录结果。'); throw new Error('步骤 7:认证页未返回可识别的登录结果。');
} catch (err) { } catch (err) {
throwIfStopped(err); throwIfStopped(err);
lastError = err; lastError = err;
@@ -96,15 +90,15 @@
break; break;
} }
await addLog(`步骤 6:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn'); await addLog(`步骤 7:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn');
} }
} }
throw new Error(`步骤 6:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`); throw new Error(`步骤 7:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
} }
return { executeStep6 }; return { executeStep7 };
} }
return { createStep6Executor }; return { createStep7Executor };
}); });
+27 -27
View File
@@ -1,7 +1,7 @@
(function attachBackgroundStep9(root, factory) { (function attachBackgroundStep10(root, factory) {
root.MultiPageBackgroundStep9 = factory(); root.MultiPageBackgroundStep10 = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep9Module() { })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep10Module() {
function createStep9Executor(deps = {}) { function createStep10Executor(deps = {}) {
const { const {
addLog, addLog,
chrome, chrome,
@@ -21,34 +21,34 @@
SUB2API_STEP9_RESPONSE_TIMEOUT_MS, SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
} = deps; } = deps;
async function executeStep9(state) { async function executeStep10(state) {
if (getPanelMode(state) === 'sub2api') { if (getPanelMode(state) === 'sub2api') {
return executeSub2ApiStep9(state); return executeSub2ApiStep10(state);
} }
return executeCpaStep9(state); return executeCpaStep10(state);
} }
async function executeCpaStep9(state) { async function executeCpaStep10(state) {
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。'); throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
} }
if (!state.localhostUrl) { if (!state.localhostUrl) {
throw new Error('缺少 localhost 回调地址,请先完成步骤 8。'); throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
} }
if (!state.vpsUrl) { if (!state.vpsUrl) {
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。'); throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
} }
if (shouldBypassStep9ForLocalCpa(state)) { if (shouldBypassStep9ForLocalCpa(state)) {
await addLog('步骤 9:检测到本地 CPA,且当前策略为“跳过第9步”,本轮不再重复提交回调地址。', 'info'); await addLog('步骤 10:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。', 'info');
await completeStepFromBackground(9, { await completeStepFromBackground(10, {
localhostUrl: state.localhostUrl, localhostUrl: state.localhostUrl,
verifiedStatus: 'local-auto', verifiedStatus: 'local-auto',
}); });
return; return;
} }
await addLog('步骤 9:正在打开 CPA 面板...'); await addLog('步骤 10:正在打开 CPA 面板...');
const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js']; const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
let tabId = await getTabId('vps-panel'); let tabId = await getTabId('vps-panel');
@@ -69,19 +69,19 @@
inject: injectFiles, inject: injectFiles,
timeoutMs: 45000, timeoutMs: 45000,
retryDelayMs: 900, retryDelayMs: 900,
logMessage: '步骤 9:CPA 面板仍在加载,正在重试连接...', logMessage: '步骤 10:CPA 面板仍在加载,正在重试连接...',
}); });
await addLog('步骤 9:正在填写回调地址...'); await addLog('步骤 10:正在填写回调地址...');
const result = await sendToContentScriptResilient('vps-panel', { const result = await sendToContentScriptResilient('vps-panel', {
type: 'EXECUTE_STEP', type: 'EXECUTE_STEP',
step: 9, step: 10,
source: 'background', source: 'background',
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword }, payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
}, { }, {
timeoutMs: 30000, timeoutMs: 30000,
retryDelayMs: 700, retryDelayMs: 700,
logMessage: '步骤 9:CPA 面板通信未就绪,正在等待页面恢复...', logMessage: '步骤 10:CPA 面板通信未就绪,正在等待页面恢复...',
}); });
if (result?.error) { if (result?.error) {
@@ -89,12 +89,12 @@
} }
} }
async function executeSub2ApiStep9(state) { async function executeSub2ApiStep10(state) {
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) { if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error('步骤 8 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 8。'); throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
} }
if (!state.localhostUrl) { if (!state.localhostUrl) {
throw new Error('缺少 localhost 回调地址,请先完成步骤 8。'); throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
} }
if (!state.sub2apiSessionId) { if (!state.sub2apiSessionId) {
throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。'); throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。');
@@ -109,7 +109,7 @@
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl); const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js']; const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
await addLog('步骤 9:正在打开 SUB2API 后台...'); await addLog('步骤 10:正在打开 SUB2API 后台...');
let tabId = await getTabId('sub2api-panel'); let tabId = await getTabId('sub2api-panel');
const alive = tabId && await isTabAlive('sub2api-panel'); const alive = tabId && await isTabAlive('sub2api-panel');
@@ -131,10 +131,10 @@
injectSource: 'sub2api-panel', injectSource: 'sub2api-panel',
}); });
await addLog('步骤 9:正在向 SUB2API 提交回调并创建账号...'); await addLog('步骤 10:正在向 SUB2API 提交回调并创建账号...');
const result = await sendToContentScript('sub2api-panel', { const result = await sendToContentScript('sub2api-panel', {
type: 'EXECUTE_STEP', type: 'EXECUTE_STEP',
step: 9, step: 10,
source: 'background', source: 'background',
payload: { payload: {
localhostUrl: state.localhostUrl, localhostUrl: state.localhostUrl,
@@ -157,11 +157,11 @@
} }
return { return {
executeCpaStep9, executeCpaStep10,
executeStep9, executeStep10,
executeSub2ApiStep9, executeSub2ApiStep10,
}; };
} }
return { createStep9Executor }; return { createStep10Executor };
}); });
+3 -3
View File
@@ -78,7 +78,7 @@
} }
return { return {
filterAfterTimestamp: getHotmailVerificationRequestTimestamp(7, state), filterAfterTimestamp: getHotmailVerificationRequestTimestamp(8, state),
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'], senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'], subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
targetEmail: state.email, targetEmail: state.email,
@@ -116,7 +116,7 @@
if (step === 4) { if (step === 4) {
await setState({ signupVerificationRequestedAt: requestedAt }); await setState({ signupVerificationRequestedAt: requestedAt });
} }
if (step === 7) { if (step === 8) {
await setState({ loginVerificationRequestedAt: requestedAt }); await setState({ loginVerificationRequestedAt: requestedAt });
} }
@@ -391,7 +391,7 @@
let lastResendAt = Number(options.lastResendAt) || 0; let lastResendAt = Number(options.lastResendAt) || 0;
const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => { const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => {
if ((step !== 4 && step !== 7) || !requestedAt) { if ((step !== 4 && step !== 8) || !requestedAt) {
return nextFilterAfterTimestamp; return nextFilterAfterTimestamp;
} }
+35 -35
View File
@@ -37,7 +37,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
} }
if (message.type === 'STEP8_FIND_AND_CLICK') { if (message.type === 'STEP8_FIND_AND_CLICK') {
log(`步骤 8${err.message}`, 'error'); log(`步骤 9${err.message}`, 'error');
sendResponse({ error: err.message }); sendResponse({ error: err.message });
return; return;
} }
@@ -61,8 +61,8 @@ async function handleCommand(message) {
case 2: return await step2_clickRegister(message.payload); case 2: return await step2_clickRegister(message.payload);
case 3: return await step3_fillEmailPassword(message.payload); case 3: return await step3_fillEmailPassword(message.payload);
case 5: return await step5_fillNameBirthday(message.payload); case 5: return await step5_fillNameBirthday(message.payload);
case 6: return await step6_login(message.payload); case 7: return await step6_login(message.payload);
case 8: return await step8_findAndClick(); case 9: return await step8_findAndClick();
default: throw new Error(`signup-page.js 不处理步骤 ${message.step}`); default: throw new Error(`signup-page.js 不处理步骤 ${message.step}`);
} }
case 'FILL_CODE': case 'FILL_CODE':
@@ -199,7 +199,7 @@ function isEmailVerificationPage() {
} }
async function resendVerificationCode(step, timeout = 45000) { async function resendVerificationCode(step, timeout = 45000) {
if (step === 7) { if (step === 8) {
await waitForLoginVerificationPageReady(); await waitForLoginVerificationPageReady();
} }
@@ -1185,7 +1185,7 @@ async function waitForLoginVerificationPageReady(timeout = 10000) {
} }
throw new Error( throw new Error(
`当前未进入登录验证码页面,请先重新完成步骤 6。当前状态:${getLoginAuthStateLabel(snapshot)}。URL: ${snapshot?.url || location.href}` `当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:${getLoginAuthStateLabel(snapshot)}。URL: ${snapshot?.url || location.href}`
); );
} }
@@ -1216,15 +1216,15 @@ async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, messag
try { try {
const recoveryResult = await recoverCurrentAuthRetryPage({ const recoveryResult = await recoverCurrentAuthRetryPage({
flow: 'login', flow: 'login',
logLabel: '步骤 6:检测到登录超时报错,正在点击“重试”恢复当前页面', logLabel: '步骤 7:检测到登录超时报错,正在点击“重试”恢复当前页面',
step: 6, step: 6,
timeoutMs: 12000, timeoutMs: 12000,
}); });
if (recoveryResult?.recovered) { if (recoveryResult?.recovered) {
log('步骤 6:登录超时报错页已点击“重试”,准备重新执行当前步骤。', 'warn'); log('步骤 7:登录超时报错页已点击“重试”,准备重新执行当前步骤。', 'warn');
} }
} catch (error) { } catch (error) {
log(`步骤 6:登录超时报错页自动点击“重试”失败:${error.message}`, 'warn'); log(`步骤 7:登录超时报错页自动点击“重试”失败:${error.message}`, 'warn');
} }
} }
@@ -1248,9 +1248,9 @@ function throwForStep6FatalState(snapshot) {
snapshot = normalizeStep6Snapshot(snapshot); snapshot = normalizeStep6Snapshot(snapshot);
switch (snapshot?.state) { switch (snapshot?.state) {
case 'oauth_consent_page': case 'oauth_consent_page':
throw new Error(`当前页面已进入 OAuth 授权页,未经过登录验证码页,无法完成步骤 6。URL: ${snapshot.url}`); throw new Error(`当前页面已进入 OAuth 授权页,未经过登录验证码页,无法完成步骤 7。URL: ${snapshot.url}`);
case 'add_phone_page': case 'add_phone_page':
throw new Error(`当前页面已进入手机号页面,未经过登录验证码页,无法完成步骤 6。URL: ${snapshot.url}`); throw new Error(`当前页面已进入手机号页面,未经过登录验证码页,无法完成步骤 7。URL: ${snapshot.url}`);
case 'unknown': case 'unknown':
throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`); throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`);
default: default:
@@ -1412,7 +1412,7 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
async function waitForVerificationSubmitOutcome(step, timeout) { async function waitForVerificationSubmitOutcome(step, timeout) {
const resolvedTimeout = timeout ?? (step === 7 ? 30000 : 12000); const resolvedTimeout = timeout ?? (step === 8 ? 30000 : 12000);
const start = Date.now(); const start = Date.now();
while (Date.now() - start < resolvedTimeout) { while (Date.now() - start < resolvedTimeout) {
@@ -1427,11 +1427,11 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
return { success: true }; return { success: true };
} }
if (step === 7 && isStep8Ready()) { if (step === 8 && isStep8Ready()) {
return { success: true }; return { success: true };
} }
if (step === 7 && isAddPhonePageReady()) { if (step === 8 && isAddPhonePageReady()) {
return { success: true, addPhonePage: true }; return { success: true, addPhonePage: true };
} }
@@ -1454,7 +1454,7 @@ async function fillVerificationCode(step, payload) {
log(`步骤 ${step}:正在填写验证码:${code}`); log(`步骤 ${step}:正在填写验证码:${code}`);
if (step === 7) { if (step === 8) {
await waitForLoginVerificationPageReady(); await waitForLoginVerificationPageReady();
} }
@@ -1771,11 +1771,11 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) {
}); });
} }
log('步骤 6:已检测到一次性验证码登录入口,准备切换...'); log('步骤 7:已检测到一次性验证码登录入口,准备切换...');
const loginVerificationRequestedAt = Date.now(); const loginVerificationRequestedAt = Date.now();
await humanPause(350, 900); await humanPause(350, 900);
simulateClick(switchTrigger); simulateClick(switchTrigger);
log('步骤 6:已点击一次性验证码登录'); log('步骤 7:已点击一次性验证码登录');
await sleep(1200); await sleep(1200);
return waitForStep6SwitchTransition(loginVerificationRequestedAt); return waitForStep6SwitchTransition(loginVerificationRequestedAt);
} }
@@ -1785,26 +1785,26 @@ async function step6LoginFromPasswordPage(payload, snapshot) {
if (currentSnapshot.passwordInput) { if (currentSnapshot.passwordInput) {
if (!payload.password) { if (!payload.password) {
throw new Error('登录时缺少密码,步骤 6 无法继续。'); throw new Error('登录时缺少密码,步骤 7 无法继续。');
} }
log('步骤 6:已进入密码页,准备填写密码...'); log('步骤 7:已进入密码页,准备填写密码...');
await humanPause(550, 1450); await humanPause(550, 1450);
fillInput(currentSnapshot.passwordInput, payload.password); fillInput(currentSnapshot.passwordInput, payload.password);
log('步骤 6:已填写密码'); log('步骤 7:已填写密码');
await sleep(500); await sleep(500);
const passwordSubmittedAt = Date.now(); const passwordSubmittedAt = Date.now();
await triggerLoginSubmitAction(currentSnapshot.submitButton, currentSnapshot.passwordInput); await triggerLoginSubmitAction(currentSnapshot.submitButton, currentSnapshot.passwordInput);
log('步骤 6:已提交密码'); log('步骤 7:已提交密码');
const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt); const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt);
if (transition.action === 'done') { if (transition.action === 'done') {
log('步骤 6:已进入登录验证码页面。', 'ok'); log('步骤 7:已进入登录验证码页面。', 'ok');
return transition.result; return transition.result;
} }
if (transition.action === 'recoverable') { if (transition.action === 'recoverable') {
log(`步骤 6${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 6。'}`, 'warn'); log(`步骤 7${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 7。'}`, 'warn');
return transition.result; return transition.result;
} }
if (transition.action === 'switch') { if (transition.action === 'switch') {
@@ -1835,23 +1835,23 @@ async function step6LoginFromEmailPage(payload, snapshot) {
if ((emailInput.value || '').trim() !== payload.email) { if ((emailInput.value || '').trim() !== payload.email) {
await humanPause(500, 1400); await humanPause(500, 1400);
fillInput(emailInput, payload.email); fillInput(emailInput, payload.email);
log('步骤 6:已填写邮箱'); log('步骤 7:已填写邮箱');
} else { } else {
log('步骤 6:邮箱已在输入框中,准备提交...'); log('步骤 7:邮箱已在输入框中,准备提交...');
} }
await sleep(500); await sleep(500);
const emailSubmittedAt = Date.now(); const emailSubmittedAt = Date.now();
await triggerLoginSubmitAction(currentSnapshot.submitButton, emailInput); await triggerLoginSubmitAction(currentSnapshot.submitButton, emailInput);
log('步骤 6:已提交邮箱'); log('步骤 7:已提交邮箱');
const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt); const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt);
if (transition.action === 'done') { if (transition.action === 'done') {
log('步骤 6:已进入登录验证码页面。', 'ok'); log('步骤 7:已进入登录验证码页面。', 'ok');
return transition.result; return transition.result;
} }
if (transition.action === 'recoverable') { if (transition.action === 'recoverable') {
log(`步骤 6${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 6。'}`, 'warn'); log(`步骤 7${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 7。'}`, 'warn');
return transition.result; return transition.result;
} }
if (transition.action === 'password') { if (transition.action === 'password') {
@@ -1867,17 +1867,17 @@ async function step6_login(payload) {
const { email } = payload; const { email } = payload;
if (!email) throw new Error('登录时缺少邮箱地址。'); if (!email) throw new Error('登录时缺少邮箱地址。');
log(`步骤 6:正在使用 ${email} 登录...`); log(`步骤 7:正在使用 ${email} 登录...`);
const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000)); const snapshot = normalizeStep6Snapshot(await waitForKnownLoginAuthState(15000));
if (snapshot.state === 'verification_page') { if (snapshot.state === 'verification_page') {
log('步骤 6:登录验证码页面已就绪。', 'ok'); log('步骤 7:登录验证码页面已就绪。', 'ok');
return createStep6SuccessResult(snapshot, { via: 'already_on_verification_page' }); return createStep6SuccessResult(snapshot, { via: 'already_on_verification_page' });
} }
if (snapshot.state === 'login_timeout_error_page') { if (snapshot.state === 'login_timeout_error_page') {
log('步骤 6:检测到登录超时报错,准备重新执行步骤 6。', 'warn'); log('步骤 7:检测到登录超时报错,准备重新执行步骤 7。', 'warn');
return await createStep6LoginTimeoutRecoverableResult( return await createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page', 'login_timeout_error_page',
snapshot, snapshot,
@@ -1898,19 +1898,19 @@ async function step6_login(payload) {
} }
// ============================================================ // ============================================================
// Step 8: Find "继续" on OAuth consent page for debugger click // Step 9: Find "继续" on OAuth consent page for debugger click
// ============================================================ // ============================================================
// After login + verification, page shows: // After login + verification, page shows:
// "使用 ChatGPT 登录到 Codex" with a "继续" submit button. // "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
// Background performs the actual click through the debugger Input API. // Background performs the actual click through the debugger Input API.
async function step8_findAndClick() { async function step8_findAndClick() {
log('步骤 8:正在查找 OAuth 同意页的“继续”按钮...'); log('步骤 9:正在查找 OAuth 同意页的“继续”按钮...');
const continueBtn = await prepareStep8ContinueButton(); const continueBtn = await prepareStep8ContinueButton();
const rect = getSerializableRect(continueBtn); const rect = getSerializableRect(continueBtn);
log('步骤 8:已找到“继续”按钮并准备好调试器点击坐标。'); log('步骤 9:已找到“继续”按钮并准备好调试器点击坐标。');
return { return {
rect, rect,
buttonText: (continueBtn.textContent || '').trim(), buttonText: (continueBtn.textContent || '').trim(),
@@ -1969,10 +1969,10 @@ async function step8_triggerContinue(payload = {}) {
simulateClick(continueBtn); simulateClick(continueBtn);
break; break;
default: default:
throw new Error(`未知的 Step 8 触发策略:${strategy}`); throw new Error(`未知的 Step 9 触发策略:${strategy}`);
} }
log(`Step 8: continue button triggered via ${strategy}.`); log(`Step 9: continue button triggered via ${strategy}.`);
return { return {
strategy, strategy,
...getStep8State(), ...getStep8State(),
+6 -6
View File
@@ -66,7 +66,7 @@ async function handleStep(step, payload = {}) {
switch (step) { switch (step) {
case 1: case 1:
return step1_generateOpenAiAuthUrl(payload); return step1_generateOpenAiAuthUrl(payload);
case 9: case 10:
return step9_submitOpenAiCallback(payload); return step9_submitOpenAiCallback(payload);
default: default:
throw new Error(`sub2api-panel.js 不处理步骤 ${step}`); throw new Error(`sub2api-panel.js 不处理步骤 ${step}`);
@@ -218,7 +218,7 @@ function parseLocalhostCallback(rawUrl) {
throw new Error('回调 URL 协议不正确。'); throw new Error('回调 URL 协议不正确。');
} }
if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) { if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) {
throw new Error('步骤 9 只接受 localhost / 127.0.0.1 回调地址。'); throw new Error('步骤 10 只接受 localhost / 127.0.0.1 回调地址。');
} }
if (parsed.pathname !== '/auth/callback') { if (parsed.pathname !== '/auth/callback') {
throw new Error('回调 URL 路径必须是 /auth/callback。'); throw new Error('回调 URL 路径必须是 /auth/callback。');
@@ -365,7 +365,7 @@ async function step9_submitOpenAiCallback(payload = {}) {
throw new Error('本次 localhost 回调中的 state 与步骤 1 生成的 state 不一致,请重新执行步骤 1。'); throw new Error('本次 localhost 回调中的 state 与步骤 1 生成的 state 不一致,请重新执行步骤 1。');
} }
log('步骤 9:正在向 SUB2API 交换 OpenAI 授权码...'); log('步骤 10:正在向 SUB2API 交换 OpenAI 授权码...');
const exchangeData = await requestJson(origin, '/api/v1/admin/openai/exchange-code', { const exchangeData = await requestJson(origin, '/api/v1/admin/openai/exchange-code', {
method: 'POST', method: 'POST',
token, token,
@@ -399,7 +399,7 @@ async function step9_submitOpenAiCallback(payload = {}) {
createPayload.extra = extra; createPayload.extra = extra;
} }
log(`步骤 9:授权码交换成功,正在创建 SUB2API 账号(名称:${accountName}...`); log(`步骤 10:授权码交换成功,正在创建 SUB2API 账号(名称:${accountName}...`);
const createdAccount = await requestJson(origin, '/api/v1/admin/accounts', { const createdAccount = await requestJson(origin, '/api/v1/admin/accounts', {
method: 'POST', method: 'POST',
token, token,
@@ -407,8 +407,8 @@ async function step9_submitOpenAiCallback(payload = {}) {
}); });
const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`; const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`;
log(`步骤 9${verifiedStatus}`, 'ok'); log(`步骤 10${verifiedStatus}`, 'ok');
reportComplete(9, { reportComplete(10, {
localhostUrl: callback.url, localhostUrl: callback.url,
verifiedStatus, verifiedStatus,
}); });
+15 -15
View File
@@ -83,7 +83,7 @@ if (document.documentElement.getAttribute(VPS_PANEL_LISTENER_SENTINEL) !== '1')
async function handleStep(step, payload) { async function handleStep(step, payload) {
switch (step) { switch (step) {
case 1: return await step1_getOAuthLink(payload); case 1: return await step1_getOAuthLink(payload);
case 9: return await step9_vpsVerify(payload); case 10: return await step9_vpsVerify(payload);
default: default:
throw new Error(`vps-panel.js 不处理步骤 ${step}`); throw new Error(`vps-panel.js 不处理步骤 ${step}`);
} }
@@ -467,11 +467,11 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
if (diagnostics.signature !== lastDiagnosticsSignature) { if (diagnostics.signature !== lastDiagnosticsSignature) {
lastDiagnosticsSignature = diagnostics.signature; lastDiagnosticsSignature = diagnostics.signature;
lastHeartbeatLoggedAt = elapsed; lastHeartbeatLoggedAt = elapsed;
log(`步骤 9:认证状态检测中,${diagnostics.summary}`); log(`步骤 10:认证状态检测中,${diagnostics.summary}`);
console.log(LOG_PREFIX, '[Step 9] status badge diagnostics changed', diagnostics); console.log(LOG_PREFIX, '[Step 9] status badge diagnostics changed', diagnostics);
} else if (elapsed - lastHeartbeatLoggedAt >= 10000) { } else if (elapsed - lastHeartbeatLoggedAt >= 10000) {
lastHeartbeatLoggedAt = elapsed; lastHeartbeatLoggedAt = elapsed;
log(`步骤 9:仍在等待认证成功,${diagnostics.summary}`); log(`步骤 10:仍在等待认证成功,${diagnostics.summary}`);
console.log(LOG_PREFIX, '[Step 9] still waiting for success badge', diagnostics); console.log(LOG_PREFIX, '[Step 9] still waiting for success badge', diagnostics);
} }
@@ -488,7 +488,7 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
? `;错误样式徽标:${diagnostics.errorStyledSummary}` ? `;错误样式徽标:${diagnostics.errorStyledSummary}`
: ''; : '';
log( log(
`步骤 9:检测到“认证成功”相关徽标,但未命中精确成功条件。当前聚焦="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}${errorStyledSuffix}`, `步骤 10:检测到“认证成功”相关徽标,但未命中精确成功条件。当前聚焦="${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);
@@ -507,7 +507,7 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
? diagnostics.pageErrorSummary ? diagnostics.pageErrorSummary
: diagnostics.failureSummary; : diagnostics.failureSummary;
log( log(
`步骤 9:同时检测到成功徽标和失败提示,本轮不判定成功。成功徽标:${diagnostics.exactSuccessSummary};失败提示:${failureSummary}`, `步骤 10:同时检测到成功徽标和失败提示,本轮不判定成功。成功徽标:${diagnostics.exactSuccessSummary};失败提示:${failureSummary}`,
'warn' 'warn'
); );
console.warn(LOG_PREFIX, '[Step 9] success badge is blocked by visible failure', diagnostics); console.warn(LOG_PREFIX, '[Step 9] success badge is blocked by visible failure', diagnostics);
@@ -777,7 +777,7 @@ async function step1_getOAuthLink(payload, options = {}) {
} }
// ============================================================ // ============================================================
// 步骤 9:CPA 回调验证——填写 localhost 回调地址并提交 // 步骤 10:CPA 回调验证——填写 localhost 回调地址并提交
// ============================================================ // ============================================================
async function step9_vpsVerify(payload) { async function step9_vpsVerify(payload) {
@@ -786,22 +786,22 @@ async function step9_vpsVerify(payload) {
// 优先从 payload 读取 localhostUrl;没有时再回退到全局状态 // 优先从 payload 读取 localhostUrl;没有时再回退到全局状态
let localhostUrl = payload?.localhostUrl; let localhostUrl = payload?.localhostUrl;
if (localhostUrl && !isLocalhostOAuthCallbackUrl(localhostUrl)) { if (localhostUrl && !isLocalhostOAuthCallbackUrl(localhostUrl)) {
throw new Error('步骤 9 只接受真实的 localhost OAuth 回调地址,请重新执行步骤 8。'); throw new Error('步骤 10 只接受真实的 localhost OAuth 回调地址,请重新执行步骤 9。');
} }
if (!localhostUrl) { if (!localhostUrl) {
log('步骤 9payload 中没有 localhostUrl,正在从状态中读取...'); log('步骤 10payload 中没有 localhostUrl,正在从状态中读取...');
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
localhostUrl = state.localhostUrl; localhostUrl = state.localhostUrl;
if (localhostUrl && !isLocalhostOAuthCallbackUrl(localhostUrl)) { if (localhostUrl && !isLocalhostOAuthCallbackUrl(localhostUrl)) {
throw new Error('步骤 9 只接受真实的 localhost OAuth 回调地址,请重新执行步骤 8。'); throw new Error('步骤 10 只接受真实的 localhost OAuth 回调地址,请重新执行步骤 9。');
} }
} }
if (!localhostUrl) { if (!localhostUrl) {
throw new Error('未找到 localhost 回调地址,请先完成步骤 8。'); throw new Error('未找到 localhost 回调地址,请先完成步骤 8。');
} }
log(`步骤 9:已获取 localhostUrl${localhostUrl.slice(0, 60)}...`); log(`步骤 10:已获取 localhostUrl${localhostUrl.slice(0, 60)}...`);
log('步骤 9:正在查找回调地址输入框...'); log('步骤 10:正在查找回调地址输入框...');
// Find the callback URL input // Find the callback URL input
// Actual DOM: <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=..."> // Actual DOM: <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=...">
@@ -818,7 +818,7 @@ async function step9_vpsVerify(payload) {
await humanPause(600, 1500); await humanPause(600, 1500);
fillInput(urlInput, localhostUrl); fillInput(urlInput, localhostUrl);
log(`步骤 9:已填写回调地址:${localhostUrl.slice(0, 80)}...`); log(`步骤 10:已填写回调地址:${localhostUrl.slice(0, 80)}...`);
// Find and click the callback submit button in supported UI languages. // Find and click the callback submit button in supported UI languages.
const callbackSubmitPattern = /提交回调\s*URL|Submit\s+Callback\s+URL|Отправить\s+Callback\s+URL/i; const callbackSubmitPattern = /提交回调\s*URL|Submit\s+Callback\s+URL|Отправить\s+Callback\s+URL/i;
@@ -839,9 +839,9 @@ async function step9_vpsVerify(payload) {
await humanPause(450, 1200); await humanPause(450, 1200);
simulateClick(submitBtn); simulateClick(submitBtn);
log('步骤 9:已点击回调提交按钮,正在等待认证结果...'); log('步骤 10:已点击回调提交按钮,正在等待认证结果...');
const verifiedStatus = await waitForExactSuccessBadge(); const verifiedStatus = await waitForExactSuccessBadge();
log(`步骤 9${verifiedStatus}`, 'ok'); log(`步骤 10${verifiedStatus}`, 'ok');
reportComplete(9, { localhostUrl, verifiedStatus }); reportComplete(10, { localhostUrl, verifiedStatus });
} }
+5 -4
View File
@@ -7,10 +7,11 @@
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' }, { id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' },
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' }, { id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' },
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' }, { id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' },
{ id: 6, order: 60, key: 'oauth-login', title: '刷新 OAuth 并登录' }, { id: 6, order: 60, key: 'clear-login-cookies', title: '清理登录 Cookies' },
{ id: 7, order: 70, key: 'fetch-login-code', title: '获取登录验证码' }, { id: 7, order: 70, key: 'oauth-login', title: '刷新 OAuth 并登录' },
{ id: 8, order: 80, key: 'confirm-oauth', title: '自动确认 OAuth' }, { id: 8, order: 80, key: 'fetch-login-code', title: '获取登录验证码' },
{ id: 9, order: 90, key: 'platform-verify', title: '平台回调验证' }, { id: 9, order: 90, key: 'confirm-oauth', title: '自动确认 OAuth' },
{ id: 10, order: 100, key: 'platform-verify', title: '平台回调验证' },
]; ];
function getSteps() { function getSteps() {
+4 -4
View File
@@ -118,16 +118,16 @@
</div> </div>
<div class="data-row" id="row-local-cpa-step9-mode"> <div class="data-row" id="row-local-cpa-step9-mode">
<span class="data-label">本地 CPA</span> <span class="data-label">本地 CPA</span>
<div id="local-cpa-step9-mode-group" class="choice-group" role="group" aria-label="本地 CPA 第 9 步策略"> <div id="local-cpa-step9-mode-group" class="choice-group" role="group" aria-label="本地 CPA 第 10 步策略">
<button type="button" class="choice-btn" data-local-cpa-step9-mode="submit">全部回调</button> <button type="button" class="choice-btn" data-local-cpa-step9-mode="submit">全部回调</button>
<button type="button" class="choice-btn" data-local-cpa-step9-mode="bypass">跳过第9</button> <button type="button" class="choice-btn" data-local-cpa-step9-mode="bypass">跳过第10</button>
</div> </div>
</div> </div>
<div class="data-row" id="row-cpa-callback-mode"> <div class="data-row" id="row-cpa-callback-mode">
<span class="data-label">CPA回调</span> <span class="data-label">CPA回调</span>
<div id="cpa-callback-mode-group" class="choice-group" role="group" aria-label="CPA 回调阶段"> <div id="cpa-callback-mode-group" class="choice-group" role="group" aria-label="CPA 回调阶段">
<button type="button" class="choice-btn" data-cpa-callback-mode="step6">步回调</button> <button type="button" class="choice-btn" data-cpa-callback-mode="step7">步回调</button>
<button type="button" class="choice-btn" data-cpa-callback-mode="step8">步回调</button> <button type="button" class="choice-btn" data-cpa-callback-mode="step9">步回调</button>
</div> </div>
</div> </div>
<div class="data-row" id="row-sub2api-url" style="display:none;"> <div class="data-row" id="row-sub2api-url" style="display:none;">
+12 -7
View File
@@ -194,7 +194,7 @@ const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
const AUTO_STEP_DELAY_MIN_SECONDS = 0; const AUTO_STEP_DELAY_MIN_SECONDS = 0;
const AUTO_STEP_DELAY_MAX_SECONDS = 600; const AUTO_STEP_DELAY_MAX_SECONDS = 600;
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit'; const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
const DEFAULT_CPA_CALLBACK_MODE = 'step8'; const DEFAULT_CPA_CALLBACK_MODE = 'step9';
const MAIL_2925_MODE_PROVIDE = 'provide'; const MAIL_2925_MODE_PROVIDE = 'provide';
const MAIL_2925_MODE_RECEIVE = 'receive'; const MAIL_2925_MODE_RECEIVE = 'receive';
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE; const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
@@ -834,7 +834,7 @@ function getStepStatuses(state = latestState) {
function getFirstUnfinishedStep(state = latestState) { function getFirstUnfinishedStep(state = latestState) {
const statuses = getStepStatuses(state); const statuses = getStepStatuses(state);
for (let step = 1; step <= 9; step++) { for (const step of STEP_IDS) {
if (!isDoneStatus(statuses[step])) { if (!isDoneStatus(statuses[step])) {
return step; return step;
} }
@@ -1320,9 +1320,14 @@ function normalizeLocalCpaStep9Mode(value = '') {
} }
function normalizeCpaCallbackMode(value = '') { function normalizeCpaCallbackMode(value = '') {
return String(value || '').trim().toLowerCase() === 'step6' const normalized = String(value || '').trim().toLowerCase();
? 'step6' if (normalized === 'step7' || normalized === 'step6') {
: DEFAULT_CPA_CALLBACK_MODE; return 'step7';
}
if (normalized === 'step9' || normalized === 'step8') {
return 'step9';
}
return DEFAULT_CPA_CALLBACK_MODE;
} }
function normalizeMail2925Mode(value = '') { function normalizeMail2925Mode(value = '') {
@@ -2623,8 +2628,8 @@ function updateStatusDisplay(state) {
.map(([k]) => Number(k)) .map(([k]) => Number(k))
.sort((a, b) => b - a)[0]; .sort((a, b) => b - a)[0];
if (lastCompleted === 9) { if (lastCompleted === STEP_IDS[STEP_IDS.length - 1]) {
displayStatus.textContent = (state.stepStatuses[9] === 'manual_completed' || state.stepStatuses[9] === 'skipped') ? '全部步骤已跳过/完成' : '全部步骤已完成'; displayStatus.textContent = (state.stepStatuses[lastCompleted] === 'manual_completed' || state.stepStatuses[lastCompleted] === 'skipped') ? '全部步骤已跳过/完成' : '全部步骤已完成';
statusBar.classList.add('completed'); statusBar.classList.add('completed');
} else if (lastCompleted) { } else if (lastCompleted) {
displayStatus.textContent = (state.stepStatuses[lastCompleted] === 'manual_completed' || state.stepStatuses[lastCompleted] === 'skipped') displayStatus.textContent = (state.stepStatuses[lastCompleted] === 'manual_completed' || state.stepStatuses[lastCompleted] === 'skipped')
@@ -70,6 +70,7 @@ const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
const AUTO_RUN_RETRY_DELAY_MS = 3000; const AUTO_RUN_RETRY_DELAY_MS = 3000;
const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds'; const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry'; const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
const STEP_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const DEFAULT_STATE = { const DEFAULT_STATE = {
stepStatuses: { stepStatuses: {
1: 'pending', 1: 'pending',
@@ -81,6 +82,7 @@ const DEFAULT_STATE = {
7: 'pending', 7: 'pending',
8: 'pending', 8: 'pending',
9: 'pending', 9: 'pending',
10: 'pending',
}, },
}; };
@@ -220,6 +222,7 @@ async function runAutoSequenceFromStep() {
7: 'completed', 7: 'completed',
8: 'completed', 8: 'completed',
9: 'completed', 9: 'completed',
10: 'completed',
}, },
tabRegistry: { tabRegistry: {
'signup-page': { tabId: 88, ready: true }, 'signup-page': { tabId: 88, ready: true },
+22 -20
View File
@@ -61,15 +61,17 @@ const bundle = [
function createHarness(options = {}) { function createHarness(options = {}) {
const { const {
startStep = 6, startStep = 7,
failureStep = 9, failureStep = 10,
failureBudget = 1, failureBudget = 1,
failureMessage = '认证失败: Request failed with status code 502', failureMessage = '认证失败: Request failed with status code 502',
authState = { state: 'password_page', url: 'https://auth.openai.com/log-in' }, authState = { state: 'password_page', url: 'https://auth.openai.com/log-in' },
} = options; } = options;
return new Function(` 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 AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const LOG_PREFIX = '[test]'; const LOG_PREFIX = '[test]';
const chrome = { const chrome = {
tabs: { tabs: {
@@ -157,9 +159,9 @@ return {
`)(); `)();
} }
test('auto-run keeps restarting from step 6 after post-login failures without a hard cap', async () => { test('auto-run keeps restarting from step 7 after post-login failures without a hard cap', async () => {
const harness = createHarness({ const harness = createHarness({
failureStep: 9, failureStep: 10,
failureBudget: 6, failureBudget: 6,
failureMessage: '认证失败: Request failed with status code 502', failureMessage: '认证失败: Request failed with status code 502',
authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' }, authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
@@ -171,23 +173,23 @@ test('auto-run keeps restarting from step 6 after post-login failures without a
assert.deepStrictEqual( assert.deepStrictEqual(
events.steps, events.steps,
[ [
6, 7, 8, 9, 7, 8, 9, 10,
6, 7, 8, 9, 7, 8, 9, 10,
6, 7, 8, 9, 7, 8, 9, 10,
6, 7, 8, 9, 7, 8, 9, 10,
6, 7, 8, 9, 7, 8, 9, 10,
6, 7, 8, 9, 7, 8, 9, 10,
6, 7, 8, 9, 7, 8, 9, 10,
] ]
); );
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新开始授权流程/.test(message))); assert.ok(events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
}); });
test('auto-run stops restarting once add-phone is detected', async () => { test('auto-run stops restarting once add-phone is detected', async () => {
const harness = createHarness({ const harness = createHarness({
failureStep: 6, failureStep: 7,
failureBudget: 1, failureBudget: 1,
failureMessage: '当前页面已进入手机号页。URL: https://auth.openai.com/add-phone', failureMessage: '当前页面已进入手机号页。URL: https://auth.openai.com/add-phone',
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' }, authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
}); });
@@ -195,13 +197,13 @@ test('auto-run stops restarting once add-phone is detected', async () => {
assert.ok(result?.error); assert.ok(result?.error);
assert.equal(result.events.invalidations.length, 0); assert.equal(result.events.invalidations.length, 0);
assert.deepStrictEqual(result.events.steps, [6]); assert.deepStrictEqual(result.events.steps, [7]);
assert.ok(result.events.logs.some(({ message }) => /进入 add-phone/.test(message))); assert.ok(result.events.logs.some(({ message }) => /进入 add-phone/.test(message)));
}); });
test('auto-run stop errors after step 6 are rethrown immediately instead of restarting', async () => { test('auto-run stop errors after step 7 are rethrown immediately instead of restarting', async () => {
const harness = createHarness({ const harness = createHarness({
failureStep: 8, failureStep: 9,
failureBudget: 1, failureBudget: 1,
failureMessage: '流程已被用户停止。', failureMessage: '流程已被用户停止。',
authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' }, authState: { state: 'password_page', url: 'https://auth.openai.com/log-in' },
@@ -211,6 +213,6 @@ test('auto-run stop errors after step 6 are rethrown immediately instead of rest
assert.equal(result?.error?.message, '流程已被用户停止。'); assert.equal(result?.error?.message, '流程已被用户停止。');
assert.equal(result.events.invalidations.length, 0); assert.equal(result.events.invalidations.length, 0);
assert.deepStrictEqual(result.events.steps, [6, 7, 8]); assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 6 重新开始授权流程/.test(message))); assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
}); });
@@ -67,7 +67,10 @@ const PERSISTED_SETTING_DEFAULTS = {
}; };
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; } function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; } function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeCpaCallbackMode(value) { return value === 'step6' ? 'step6' : 'step8'; } function normalizeCpaCallbackMode(value) {
if (value === 'step7' || value === 'step6') return 'step7';
return value === 'step9' || value === 'step8' ? 'step9' : 'step9';
}
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; } function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; } function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value == null || value === '' ? null : Number(value); } function normalizeAutoStepDelaySeconds(value) { return value == null || value === '' ? null : Number(value); }
+2 -2
View File
@@ -496,7 +496,7 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null); assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null);
}); });
test('handleStepData step 9 marks current LuckMail purchase as used and clears runtime state', async () => { test('handleStepData step 10 marks current LuckMail purchase as used and clears runtime state', async () => {
const bundle = extractFunction('handleStepData'); const bundle = extractFunction('handleStepData');
const factory = new Function(` const factory = new Function(`
@@ -561,7 +561,7 @@ return {
`); `);
const api = factory(); const api = factory();
await api.handleStepData(9, { await api.handleStepData(10, {
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz', localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
}); });
+2 -1
View File
@@ -2,7 +2,7 @@ const test = require('node:test');
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
const fs = require('node:fs'); const fs = require('node:fs');
test('background imports step 1~9 modules', () => { test('background imports step 1~10 modules', () => {
const source = fs.readFileSync('background.js', 'utf8'); const source = fs.readFileSync('background.js', 'utf8');
[ [
@@ -11,6 +11,7 @@ test('background imports step 1~9 modules', () => {
'background/steps/fill-password.js', 'background/steps/fill-password.js',
'background/steps/fetch-signup-code.js', 'background/steps/fetch-signup-code.js',
'background/steps/fill-profile.js', 'background/steps/fill-profile.js',
'background/steps/clear-login-cookies.js',
'background/steps/oauth-login.js', 'background/steps/oauth-login.js',
'background/steps/fetch-login-code.js', 'background/steps/fetch-login-code.js',
'background/steps/confirm-oauth.js', 'background/steps/confirm-oauth.js',
+31 -56
View File
@@ -2,19 +2,43 @@ const test = require('node:test');
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
const fs = require('node:fs'); const fs = require('node:fs');
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); test('step 6 runs cookie cleanup and completes from background', async () => {
const globalScope = {}; const source = fs.readFileSync('background/steps/clear-login-cookies.js', 'utf8');
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep6;`)(globalScope); const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep6;`)(globalScope);
test('step 6 retries up to configured limit and then fails', async () => {
const events = { const events = {
cleanupCalls: 0, cleanupCalls: 0,
completedSteps: [],
};
const executor = api.createStep6Executor({
completeStepFromBackground: async (step) => {
events.completedSteps.push(step);
},
runPreStep6CookieCleanup: async () => {
events.cleanupCalls += 1;
},
});
await executor.executeStep6();
assert.equal(events.cleanupCalls, 1);
assert.deepStrictEqual(events.completedSteps, [6]);
});
test('step 7 retries up to configured limit and then fails', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
refreshCalls: 0, refreshCalls: 0,
sendCalls: 0, sendCalls: 0,
completed: 0, completed: 0,
}; };
const executor = api.createStep6Executor({ const executor = api.createStep7Executor({
addLog: async () => {}, addLog: async () => {},
completeStepFromBackground: async () => { completeStepFromBackground: async () => {
events.completed += 1; events.completed += 1;
@@ -29,15 +53,12 @@ test('step 6 retries up to configured limit and then fails', async () => {
return `https://oauth.example/${events.refreshCalls}`; return `https://oauth.example/${events.refreshCalls}`;
}, },
reuseOrCreateTab: async () => {}, reuseOrCreateTab: async () => {},
runPreStep6CookieCleanup: async () => {
events.cleanupCalls += 1;
},
sendToContentScriptResilient: async () => { sendToContentScriptResilient: async () => {
events.sendCalls += 1; events.sendCalls += 1;
return { return {
step6Outcome: 'recoverable', step6Outcome: 'recoverable',
state: 'email_page', state: 'email_page',
message: '当前仍停留在邮箱页', message: '当前仍停留在邮箱页',
}; };
}, },
shouldSkipLoginVerificationForCpaCallback: () => false, shouldSkipLoginVerificationForCpaCallback: () => false,
@@ -47,57 +68,11 @@ test('step 6 retries up to configured limit and then fails', async () => {
}); });
await assert.rejects( await assert.rejects(
() => executor.executeStep6({ email: 'user@example.com', password: 'secret' }), () => executor.executeStep7({ email: 'user@example.com', password: 'secret' }),
/已重试 2 次,仍未成功/ /已重试 2 次,仍未成功/
); );
assert.equal(events.cleanupCalls, 1);
assert.equal(events.refreshCalls, 3); assert.equal(events.refreshCalls, 3);
assert.equal(events.sendCalls, 3); assert.equal(events.sendCalls, 3);
assert.equal(events.completed, 0); assert.equal(events.completed, 0);
}); });
test('step 6 can skip pre-login cleanup during step 7 recovery replay', async () => {
const events = {
cleanupCalls: 0,
completedPayloads: [],
};
const executor = api.createStep6Executor({
addLog: async () => {},
completeStepFromBackground: async (step, payload) => {
events.completedPayloads.push({ step, payload });
},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
isStep6RecoverableResult: () => false,
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
reuseOrCreateTab: async () => {},
runPreStep6CookieCleanup: async () => {
events.cleanupCalls += 1;
},
sendToContentScriptResilient: async () => ({
step6Outcome: 'success',
loginVerificationRequestedAt: 123,
}),
shouldSkipLoginVerificationForCpaCallback: () => false,
skipLoginVerificationStepsForCpaCallback: async () => {},
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await executor.executeStep6(
{ email: 'user@example.com', password: 'secret' },
{ skipPreLoginCleanup: true }
);
assert.equal(events.cleanupCalls, 0);
assert.deepStrictEqual(events.completedPayloads, [
{
step: 6,
payload: { loginVerificationRequestedAt: 123 },
},
]);
});
+14 -14
View File
@@ -4,17 +4,17 @@ const fs = require('node:fs');
const source = fs.readFileSync('background/steps/fetch-login-code.js', 'utf8'); const source = fs.readFileSync('background/steps/fetch-login-code.js', 'utf8');
const globalScope = {}; const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); const api = new Function('self', `${source}; return self.MultiPageBackgroundStep8;`)(globalScope);
test('step 7 refreshes CPA oauth via step 6 replay before submitting verification code', async () => { test('step 8 refreshes CPA oauth via step 7 replay before submitting verification code', async () => {
const calls = { const calls = {
ensureReady: 0, ensureReady: 0,
executeStep6: [], executeStep7: 0,
sleep: [], sleep: [],
resolveOptions: null, resolveOptions: null,
}; };
const executor = api.createStep7Executor({ const executor = api.createStep8Executor({
addLog: async () => {}, addLog: async () => {},
chrome: { chrome: {
tabs: { tabs: {
@@ -23,12 +23,12 @@ test('step 7 refreshes CPA oauth via step 6 replay before submitting verificatio
}, },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {}, confirmCustomVerificationStepBypass: async () => {},
ensureStep7VerificationPageReady: async () => { ensureStep8VerificationPageReady: async () => {
calls.ensureReady += 1; calls.ensureReady += 1;
return { state: 'verification_page' }; return { state: 'verification_page' };
}, },
executeStep6: async (_state, options = {}) => { executeStep7: async () => {
calls.executeStep6.push(options); calls.executeStep7 += 1;
}, },
getMailConfig: () => ({ getMailConfig: () => ({
provider: 'qq', provider: 'qq',
@@ -61,7 +61,7 @@ test('step 7 refreshes CPA oauth via step 6 replay before submitting verificatio
throwIfStopped: () => {}, throwIfStopped: () => {},
}); });
await executor.executeStep7({ await executor.executeStep8({
email: 'user@example.com', email: 'user@example.com',
password: 'secret', password: 'secret',
oauthUrl: 'https://oauth.example/latest', oauthUrl: 'https://oauth.example/latest',
@@ -69,15 +69,15 @@ test('step 7 refreshes CPA oauth via step 6 replay before submitting verificatio
assert.equal(typeof calls.resolveOptions.beforeSubmit, 'function'); assert.equal(typeof calls.resolveOptions.beforeSubmit, 'function');
assert.equal(calls.ensureReady, 2); assert.equal(calls.ensureReady, 2);
assert.deepStrictEqual(calls.executeStep6, [{ skipPreLoginCleanup: true }]); assert.equal(calls.executeStep7, 1);
assert.deepStrictEqual(calls.sleep, [1200]); assert.deepStrictEqual(calls.sleep, [1200]);
assert.equal(calls.resolveOptions.resendIntervalMs, 25000); assert.equal(calls.resolveOptions.resendIntervalMs, 25000);
}); });
test('step 7 disables resend interval for 2925 mailbox polling', async () => { test('step 8 disables resend interval for 2925 mailbox polling', async () => {
let capturedOptions = null; let capturedOptions = null;
const executor = api.createStep7Executor({ const executor = api.createStep8Executor({
addLog: async () => {}, addLog: async () => {},
chrome: { chrome: {
tabs: { tabs: {
@@ -86,8 +86,8 @@ test('step 7 disables resend interval for 2925 mailbox polling', async () => {
}, },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {}, confirmCustomVerificationStepBypass: async () => {},
ensureStep7VerificationPageReady: async () => ({ state: 'verification_page' }), ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
executeStep6: async () => {}, executeStep7: async () => {},
getMailConfig: () => ({ getMailConfig: () => ({
provider: '2925', provider: '2925',
label: '2925 邮箱', label: '2925 邮箱',
@@ -116,7 +116,7 @@ test('step 7 disables resend interval for 2925 mailbox polling', async () => {
throwIfStopped: () => {}, throwIfStopped: () => {},
}); });
await executor.executeStep7({ await executor.executeStep8({
email: 'user@example.com', email: 'user@example.com',
password: 'secret', password: 'secret',
oauthUrl: 'https://oauth.example/latest', oauthUrl: 'https://oauth.example/latest',
+2 -1
View File
@@ -10,7 +10,7 @@ test('step definitions module exposes ordered shared step metadata', () => {
const steps = api.getSteps(); const steps = api.getSteps();
assert.equal(Array.isArray(steps), true); assert.equal(Array.isArray(steps), true);
assert.equal(steps.length >= 9, true); assert.equal(steps.length >= 10, true);
assert.deepStrictEqual( assert.deepStrictEqual(
steps.map((step) => step.order), steps.map((step) => step.order),
steps.map((step) => step.order).slice().sort((left, right) => left - right) steps.map((step) => step.order).slice().sort((left, right) => left - right)
@@ -23,6 +23,7 @@ test('step definitions module exposes ordered shared step metadata', () => {
'fill-password', 'fill-password',
'fetch-signup-code', 'fetch-signup-code',
'fill-profile', 'fill-profile',
'clear-login-cookies',
'oauth-login', 'oauth-login',
'fetch-login-code', 'fetch-login-code',
'confirm-oauth', 'confirm-oauth',
+3 -3
View File
@@ -203,7 +203,7 @@ function setStep8PendingReject(handler) {
${helperBundle} ${helperBundle}
${step8ModuleSource} ${step8ModuleSource}
const executor = self.MultiPageBackgroundStep8.createStep8Executor({ const executor = self.MultiPageBackgroundStep9.createStep9Executor({
addLog, addLog,
chrome, chrome,
cleanupStep8NavigationListeners, cleanupStep8NavigationListeners,
@@ -237,7 +237,7 @@ const executor = self.MultiPageBackgroundStep8.createStep8Executor({
}); });
return { return {
executeStep8: executor.executeStep8, executeStep9: executor.executeStep9,
requestStop, requestStop,
resolveTabId(tabId) { resolveTabId(tabId) {
if (!resolveTabId) { if (!resolveTabId) {
@@ -263,7 +263,7 @@ return {
`)(step8ModuleSource); `)(step8ModuleSource);
(async () => { (async () => {
const step8Promise = api.executeStep8({ oauthUrl: 'https://example.com/oauth' }); const step8Promise = api.executeStep9({ oauthUrl: 'https://example.com/oauth' });
const settledStep8Promise = step8Promise.catch((err) => err); const settledStep8Promise = step8Promise.catch((err) => err);
await new Promise((resolve) => setImmediate(resolve)); await new Promise((resolve) => setImmediate(resolve));
+1 -1
View File
@@ -192,7 +192,7 @@ return {
}, },
}); });
await api.handleStepData(9, { localhostUrl: codexCallbackUrl }); await api.handleStepData(10, { localhostUrl: codexCallbackUrl });
let snapshot = api.snapshot(); let snapshot = api.snapshot();
assert.deepStrictEqual(snapshot.removedBatches, [[1], [2]]); assert.deepStrictEqual(snapshot.removedBatches, [[1], [2]]);
assert.strictEqual(snapshot.currentState.tabRegistry['signup-page'], null); assert.strictEqual(snapshot.currentState.tabRegistry['signup-page'], null);
+3 -3
View File
@@ -35,12 +35,12 @@ test('verification flow extends 2925 polling window', () => {
}); });
const step4Payload = helpers.getVerificationPollPayload(4, { email: 'user@example.com', mailProvider: '2925' }); const step4Payload = helpers.getVerificationPollPayload(4, { email: 'user@example.com', mailProvider: '2925' });
const step7Payload = helpers.getVerificationPollPayload(7, { email: 'user@example.com', mailProvider: '2925' }); const step8Payload = helpers.getVerificationPollPayload(8, { email: 'user@example.com', mailProvider: '2925' });
assert.equal(step4Payload.maxAttempts, 15); assert.equal(step4Payload.maxAttempts, 15);
assert.equal(step4Payload.intervalMs, 15000); assert.equal(step4Payload.intervalMs, 15000);
assert.equal(step7Payload.maxAttempts, 15); assert.equal(step8Payload.maxAttempts, 15);
assert.equal(step7Payload.intervalMs, 15000); assert.equal(step8Payload.intervalMs, 15000);
}); });
test('verification flow runs beforeSubmit hook before filling the code', async () => { test('verification flow runs beforeSubmit hook before filling the code', async () => {
+42 -16
View File
@@ -245,9 +245,9 @@
补充行为: 补充行为:
- `2925` provider 会拉长单轮轮询窗口,并关闭 Step 4 / 7 的自动重发间隔,减少因邮件延迟过早判负。 - `2925` provider 会拉长单轮轮询窗口,并关闭 Step 4 / 8 的自动重发间隔,减少因邮件延迟过早判负。
- Hotmail 在 Step 4 / 7 首轮轮询时会优先使用最近一次验证码请求时间作为时间窗口;如果中途触发重发,轮询窗口也会同步前移,避免旧邮件反复命中。 - Hotmail 在 Step 4 / 8 首轮轮询时会优先使用最近一次验证码请求时间作为时间窗口;如果中途触发重发,轮询窗口也会同步前移,避免旧邮件反复命中。
- CPA 模式下,Step 7 在真正回填登录验证码前,会先刷新最新 OAuth 地址并快速重走一次 Step 6,降低验证码等待过久导致 OAuth 过期的概率。 - CPA 模式下,Step 8 在真正回填登录验证码前,会先刷新最新 OAuth 地址并快速重走一次 Step 7,降低验证码等待过久导致 OAuth 过期的概率。
### Step 5 ### Step 5
@@ -266,22 +266,48 @@
文件: 文件:
- [background/steps/clear-login-cookies.js](c:/Users/projectf/Downloads/codex注册扩展/background/steps/clear-login-cookies.js)
流程:
1. 等待登录前清理延迟
2. 直接删除 ChatGPT / OpenAI cookies
3. 必要时用 `browsingData` 补扫 cookies
4. 完成后进入后续登录链路
### Step 7
文件:
- [background/steps/oauth-login.js](c:/Users/projectf/Downloads/codex注册扩展/background/steps/oauth-login.js) - [background/steps/oauth-login.js](c:/Users/projectf/Downloads/codex注册扩展/background/steps/oauth-login.js)
流程: 流程:
1. 清理登录前 Cookie 1. 通过 CPA / SUB2API 刷新 OAuth 地址
2. 通过 CPA / SUB2API 刷新 OAuth 地址 2. 打开最新 OAuth 链接
3. 打开最新 OAuth 链接 3. 登录
4. 登录 4. 确保真正进入验证码页
5. 确保真正进入验证码页 5. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次
6. 如果未进入验证码页,则按可恢复逻辑最多重试 3 次 6. 自动运行一旦进入步骤 7 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 7 重新开始授权流程
7. 自动运行一旦进入步骤 6 之后的链路,若后续步骤报错且认证页未进入 `https://auth.openai.com/add-phone`,则统一回到步骤 6 重新开始授权流程
### Step 8 ### Step 8
文件: 文件:
- [background/steps/fetch-login-code.js](c:/Users/projectf/Downloads/codex注册扩展/background/steps/fetch-login-code.js)
流程:
1. 确认当前认证页已经进入登录验证码页
2. 打开邮箱页或 API 轮询入口
3. 轮询登录验证码
4. 回填登录验证码
5. 如遇邮箱轮询类失败,则按有限次数回到 Step 7 重试
### Step 9
文件:
- [background/steps/confirm-oauth.js](c:/Users/projectf/Downloads/codex注册扩展/background/steps/confirm-oauth.js) - [background/steps/confirm-oauth.js](c:/Users/projectf/Downloads/codex注册扩展/background/steps/confirm-oauth.js)
流程: 流程:
@@ -291,7 +317,7 @@
3. 尝试多轮点击“继续” 3. 尝试多轮点击“继续”
4. 一旦捕获 localhost callback,写入状态并完成步骤 4. 一旦捕获 localhost callback,写入状态并完成步骤
### Step 9 ### Step 10
文件: 文件:
@@ -408,9 +434,9 @@
2. 计算是否从中断点继续 2. 计算是否从中断点继续
3. 每轮执行前重置必要运行态 3. 每轮执行前重置必要运行态
4. 执行 `runAutoSequenceFromStep` 4. 执行 `runAutoSequenceFromStep`
- 步骤 6 内部仍保留登录态恢复的有限重试 - 步骤 7 内部仍保留登录态恢复的有限重试
- 一旦进入步骤 6~9,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 6 无限重开 - 一旦进入步骤 7~10,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 7 无限重开
- 如果是手动停止,则立即退出自动流程,不会再触发“回到步骤 6 重开” - 如果是手动停止,则立即退出自动流程,不会再触发“回到步骤 7 重开”
5. 如果失败,根据设置决定: 5. 如果失败,根据设置决定:
- 立即停止 - 立即停止
- 当前轮重试 - 当前轮重试
@@ -468,5 +494,5 @@
- 新增共享恢复层:`content/auth-page-recovery.js` - 新增共享恢复层:`content/auth-page-recovery.js`
- Step 4 在等待注册验证码页时,如果命中认证页 `Try again / 重试` 页,会先自动点击 `重试` 恢复,再继续回到密码页重提和验证码页确认流程。 - Step 4 在等待注册验证码页时,如果命中认证页 `Try again / 重试` 页,会先自动点击 `重试` 恢复,再继续回到密码页重提和验证码页确认流程。
- Step 6 在识别到登录超时报错页时,会先尝试自动点击 `重试` 恢复当前页面,再按原有有限重试逻辑重新执行整步。 - Step 7 在识别到登录超时报错页时,会先尝试自动点击 `重试` 恢复当前页面,再按原有有限重试逻辑重新执行整步。
- Step 8 在点击 OAuth 同意页 `继续` 后,会额外检查是否进入认证页重试页;若命中则先自动点击 `重试` 恢复,再重新执行当前轮的 `继续` 点击。 - Step 9 在点击 OAuth 同意页 `继续` 后,会额外检查是否进入认证页重试页;若命中则先自动点击 `重试` 恢复,再重新执行当前轮的 `继续` 点击。
+140 -177
View File
@@ -1,8 +1,8 @@
# 妞ゅ湱娲伴弬鍥︽缂佹挻鐎拠瀛樻 # 项目文件结构说明
閺堫剚鏋冨锝呭灙閸戝搫缍嬮崜宥勭波鎼存挷鑵戦幍鈧張澶嗏偓婊堟姜韫囩晫鏆愰弬鍥︽閳ユ繐绱濋獮鎯邦嚛閺勫孩鐦℃稉顏呮瀮娴犲墎娈戞担婊呮暏閵? 本文档列出当前仓库中所有“非忽略文件”,并说明每个文件的作用。
娑撳秶鎾奸崗銉︽拱濞撳懎宕熼惃鍕嫹閻c儳娲拌ぐ鏇窗 不纳入本清单的忽略目录:
- `.github/` - `.github/`
- `_metadata/` - `_metadata/`
@@ -10,207 +10,170 @@
- `.vscode/` - `.vscode/`
- `.git/` - `.git/`
閺囧瓨鏌婄憴鍕灟閿? 更新规则:
- 閺傛澘顤冮妴浣稿灩闂勩們鈧線鍣搁崨钘夋倳闂堢偛鎷烽悾銉︽瀮娴犺泛鎮楅敍灞界箑妞よ鎮撳銉︽纯閺傜増婀伴弬鍥︽閵? - 新增、删除、重命名非忽略文件后,必须同步更新本文件。
- 婵″倹鐏夐弬鍥︽閼卞矁鐭楅崣鎴犳晸閺勫孩妯夐崣妯哄閿涘奔绡冭箛鍛淬€忛崥灞绢劄閺囧瓨鏌婇張顒佹瀮娴犳湹鑵戦惃鍕嚛閺勫簺鈧? - 如果文件职责发生明显变化,也必须同步更新本文件中的说明。
- 本文档不保留“增量补充”“结构补充”一类追加章节;结构变化时,直接更新对应文件条目与职责说明。
## 閺嶅湱娲拌ぐ? ## 根目录
- `.gitignore`閿涙艾鐣炬稊澶夌波鎼存挸鎷烽悾銉潐閸掓瑱绱濊ぐ鎾冲韫囩晫鏆?`docs/md/`閵嗕梗.github/`閵嗕梗_metadata/`閵嗕梗.vscode/` 缁涘娲拌ぐ鏇樷偓? - `.gitignore`:定义仓库忽略规则,当前忽略 `docs/md/``.github/``_metadata/``.vscode/` 等目录。
- `LICENSE`閿涙岸銆嶉惄顔款啅閸欘垵鐦夐弬鍥︽閵? - `LICENSE`:项目许可证文件。
- `README.md`閿涙岸娼伴崥鎴滃▏閻劏鈧懐娈戞い鍦窗娴犲绮涢妴浣哥暔鐟佸懓顕╅弰搴涒偓浣藉厴閸旀稒绔婚崡鏇氱瑢閹垮秳缍旈幐鍥х穿閵? - `README.md`:面向使用者的项目介绍、安装说明、能力清单与操作指引。
- `background.js`閿涙碍澧跨仦鏇炴倵閸?Service Worker 閸忋儱褰涙竟绛圭礉鐠愮喕鐭楀Ο鈥虫健鐟佸懘鍘ら妴浣稿灥婵瀵查妴浣稿弿鐏炩偓鐢悂鍣洪妴浣哥毌闁插繋绻氶悾娆戞畱妫板棗鐓欓崙鑺ユ殶娑撳氦绻嶇悰灞藉弳閸欙絻鈧? - `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口。
- `cloudflare-temp-email-utils.js`閿涙loudflare Temp Email 閻╃鍙ч惃鍕嚱瀹搞儱鍙块崙鑺ユ殶閿涘矁绀嬬拹?URL閵嗕礁鐓欓崥宥冣偓渚€鍋栨禒璺哄敶鐎归€涚瑢 MIME 閺佺増宓佽ぐ鎺嶇閸栨牓鈧? - `cloudflare-temp-email-utils.js`Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。
- `hotmail-utils.js`閿涙otmail 鐠愶箑褰挎稉搴ㄧ崣鐠囦胶鐖滈幓鎰絿閻╃鍙ч惃鍕嚱瀹搞儱鍙块崙鑺ユ殶閿涘矁绀嬬拹锝堝閸欓鐡柅澶堚偓渚€鐛欑拠浣虹垳閸栧綊鍘ら妴浣侯儑娑撳鏌熼幒銉ュ經閺佺増宓佽ぐ鎺嶇閸栨牓鈧? - `hotmail-utils.js`Hotmail 账号与验证码提取相关的纯工具函数,负责账号筛选、验证码匹配、第三方接口数据归一化。
- `icloud-utils.js`閿涙Cloud 闂呮劗顫嗛柇顔绢唸閻╃鍙ч惃鍕嚱瀹搞儱鍙块崙鑺ユ殶閿涘矁绀嬬拹?host閵嗕礁鍩嗛崥宥呭灙鐞涖劊鈧椒绻氶悾娆戝Ц閹降鈧礁鍑¢悽銊уЦ閹胶鐡戣ぐ鎺嶇閸栨牓鈧? - `icloud-utils.js`iCloud 隐私邮箱相关的纯工具函数,负责 host、别名列表、保留状态、已用状态等归一化。
- `luckmail-utils.js`閿涙瓈uckMail 閻╃鍙ч惃鍕嚱瀹搞儱鍙块崙鑺ユ殶閿涘矁绀嬬拹锝夊仏缁犺精鍠樻稊鎷岊唶瑜版洏鈧焦鐖g粵淇扁偓渚€鍋栨禒?cursor閵嗕線鐛欑拠浣虹垳閸栧綊鍘ょ粵澶婄秺娑撯偓閸栨牓鈧? - `luckmail-utils.js`LuckMail 相关的纯工具函数,负责邮箱购买记录、标签、邮件 cursor、验证码匹配等归一化。
- `manifest.json`閿涙hrome 閹碘晛鐫嶅〒鍛礋閿涘苯锛愰弰搴㈡綀闂勬劑鈧浇鍎楅弲顖濆壖閺堫兙鈧椒鏅舵潏瑙勭埉閵嗕礁鍞寸€圭鍓奸張顑跨瑢鐟欏嫬鍨梿鍡愨偓? - `managed-alias-utils.js`:共享 Gmail / 2925 的别名邮箱规则,负责解析基邮箱、校验“当前完整注册邮箱”是否仍与基邮箱兼容、生成 Gmail `+tag` 与 2925 随机后缀邮箱,并输出 sidepanel 可复用的 UI 文案。
- `microsoft-email.js`閿涙瓉icrosoft Graph / Outlook 闁喕娆㈢拠璇插絿鏉堝懎濮Ο鈥虫健閿涘矁绀嬬拹锝呭煕閺傞鎶ら悧灞惧床 token閵嗕線鍋栫粻鍗炪仚鏉烆喛顕楅崪宀勭崣鐠囦胶鐖滈幓鎰絿閵? - `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集。
- `package.json`閿涙矮绮ㄦ惔鎾存付鐏?Node 閸栧懘鍘ょ純顕嗙礉閻╊喖澧犳稉鏄忣洣閹绘劒绶靛ù瀣槸閼存碍婀扮€规矮绠熼妴? - `microsoft-email.js`Microsoft Graph / Outlook 邮件读取辅助模块,负责刷新令牌换 token、邮箱夹轮询和验证码提取。
- `rules.json`閿涙岸娼ら幀?DNR 鐟欏嫬鍨敍灞煎瘜鐟曚礁顦╅悶?iCloud 閻╃鍙х拠閿嬬湴婢舵番鈧? - `package.json`:仓库最小 Node 包配置,目前主要提供测试脚本定义。
- `start-hotmail-helper.bat`閿涙瓙indows 娑撳鎯庨崝銊︽拱閸?helper 閻ㄥ嫯鍓奸張顒婄幢瑜版挸澧犻弮銏℃箛閸斺€茬艾 Hotmail 閺堫剙婀撮弨鏈典繆閿涘奔绡冮張宥呭娴滃氦澶勯崣鐤箥鐞涘苯宸婚崣鍙夋瀮閺堫剛鏆€濡楋絻鈧? - `rules.json`:静态 DNR 规则,主要处理 iCloud 相关请求头。
- `start-hotmail-helper.command`閿涙acOS 娑撳鎯庨崝銊︽拱閸?helper 閻ㄥ嫯鍓奸張顒婄幢瑜版挸澧犻弮銏℃箛閸斺€茬艾 Hotmail 閺堫剙婀撮弨鏈典繆閿涘奔绡冮張宥呭娴滃氦澶勯崣鐤箥鐞涘苯宸婚崣鍙夋瀮閺堫剛鏆€濡楋絻鈧? - `start-hotmail-helper.bat`Windows 下启动本地 helper 的脚本;当前既服务于 Hotmail 本地收信,也服务于账号运行历史文本留档。
- `瀵偓閸欐垼鈧寬I瀵偓閸欐垳绗孭R閹绘劒姘﹀ù浣衡柤.md`閿涙矮绮ㄦ惔鎾跺箛閺堝娈?AI 瀵偓閸欐垳绗?PR 閹绘劒姘﹀ù浣衡柤鐠囧瓨妲戦妴? - `start-hotmail-helper.command`macOS 下启动本地 helper 的脚本;当前既服务于 Hotmail 本地收信,也服务于账号运行历史文本留档。
- `妞ゅ湱娲伴弬鍥︽缂佹挻鐎拠瀛樻.md`閿涙艾缍嬮崜宥嗘瀮娴犺绱濈紒瀛樺Б閺佺繝閲滄禒鎾崇氨闂堢偛鎷烽悾銉︽瀮娴犲墎娈戠紒鎾寸€稉搴や捍鐠愶絿鍌ㄥ鏇樷偓? - `开发者AI开发与PR提交流程.md`:仓库现有的 AI 开发与 PR 提交流程说明。
- `妞ゅ湱娲扮€瑰本鏆i柧鎹愮熅鐠囧瓨妲?md`閿涙岸娼伴崥?AI/瀵偓閸欐垼鈧懐娈戠€瑰本鏆i崝鐔诲厴闁炬崘鐭剧拠瀛樻閿涘瞼鏁ゆ禍搴℃彥闁喓鎮婄憴锝囬兇缂佺喐鏆f担鎾圭箥鐞涘矁绻冪粙瀣ㄢ偓? - `项目文件结构说明.md`:当前文件,维护整个仓库非忽略文件的结构与职责索引。
- `妞ゅ湱娲板鈧崣鎴n潐閼煎喛绱橝I閸楀繋缍旈敍?md`閿涙岸娼伴崥?AI/瀵偓閸欐垼鈧懐娈戞い鍦窗瀵偓閸欐垼顫夐懠鍐︹偓浣哄閺夌喍绗岄崣妯绘纯濡偓閺屻儲绔婚崡鏇樷偓? - `项目完整链路说明.md`:面向 AI/开发者的完整功能链路说明,用于快速理解系统整体运行过程。
- `项目开发规范(AI协作).md`:面向 AI/开发者的项目开发规范、约束与变更检查清单。
## `background/` ## `background/`
- `background/account-run-history.js`閿涙俺澶勯崣鐤箥鐞涘苯宸婚崣鍙壞侀崸妤嬬礉鐠愮喕鐭楃亸鍡樺灇閸?婢惰精瑙?閸嬫粍顒涚紒鎾寸亯閹镐椒绠欓崠鏍у煂 `chrome.storage.local`閿涘苯鑻熼崷銊ユ儙閻劎瀚粩瀣畱閺堫剙婀?txt 閻f瑦銆傞柊宥囩枂閸氬氦鎷烽崝鐘插晸閸忋儲鏋冮張顒佹)韫囨ぜ鈧? - `background/account-run-history.js`:账号运行历史模块,负责将成功、失败、停止结果持久化到 `chrome.storage.local`,并在启用独立本地 txt 留档配置后追加写入文本日志。
- `background/auto-run-controller.js`閿涙俺鍤滈崝銊ㄧ箥鐞涘奔瀵岄幒褍鍩楅崳顭掔礉鐏忎浇顥婃径姘崇枂閹笛嗩攽閵嗕線鍣哥拠鏇樷偓浣界枂濞嗏剝鎲崇憰浣碘偓浣哄殠缁嬪妫块梾鏂剧瑢閸婃帟顓搁弮鑸典划婢跺秹鈧槒绶妴? - `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;fresh-attempt reset 时会额外保留 `gmailBaseEmail``mail2925BaseEmail`,避免自动流程重置后丢失别名基邮箱配置。
- `background/generated-email-helpers.js`閿涙氨鏁撻幋鎰板仏缁犺精绶熼崝鈺佺湴閿涘苯鐨濈憗?Duck閵嗕竼loudflare閵嗕竼loudflare Temp Email閵嗕巩Cloud 闂呮劗顫嗛柇顔绢唸閻ㄥ嫯骞忛崣鏍偓鏄忕帆閵? - `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口。
- `background/logging-status.js`閿涙艾鎮楅崣鐗堟)韫囨ぜ鈧焦顒炴銈囧Ц閹降鈧線鏁婄拠顖欎繆閹垰鎷伴懟銉ュ叡閻樿埖鈧礁鍨介弬顓犳畱閸忣剙鍙″銉ュ徔鐏炲倶鈧? - `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层。
- `background/message-router.js`閿涙艾鎮楅崣鐗堢Х閹垵鐭鹃悽鍗炵湴閿涘矁绀嬬拹锝咁槱閻?`chrome.runtime.onMessage` 鏉╂稑鍙嗛惃鍕閺堝绗熼崝鈩冪Х閹垬鈧? - `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息。
- `background/navigation-utils.js`閿涙艾顕遍懜顏冪瑢 URL 閸掋倖鏌囧銉ュ徔鐏炲偊绱濈拹鐔荤煑 callback閵嗕礁鍙嗛崣锝夈€夐妴涓哖A/SUB2API 閸︽澘娼冮妴浣诡劄妤犮倛鐑︽潪顒傛祲閸忓啿鍨介弬顓溾偓? - `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API 地址、步骤跳转相关判断。
- `background/panel-bridge.js`閿涙PA / SUB2API 闂堛垺婢樺銉﹀复鐏炲偊绱濈亸浣筋棅 OAuth 閸︽澘娼冮懢宄板絿閹碘偓闂団偓閻ㄥ嫰銆夐棃銏″ⅵ瀵偓閵嗕浇鍓奸張顒佹暈閸忋儱鎷伴柅姘繆閵? - `background/panel-bridge.js`CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。
- `background/signup-flow-helpers.js`閿涙碍鏁為崘宀勩€夋潏鍛И鐏炲偊绱濈拹鐔荤煑閹垫挸绱戝▔銊ュ斀閸忋儱褰涢妴浣虹搼瀵板懎鐦戦惍渚€銆夋禒銉ュ挤鐟欙絾鐎借ぐ鎾冲濞翠胶鈻奸幍鈧悽銊╁仏缁犱究鈧? - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail / 2925 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成。
- `background/tab-runtime.js`閿涙碍鐖g粵楣冦€夋稉搴″敶鐎圭鍓奸張顒冪箥鐞涘本妞傞崺铏诡攨鐠佺偓鏌﹂敍灞界殱鐟佸懏鐖g粵鐐暈閸愬被鈧礁鍟跨粣浣圭閻炲棎鈧焦绉烽幁顖濈Т閺冭翰鈧焦鏁為崗銉╁櫢鐠囨洑绗岄梼鐔峰灙閵? - `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列。
- `background/verification-flow.js`閿涙碍鏁為崘?閻ц缍嶆宀冪槈閻礁鍙℃禍顐gウ缁嬪鐪伴敍灞界殱鐟佸懘鍣搁崣鎴欌偓浣界枂鐠囶潿鈧焦褰佹禍銈冣偓浣搞亼鐠愩儱娲栭柅鈧妴浣藉殰鐎规矮绠熼柇顔绢唸鐠哄疇绻冩禒銉ュ挤 2925 闂€鑳枂鐠囥垹寮弫鑸偓? - `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过以及 2925 长轮询参数。
## `background/steps/` ## `background/steps/`
- `background/steps/confirm-oauth.js`閿涙碍顒炴?8 鐎圭偟骞囬敍宀冪鐠?OAuth 閸氬本鍓版い鍨瘻闁筋喖鐣炬担宥冣偓浣哄仯閸戞眹鈧勾ocalhost 閸ョ偠鐨熼惄鎴濇儔娑撳骸娲栫拫鍐ㄧ暚閹存劑鈧? - `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。
- `background/steps/fetch-login-code.js`閿涙碍顒炴?7 鐎圭偟骞囬敍宀冪鐠愶絿娅ヨぐ鏇㈢崣鐠囦胶鐖滈梼鑸殿唽閻ㄥ嫰鍋栫粻杈枂鐠囶澀绗岄崶鐐衡偓鈧幒褍鍩楅妴? - `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成。
- `background/steps/fetch-signup-code.js`閿涙碍顒炴?4 鐎圭偟骞囬敍宀冪鐠愶絾鏁為崘宀勭崣鐠囦胶鐖滈梼鑸殿唽閻ㄥ嫰銆夐棃銏犲櫙婢跺洣绗屾宀冪槈閻焦绁︾粙瀣弳閸欙絻鈧? - `background/steps/fetch-login-code.js`:步骤 8 实现,负责登录验证码阶段的邮箱轮询、验证码回填与回退控制。
- `background/steps/fill-password.js`閿涙碍顒炴?3 鐎圭偟骞囬敍宀冪鐠愶絽鐦戦惍浣烘晸閹存劑鈧椒绻氱€涙ǜ鈧礁娲栨繅顐$瑢閹绘劒姘﹂妴? - `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口。
- `background/steps/fill-profile.js`閿涙碍顒炴?5 鐎圭偟骞囬敍宀冪鐠愶絽顫橀崥宥冣偓浣烘晸閺冦儱锝為崘娆忚嫙閹跺﹨绁弬娆愬絹娴溿倗绮板▔銊ュ斀妞ら潧鍞寸€圭鍓奸張顑锯偓? - `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。
- `background/steps/oauth-login.js`閿涙碍顒炴?6 鐎圭偟骞囬敍宀冪鐠愶絽鍩涢弬?OAuth 闁剧偓甯撮妴浣烘瑜版洖鎷扮涵顔荤箽鏉╂稑鍙嗘宀冪槈閻線銆夐妴? - `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。
- `background/steps/open-chatgpt.js`閿涙碍顒炴?1 鐎圭偟骞囬敍宀冪鐠愶絾澧﹀鈧?ChatGPT 鐎规缍夐獮鍓佲€樼拋銈呭弳閸欙絽姘ㄧ紒顏傗偓? - `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页。
- `background/steps/platform-verify.js`閿涙碍顒炴?9 鐎圭偟骞囬敍宀冪鐠?CPA / SUB2API 閸ョ偠鐨熸宀冪槈閵? - `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
- `background/steps/registry.js`閿涙碍顒炴銈嗘暈閸愬矁銆冨銉ュ范閿涘矁绀嬬拹锝囨暏缁嬪啿鐣鹃惃鍕劄妤犮倕鍘撻弫鐗堝祦閺勭姴鐨犻崚鐗堝⒔鐞涘苯娅掗妴? - `background/steps/platform-verify.js`:步骤 10 实现,负责 CPA / SUB2API 回调验证。
- `background/steps/submit-signup-email.js`閿涙碍顒炴?2 鐎圭偟骞囬敍宀冪鐠愶絾鏁為崘灞藉弳閸欙絿鍋i崙姹団偓渚€鍋栫粻杈ㄥ絹娴溿倓绗岄幓鎰唉閸氬氦鎯ら崷浼淬€夐崚鍡樻暜閸掋倖鏌囬妴? - `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与提交后落地页分支判断。
## `content/` ## `content/`
- `content/activation-utils.js`閿涙艾鍞寸€圭鍓奸張顒勨偓姘辨暏濠碘偓濞茶崵鐡ラ悾銉ヤ紣閸忓嚖绱濈拹鐔荤煑閹稿鎸抽悙鐟板毊閺傜懓绱¢崚銈嗘焽娑?Step 9 閸欘垱浠径宥呫亼鐠愩儲鏋冨鍫濆灲閺傤厹鈧? - `content/activation-utils.js`:内容脚本通用激活策略工具,负责按钮点击方式判断与平台回调可恢复失败文案判断。
- `content/duck-mail.js`閿涙uckDuckGo Email Protection 妞ょ敻娼伴懘姘拱閿涘矁绀嬬拹锝囨晸閹存劖鍨ㄧ拠璇插絿 `@duck.com` 閸︽澘娼冮妴? - `content/auth-page-recovery.js`:认证页共享恢复模块,统一封装 `Try again / 重试` 页识别、点击重试和等待退出重试页;当前由注册验证码等待、OAuth 登录恢复、OAuth 同意页重试恢复链路复用。
- `content/gmail-mail.js`閿涙mail 闁喚顔堟潪顔款嚄閼存碍婀伴敍宀冪鐠愶絽婀?Gmail 妞ょ敻娼版稉顓炲爱闁板秹鐛欑拠浣虹垳闁喕娆㈤妴? - `content/duck-mail.js`DuckDuckGo Email Protection 页面脚本,负责生成或读取 `@duck.com` 地址。
- `content/icloud-mail.js`閿涙Cloud 闁喚顔堟い鐢告桨閼存碍婀伴敍宀冪鐠愶絽婀?iCloud Mail 妞ょ敻娼版稉顓☆嚢閸欐牠鍋栨禒鎯邦嚊閹懎鎷版宀冪槈閻降鈧? - `content/gmail-mail.js`:Gmail 邮箱轮询脚本,负责在 Gmail 页面中匹配验证码邮件。
- `content/inbucket-mail.js`閿涙nbucket 闁喚顔堟潪顔款嚄閼存碍婀伴敍宀冪鐠愶絽婀?Inbucket 妞ょ敻娼版稉顓☆嚢閸?閸掔娀娅庢宀冪槈閻線鍋栨禒韬测偓? - `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
- `content/mail-163.js`閿?63 / 163 VIP 闁喚顔堟潪顔款嚄閼存碍婀伴敍宀冪鐠愶絿缍夋い鐢稿仏缁犻亶鐛欑拠浣虹垳鐠囪褰囬崪宀勫仏娴犺埖绔婚悶鍡愨偓? - `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。
- `content/mail-2925.js`閿?925 闁喚顔堟い鐢告桨閼存碍婀伴敍宀冪鐠?2925 闁喚顔堥弨鏈垫鏉烆喛顕楁稉搴ㄧ崣鐠囦胶鐖滈崠褰掑帳閵? - `content/mail-163.js`163 / 163 VIP 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。
- `content/qq-mail.js`閿涙瓐Q 闁喚顔堟潪顔款嚄閼存碍婀伴敍宀冪鐠愶絿缍夋い鐢稿仏缁犻亶鐛欑拠浣虹垳鐠囪褰囬妴? - `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱收件轮询与验证码匹配。
- `content/signup-page.js`閿涙碍鏁為崘?閻ц缍?閹哄牊娼堟稉璇插敶鐎圭鍓奸張顒婄礉鐠愮喕鐭?OpenAI / ChatGPT 妞ょ敻娼版稉濠勬畱濮濄儵顎冮幍褑顢戦妴? - `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
- `content/sub2api-panel.js`閿涙瓔UB2API 閸氬骸褰撮崘鍛啇閼存碍婀伴敍宀冪鐠愶綀骞忛崣?OAuth 閸︽澘娼冮崪灞惧絹娴?localhost 閸ョ偠鐨熼妴? - `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行。
- `content/utils.js`閿涙艾鍞寸€圭鍓奸張顒€鍙曢崗鍗炰紣閸忓嘲鐪伴敍宀冪鐠愶絾妫╄箛妞尖偓涓稥ADY/COMPLETE/ERROR 娑撳﹥濮ら妴浣稿帗缁辩姷鐡戝鍛偓浣界翻閸忋儰绗岄悙鐟板毊閵? - `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;当前承接步骤 10。
- `content/vps-panel.js`閿涙PA 闂堛垺婢橀崘鍛啇閼存碍婀伴敍宀冪鐠愶綀骞忛崣?OAuth 閸︽澘娼冮妴浣瑰絹娴溿倕娲栫拫?URL閿涘苯鑻熼崺杞扮艾缁墽鈥橀幋鎰瀵拌姤鐖f稉搴ㄦ晩鐠囶垱鈧礁浠?Step 9 閸掋倕鐣鹃妴? - `content/utils.js`:内容脚本公共工具层,负责日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击。
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做步骤 10 判定。
## `data/` ## `data/`
- `data/names.js`閿涙岸娈㈤張鍝勵潣閸氬秲鈧胶鏁撻弮銉х搼濞村鐦弫鐗堝祦濠ф劑鈧? - `data/names.js`:随机姓名、生日等测试数据源。
- `data/step-definitions.js`閿涙艾鍙℃禍顐n劄妤犮倕鍘撻弫鐗堝祦閿涘苯澧犻崥搴″酱閸忓崬鎮撴担璺ㄦ暏閿涘瞼鏁ゆ禍搴″З閹焦瑕嗛弻鎾虫嫲濮濄儵顎冨▔銊ュ斀閵? - `data/step-definitions.js`:共享步骤元数据,前后台共同使用,用于动态渲染和步骤注册。
## `docs/` ## `docs/`
- `docs/Hotmail閸欏本膩瀵繘鈧倿鍘ゅ鈧崣鎴炴煙濡楀牆寮峰鈧崣鎴n唶瑜?md`閿涙otmail 閸欏本膩瀵繑甯撮崗銉ф畱閸樺棗褰堕弬瑙勵攳閸滃苯绱戦崣鎴n唶瑜版洏鈧? - `docs/仓库协作者AI分析PR与合并标准流程.md`:仓库协作者进行 AI 分析 PR 与合并时的流程说明。
- `docs/娴犳挸绨遍崡蹇庣稊閼板寬I閸掑棙鐎絇R娑撳骸鎮庨獮鑸电垼閸戝棙绁︾粙?md`閿涙矮绮ㄦ惔鎾冲礂娴f粏鈧懓绻樼悰?AI 閸掑棙鐎?PR 娑撳骸鎮庨獮鑸垫閻ㄥ嫭绁︾粙瀣嚛閺勫簺鈧? - `docs/images/交流群.jpg`:README 中展示的交流群图片资源。
- `docs/images/娴溿倖绁︾紘?jpg`閿涙瓓EADME 娑擃厼鐫嶇粈铏规畱娴溿倖绁︾紘銈呮禈閻楀洩绁┃鎰┾偓? - `docs/images/十轮自动.png`:README 中展示的自动运行效果图。
- `docs/images/閸椾浇鐤嗛懛顏勫З.png`閿涙瓓EADME 娑擃厼鐫嶇粈铏规畱閼奉亜濮╂潻鎰攽閺佸牊鐏夐崶淇扁偓? - `docs/images/微信.png`README 中展示的微信收款码图片。
- `docs/images/瀵邦喕淇?png`閿涙瓓EADME 娑擃厼鐫嶇粈铏规畱瀵邦喕淇婇弨鑸殿儥閻礁娴橀悧鍥モ偓? - `docs/images/支付宝.jpg`:README 中展示的支付宝收款码图片。
- `docs/images/閺€顖欑帛鐎?jpg`閿涙瓓EADME 娑擃厼鐫嶇粈铏规畱閺€顖欑帛鐎规繃鏁瑰▎鍓х垳閸ュ墽澧栭妴? - `docs/refactor/2026-04-16-architecture-refactor-plan.md`:本轮重构阶段的结构设计与迁移计划记录。
- `docs/refactor/2026-04-16-architecture-refactor-plan.md`閿涙碍婀版潪顕€鍣搁弸鍕▉濞堢數娈戠紒鎾寸€拋鎹愵吀娑撳氦绺肩粔鏄忣吀閸掓帟顔囪ぐ鏇樷偓? - `docs/superpowers/plans/2026-04-10-hotmail-oauth-mail-pool.md`Hotmail OAuth 邮箱池实现计划文档。
- `docs/superpowers/plans/2026-04-10-hotmail-oauth-mail-pool.md`閿涙otmail OAuth 闁喚顔堝Ч鐘茬杽閻滄媽顓搁崚鎺撴瀮濡楋絻鈧? - `docs/superpowers/specs/2026-04-10-hotmail-oauth-design.md`Hotmail OAuth + Graph Mail 的设计规格文档。
- `docs/superpowers/specs/2026-04-10-hotmail-oauth-design.md`閿涙otmail OAuth + Graph Mail 閻ㄥ嫯顔曠拋陇顫夐弽鍏兼瀮濡楋絻鈧?
## `icons/` ## `icons/`
- `icons/icon128.png`閿涙碍澧跨仦?128 閸嶅繒绀岄崶鐐垼閵? - `icons/icon128.png`:扩展 128 像素图标。
- `icons/icon16.png`閿涙碍澧跨仦?16 閸嶅繒绀岄崶鐐垼閵? - `icons/icon16.png`:扩展 16 像素图标。
- `icons/icon48.png`閿涙碍澧跨仦?48 閸嶅繒绀岄崶鐐垼閵? - `icons/icon48.png`:扩展 48 像素图标。
## `scripts/` ## `scripts/`
- `scripts/hotmail_helper.py`閿涙碍婀伴崷?helper 閺堝秴濮熼敍宀冪鐠愶綁鈧俺绻冮張顒€婀撮幒銉ュ經閸楀繐濮?Hotmail 閼惧嘲褰囬柇顔绘閸滃矂鐛欑拠浣虹垳閿涘苯鑻熼幓鎰返鐠愶箑褰挎潻鎰攽閸樺棗褰堕弬鍥ㄦ拱鏉╄棄濮為幒銉ュ經閵? - `scripts/hotmail_helper.py`:本地 helper 服务,负责通过本地接口协助 Hotmail 获取邮件和验证码,并提供账号运行历史文本追加接口。
## `sidepanel/` ## `sidepanel/`
- `sidepanel/hotmail-manager.js`閿涙矮鏅舵潏瑙勭埉 Hotmail 鐠愶箑褰垮Ч鐘殿吀閻炲棗娅掗敍宀冪鐠愶絽鍨悰銊b偓浣割嚤閸忋儯鈧線鐛欑拠浣碘偓浣圭ゴ鐠囨洘鏁规穱鈥虫嫲閹靛綊鍣洪幙宥勭稊閵? - `sidepanel/hotmail-manager.js`:侧边栏 Hotmail 账号池管理器,负责列表、导入、验证、测试收信和批量操作。
- `sidepanel/icloud-manager.js`閿涙矮鏅舵潏瑙勭埉 iCloud 闂呮劗顫嗛柇顔绢唸缁狅紕鎮婇崳顭掔礉鐠愮喕鐭楅崚妤勩€冮妴浣虹摣闁鈧椒绻氶悾娆嶁偓浣稿灩闂勩倕鎷伴幍褰掑櫤閹垮秳缍旈妴? - `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。
- `sidepanel/luckmail-manager.js`閿涙矮鏅舵潏瑙勭埉 LuckMail 缁狅紕鎮婇崳顭掔礉鐠愮喕鐭楅柇顔绢唸閸掓銆冮妴浣虹摣闁鈧礁鎯庨崑婧库偓浣风箽閻f瑤绗岄幍褰掑櫤閹垮秳缍旈妴? - `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
- `sidepanel/sidepanel.css`閿涙矮鏅舵潏瑙勭埉閺嶅嘲绱¢弬鍥︽閵? - `sidepanel/sidepanel.css`:侧边栏样式文件。
- `sidepanel/sidepanel.html`閿涙矮鏅舵潏瑙勭埉妞ょ敻娼扮紒鎾寸€敍灞界秼閸撳秵顒炴銈呭灙鐞涖劌鍑¢弨閫涜礋閸斻劍鈧礁顔愰崳顭掔礉楠炶泛婀弮銉ョ箶閸栫儤鐖f0妯圭瑓閺傚綊顣╅悾娆掑閸欑柉绻嶇悰灞藉坊閸欏弶鎲崇憰浣规蒋鐢负鈧? - `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区标题下方预留账号运行历史摘要条带,且会加载 `managed-alias-utils.js`,把旧的“邮箱前缀”字段语义改为“别名基邮箱”。
- `sidepanel/sidepanel.js`閿涙矮鏅舵潏瑙勭埉娑撹鍙嗛崣锝堝壖閺堫剨绱濈拹鐔荤煑 UI 閻樿埖鈧礁鎮撳銉b偓浣稿З閹焦顒炴銈嗚閺屾挶鈧焦瀵滈柦顔绘唉娴滄帇鈧浇澶勯崣鐤箥鐞涘苯宸婚崣鍙夋喅鐟曚焦瑕嗛弻鎾扁偓浣哄缁?txt 閻f瑦銆傞柊宥囩枂娑撳骸绠嶉幘顓熷复閺€韬测偓? - `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、账号运行历史摘要渲染、独立 txt 留档配置与广播接收;同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上。
- `sidepanel/update-service.js`閿涙矮鏅舵潏瑙勭埉閺囧瓨鏌婂Λ鈧弻銉︽箛閸斺槄绱濈拹鐔荤煑 GitHub Releases 閺屻儴顕楁稉搴e閺堫剙鐫嶇粈鎭掆偓? - `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询与版本展示。
## `tests/` ## `tests/`
## 2026-04-17 增量补充 - `tests/activation-utils.test.js`:测试内容脚本激活策略与平台回调可恢复错误判断。
- `tests/auth-page-recovery.test.js`:测试认证页共享恢复层的重试页识别与恢复行为。
### 新增共享模块 - `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文,并补充 `gmailBaseEmail` / `mail2925BaseEmail` 在 fresh reset 后仍被保留的回归验证。
- `tests/auto-run-step6-restart.test.js`:测试自动运行在后半段授权链路遇错时会回到步骤 7 重开,并在命中 add-phone 时停止重开。
- `managed-alias-utils.js` - `tests/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。
共享 Gmail / 2925 的别名邮箱规则,负责: - `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。
- 解析基邮箱 - `tests/background-account-run-history-module.test.js`:测试账号运行历史模块已接入、导出工厂,并能在不启用本地 helper 时正确持久化记录。
- 校验“当前完整注册邮箱”是否仍与基邮箱兼容 - `tests/background-account-history-settings.test.js`:测试账号运行历史的独立配置项归一化,不再复用 Hotmail 模式作为启停条件。
- 生成 Gmail `+tag` 与 2925 随机后缀邮箱 - `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂。
- 输出 sidepanel 可复用的 UI 文案 - `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。
- `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析。
### 本轮受影响文件 - `tests/background-logging-status-module.test.js`:测试日志 / 状态模块已接入且导出工厂。
- `tests/background-luckmail.test.js`:测试 LuckMail 相关后台逻辑,如购买、复用、标记已用与重置。
- `background/generated-email-helpers.js` - `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。
现在除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口 - `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑
- `tests/background-navigation-utils-module.test.js`:测试导航工具模块已接入且导出工厂。
- `background/signup-flow-helpers.js` - `tests/background-panel-bridge-module.test.js`:测试面板桥接模块已接入且导出工厂。
在 Gmail / 2925 模式下,会优先复用已经存在且仍兼容的完整注册邮箱;只有不兼容或为空时才重新生成 - `tests/background-signup-flow-module.test.js`:测试注册页辅助模块已接入且导出工厂
- `tests/background-signup-step2-branching.test.js`:测试在 Gmail / 2925 模式下,已有兼容别名邮箱时应直接复用,不应再次重生成。
- `background/auto-run-controller.js` - `tests/background-step-modules.test.js`:测试步骤模块文件都已由后台入口加载。
fresh-attempt reset 时,额外保留 `gmailBaseEmail``mail2925BaseEmail`,避免自动流程重置后丢失别名基邮箱配置 - `tests/background-step5-submit-short-circuit.test.js`:测试步骤 5 会把生成好的资料直接转发给内容脚本,并依赖完成信号收尾
- `tests/background-step6-retry-limit.test.js`:测试步骤 6 的 Cookie 清理,以及步骤 7 的有限重试上限。
- `sidepanel/sidepanel.js` - `tests/background-step7-recovery.test.js`:测试步骤 8 在 CPA 模式下会先回放步骤 7 再提交登录验证码,并为 2925 关闭重发间隔。
现在把 Gmail / 2925 的“基邮箱输入”“完整注册邮箱输入”“自动生成按钮”“兼容性校验”统一接到共享别名逻辑上 - `tests/background-step-registry.test.js`:测试后台步骤注册表和共享步骤定义已接入
- `tests/background-tab-runtime-module.test.js`:测试标签运行时模块已接入且导出工厂。
- `sidepanel/sidepanel.html` - `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。
加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱” - `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑
- `tests/cloudflare-temp-email-utils.test.js`:测试 Cloudflare Temp Email 工具层的 URL、域名、邮件解析逻辑。
### 新增/补充测试 - `tests/hotmail-api-mode.test.js`:测试 Hotmail API 模式相关文案和集成方式。
- `tests/hotmail-cors-headers.test.js`:测试 Microsoft token 请求相关 DNR 配置。
- `tests/managed-alias-utils.test.js` - `tests/hotmail-utils.test.js`:测试 Hotmail 工具层的账号筛选、验证码提取和消息匹配逻辑。
覆盖 Gmail / 2925 共享别名工具的解析、生成、兼容性判断 - `tests/icloud-mail-content.test.js`:测试 iCloud Mail 内容脚本读取邮件正文和选中状态逻辑
- `tests/icloud-utils.test.js`:测试 iCloud 工具层的 host、别名列表、保留状态与筛选逻辑。
- `tests/background-signup-step2-branching.test.js` - `tests/luckmail-utils.test.js`:测试 LuckMail 工具层的购买记录、邮件、游标和验证码匹配逻辑。
补充“已有兼容别名邮箱时应直接复用,不应再次重生成”的行为验证 - `tests/managed-alias-utils.test.js`:覆盖 Gmail / 2925 共享别名工具的解析、生成、兼容性判断
- `tests/microsoft-email.test.js`:测试 Microsoft 邮件拉取与验证码提取逻辑。
- `tests/auto-run-fresh-attempt-reset.test.js` - `tests/sidepanel-hotmail-manager.test.js`:测试侧边栏 Hotmail 管理器模块接线与空态渲染。
补充 `gmailBaseEmail` / `mail2925BaseEmail` 在自动流程 fresh reset 后仍被保留的回归验证 - `tests/sidepanel-account-run-history.test.js`:测试侧边栏日志区下方的账号运行历史条带结构、独立留档配置控件与状态分类摘要逻辑
- `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。
- `tests/activation-utils.test.js`閿涙碍绁寸拠鏇炲敶鐎圭鍓奸張顒佺负濞茶崵鐡ラ悾銉ょ瑢 Step 9 閸欘垱浠径宥夋晩鐠囶垰鍨介弬顓溾偓? - `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析逻辑。
- `tests/auto-run-fresh-attempt-reset.test.js`閿涙碍绁寸拠鏇″殰閸斻劏绻嶇悰灞芥躬閺傞绔存潪顔肩磻婵澧犳导姘跺櫢缂冾喗妫潻鎰攽閺冩湹绗傛稉瀣瀮閵? - `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
- `tests/auto-run-step6-restart.test.js`閿涙碍绁寸拠鏇″殰閸斻劏绻嶇悰灞芥躬濮濄儵顎?6 娑斿鎮楅柆鍥晩閺冩湹绱伴崶鐐插煂濮濄儵顎?6 闁插秴绱戦敍灞借嫙閸︺劌鎳℃稉?add-phone 閺冭泛浠犲銏ゅ櫢瀵偓閵? - `tests/signup-entry-diagnostics.test.js`:测试注册入口诊断快照输出。
- `tests/auto-step-random-delay.test.js`閿涙碍绁寸拠鏇″殰閸斻劏绻嶇悰灞绢劄闂傛潙娆㈡潻鐔剁瑢閺冄囧帳缂冾噣鏁崗鐓庮啇鐟欙絾鐎介妴? - `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
- `tests/background-auto-run-module.test.js`閿涙碍绁寸拠鏇″殰閸斻劏绻嶇悰灞惧付閸掕泛娅掑Ο鈥虫健瀹稿弶甯撮崗銉ょ瑬鐎电厧鍤銉ュ范閵? - `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。
- `tests/background-account-run-history-module.test.js`閿涙碍绁寸拠鏇″閸欑柉绻嶇悰灞藉坊閸欏弶膩閸ф鍑¢幒銉ュ弳閵嗕礁顕遍崙鍝勪紣閸樺偊绱濋獮鎯板厴閸︺劋绗夐崥顖滄暏閺堫剙婀?helper 閺冭埖顒滅涵顔藉瘮娑斿懎瀵茬拋鏉跨秿閵? - `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报。
- `tests/background-account-history-settings.test.js`閿涙碍绁寸拠鏇″閸欑柉绻嶇悰灞藉坊閸欒尙娈戦悪顒傜彌闁板秶鐤嗘い鐟扮秺娑撯偓閸栨牭绱濇稉宥呭晙婢跺秶鏁?Hotmail 濡€崇础娴f粈璐熼崥顖氫粻閺夆€叉閵? - `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。
- `tests/background-generated-email-module.test.js`閿涙碍绁寸拠鏇犳晸閹存劙鍋栫粻杈窡閸斺晜膩閸ф鍑¢幒銉ュ弳娑撴柨顕遍崙鍝勪紣閸樺倶鈧? - `tests/step6-login-state.test.js`:测试 OAuth 登录状态识别逻辑,当前对应步骤 7 链路。
- `tests/background-icloud.test.js`閿涙碍绁寸拠?iCloud 閻╃鍙ч崥搴″酱缁绢垰鍤遍弫棰佺瑢閸掝偄鎮曢弨璺虹啲闁槒绶妴? - `tests/step6-timeout-recovery.test.js`:测试 OAuth 登录超时报错页的可恢复结果构造,当前对应步骤 7 链路。
- `tests/background-icloud-mail-provider.test.js`閿涙碍绁寸拠?iCloud 闁喚顔?provider 闁板秶鐤嗙憴锝嗙€介妴? - `tests/step8-callback-handling.test.js`:测试 localhost 回调地址捕获逻辑,当前对应步骤 9。
- `tests/background-logging-status-module.test.js`閿涙碍绁寸拠鏇熸)韫?閻樿埖鈧焦膩閸ф鍑¢幒銉ュ弳娑撴柨顕遍崙鍝勪紣閸樺倶鈧? - `tests/step8-debugger-stop.test.js`:测试调试器点击在 Stop 场景下的中止行为,当前对应步骤 9。
- `tests/background-luckmail.test.js`閿涙碍绁寸拠?LuckMail 閻╃鍙ч崥搴″酱闁槒绶敍灞筋洤鐠愵厺鎷遍妴浣割槻閻劊鈧焦鐖g拋鏉垮嚒閻劋绗岄柌宥囩枂閵? - `tests/step8-retry-page-recovery.test.js`:测试 OAuth 同意页点击后的重试页恢复分支,当前对应步骤 9。
- `tests/background-message-router-module.test.js`閿涙碍绁寸拠鏇熺Х閹垵鐭鹃悽杈侀崸妤€鍑¢幒銉ュ弳娑撴柨顕遍崙鍝勪紣閸樺倶鈧? - `tests/step8-state-timeout-retry.test.js`:测试认证页通信错误是否可判定为可重试,当前对应步骤 9。
- `tests/background-message-router-step2-skip.test.js`閿涙碍绁寸拠鏇燁劄妤?2 閻╁瓨甯撮拃钘夊煂妤犲矁鐦夐惍渚€銆夐弮鍓佹畱鐠鸿櫕顒炴稉搴eЦ閹椒绻氶幎銈夆偓鏄忕帆閵? - `tests/step8-stop-cleanup.test.js`:测试 OAuth 确认流程在 Stop 后能正确清理监听器与挂起状态,当前对应步骤 9。
- `tests/background-navigation-utils-module.test.js`閿涙碍绁寸拠鏇烆嚤閼割亜浼愰崗閿嬆侀崸妤€鍑¢幒銉ュ弳娑撴柨顕遍崙鍝勪紣閸樺倶鈧? - `tests/step9-cpa-mode.test.js`:测试本地 CPA 平台回调跳过策略判断,当前作用于步骤 10。
- `tests/background-panel-bridge-module.test.js`閿涙碍绁寸拠鏇㈡桨閺夋寧藟閹恒儲膩閸ф鍑¢幒銉ュ弳娑撴柨顕遍崙鍝勪紣閸樺倶鈧? - `tests/step9-localhost-cleanup-scope.test.js`:测试 localhost callback 与路径前缀残留页的精确清理范围,当前对应步骤 10 收尾。
- `tests/background-signup-flow-module.test.js`閿涙碍绁寸拠鏇熸暈閸愬矂銆夋潏鍛И濡€虫健瀹稿弶甯撮崗銉ょ瑬鐎电厧鍤銉ュ范閵? - `tests/step9-status-diagnostics.test.js`:测试平台回调成功判定、红色错误态过滤,以及成功徽标与失败提示并存时的优先级,当前对应步骤 10。
- `tests/background-step-modules.test.js`閿涙碍绁寸拠鏇燁劄妤犮倖膩閸ф鏋冩禒鍫曞厴瀹歌尙鏁遍崥搴″酱閸忋儱褰涢崝鐘烘祰閵? - `tests/verification-stop-propagation.test.js`:测试验证码流程在 Stop 场景下的错误传播与不中途降级。
- `tests/background-step5-submit-short-circuit.test.js`閿涙碍绁寸拠?Step 5 娴兼碍濡搁悽鐔稿灇婵傜晫娈戠挧鍕灐閻╁瓨甯存潪顒€褰傜紒娆忓敶鐎圭鍓奸張顒婄礉楠炴湹绶风挧鏍х暚閹存劒淇婇崣閿嬫暪鐏忎勘鈧? - `tests/verification-flow-polling.test.js`:测试 2925 长轮询参数,以及验证码提交流程中的 `beforeSubmit` 钩子执行顺序。
- `tests/signup-entry-diagnostics.test.js`:测试 Step 1 / Step 2 注册入口识别失败时的诊断快照摘要。
- `tests/step3-direct-complete.test.js`锛氭祴璇?Step 3 浼氬厛鍥炰紶瀹屾垚杞借嵎锛屽啀寮傛瑙﹀彂琛ㄥ崟鎻愪氦锛岄伩鍏嶉〉闈㈣烦杞崱浣忎富娴佺▼銆?
- `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`閿涙碍绁寸拠鏇炴倵閸欑増顒炴銈嗘暈閸愬矁銆冮崪灞藉彙娴滎偅顒炴銈呯暰娑斿鍑¢幒銉ュ弳閵?
- `tests/background-tab-runtime-module.test.js`閿涙碍绁寸拠鏇熺垼缁涙崘绻嶇悰灞炬濡€虫健瀹稿弶甯撮崗銉ょ瑬鐎电厧鍤銉ュ范閵?
- `tests/background-verification-flow-module.test.js`閿涙碍绁寸拠鏇㈢崣鐠囦胶鐖滃ù浣衡柤濡€虫健瀹稿弶甯撮崗銉ょ瑬鐎电厧鍤銉ュ范閵?
- `tests/cloudflare-temp-email-provider.test.js`閿涙碍绁寸拠?Cloudflare Temp Email provider 閻ㄥ嫯鐤嗙拠顫瑢閻╊喗鐖i柇顔绢唸闁瀚ㄩ柅鏄忕帆閵?
- `tests/cloudflare-temp-email-utils.test.js`閿涙碍绁寸拠?Cloudflare Temp Email 瀹搞儱鍙跨仦鍌滄畱 URL閵嗕礁鐓欓崥宥冣偓渚€鍋栨禒鎯靶掗弸鎰扳偓鏄忕帆閵?
- `tests/hotmail-api-mode.test.js`閿涙碍绁寸拠?Hotmail API 濡€崇础閻╃鍙ч弬鍥攳閸滃矂娉﹂幋鎰煙瀵繈鈧?
- `tests/hotmail-cors-headers.test.js`閿涙碍绁寸拠?Microsoft token 鐠囬攱鐪伴惄绋垮彠 DNR 闁板秶鐤嗛妴?
- `tests/hotmail-utils.test.js`閿涙碍绁寸拠?Hotmail 瀹搞儱鍙跨仦鍌滄畱鐠愶箑褰跨粵娑⑩偓澶堚偓渚€鐛欑拠浣虹垳閹绘劕褰囬崪灞剧Х閹垰灏柊宥夆偓鏄忕帆閵?
- `tests/icloud-mail-content.test.js`閿涙碍绁寸拠?iCloud Mail 閸愬懎顔愰懘姘拱鐠囪褰囬柇顔绘濮濓絾鏋冮崪宀勨偓澶夎厬閻樿埖鈧線鈧槒绶妴?
- `tests/icloud-utils.test.js`閿涙碍绁寸拠?iCloud 瀹搞儱鍙跨仦鍌滄畱 host閵嗕礁鍩嗛崥宥呭灙鐞涖劊鈧椒绻氶悾娆戝Ц閹椒绗岀粵娑⑩偓澶愨偓鏄忕帆閵?
- `tests/luckmail-utils.test.js`閿涙碍绁寸拠?LuckMail 瀹搞儱鍙跨仦鍌滄畱鐠愵厺鎷辩拋鏉跨秿閵嗕線鍋栨禒韬测偓浣圭埗閺嶅洤鎷版宀冪槈閻礁灏柊宥夆偓鏄忕帆閵?
- `tests/microsoft-email.test.js`閿涙碍绁寸拠?Microsoft 闁喕娆㈤幏澶婂絿娑撳酣鐛欑拠浣虹垳閹绘劕褰囬柅鏄忕帆閵?
- `tests/sidepanel-hotmail-manager.test.js`閿涙碍绁寸拠鏇氭櫠鏉堣鐖?Hotmail 缁狅紕鎮婇崳銊δ侀崸妤佸复缁惧じ绗岀粚鐑樷偓浣硅閺屾挶鈧?
- `tests/sidepanel-account-run-history.test.js`閿涙碍绁寸拠鏇氭櫠鏉堣鐖弮銉ョ箶閸栬桨绗呴弬鍦畱鐠愶箑褰挎潻鎰攽閸樺棗褰堕弶鈥崇敨缂佹挻鐎妴浣哄缁斿鏆€濡楋綁鍘ょ純顔藉付娴犳湹绗岄悩鑸碘偓浣稿瀻缁粯鎲崇憰渚€鈧槒绶妴?
- `tests/sidepanel-icloud-manager.test.js`閿涙碍绁寸拠鏇氭櫠鏉堣鐖?iCloud 缁狅紕鎮婇崳銊δ侀崸妤佸复缁惧じ绗岀粚鐑樷偓浣硅閺屾挶鈧?
- `tests/sidepanel-icloud-provider.test.js`閿涙碍绁寸拠鏇氭櫠鏉堣鐖?iCloud 閻ц缍嶉崷鏉挎絻鐟欙絾鐎介柅鏄忕帆閵?
- `tests/sidepanel-luckmail-manager.test.js`閿涙碍绁寸拠鏇氭櫠鏉堣鐖?LuckMail 缁狅紕鎮婇崳銊δ侀崸妤佸复缁惧じ绗岀粚鐑樷偓浣硅閺屾挶鈧?
- `tests/signup-page-tab-cleanup.test.js`閿涙碍绁寸拠鏇熸暈閸愬矂銆夐弶銉︾爱閺嶅洨顒烽惃鍕暱缁愪焦绔婚悶鍡涒偓鏄忕帆閵?
- `tests/step-definitions-module.test.js`閿涙碍绁寸拠鏇炲彙娴滎偅顒炴銈呯暰娑斿膩閸ф寮锋笟褑绔熼弽蹇氬壖閺堫剙濮炴潪浠嬨€庢惔蹇嬧偓?
- `tests/step5-direct-complete.test.js`閿涙碍绁寸拠?Step 5 閸︺劏绁弬娆撱€夐悙鐟板毊閹绘劒姘﹂崥搴g彌閸楀啿鐣幋鎰秼閸撳秵顒炴銈冣偓?
- `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 閸︾儤娅欐稉瀣畱娑擃厽顒涚悰灞艰礋閵?
- `tests/step8-state-timeout-retry.test.js`閿涙碍绁寸拠?Step 8 闁矮淇婇柨娆掝嚖閺勵垰鎯侀崣顖氬灲鐎规矮璐熼崣顖炲櫢鐠囨洏鈧?
- `tests/step8-stop-cleanup.test.js`閿涙碍绁寸拠?Step 8 閸?Stop 閸氬氦鍏樺锝団€樺〒鍛倞閻╂垵鎯夐崳銊ょ瑢閹稿倽鎹i悩鑸碘偓浣碘偓?
- `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` 闁解晛鐡欓幍褑顢戞い鍝勭碍閵?
## 2026-04 结构补充
- `content/auth-page-recovery.js`
- 认证页共享恢复模块。
- 统一封装 `Try again / 重试` 页识别、点击重试、等待退出重试页。
- 当前由 Step 4 / Step 6 / Step 8 复用。