diff --git a/README.md b/README.md
index 122562e..7cf5f51 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,12 @@
一百五十个号,一个401:
-进官网,获取最新交流群:
-
- |
+ |
+
+ |
+
|
@@ -74,7 +75,6 @@
- 至少准备一种验证码接收方式:
- DuckDuckGo `@duck.com` + QQ / 163 / Inbucket 转发
- Cloudflare 自定义域邮箱前缀 + QQ / 163 / Inbucket 转发
- - iCloud Hide My Email,可选择直接从 iCloud 收件箱收码,或转发到 QQ / 163 / 163 VIP / 126 / Gmail 后收码
- 手动填写一个可收信邮箱
- 如果使用 `QQ` / `163` / `163 VIP` / `126` / `Inbucket`,对应页面需要提前能正常打开
@@ -592,7 +592,6 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
支持:
- `Hotmail`(远程服务 / 本地助手)
-- `iCloud` 收件箱,或 iCloud Hide My Email 转发到 QQ / 163 / 163 VIP / 126 / Gmail 后收码
- `content/qq-mail.js`
- `content/mail-163.js`(163 / 163 VIP / 126)
- `content/inbucket-mail.js`
@@ -774,7 +773,7 @@ content/utils.js 通用工具:等待元素、点击、日志、停
content/vps-panel.js CPA 面板步骤:内部 OAuth 刷新 / Step 10
content/signup-page.js ChatGPT 官网 + OpenAI 注册/登录页步骤:Step 1 / 2 / 3 / 5 / 7 / 9
hotmail-utils.js Hotmail 收信相关通用辅助
-mail-provider-utils.js 网页邮箱 provider 与 iCloud 转发收码配置辅助
+mail-provider-utils.js 网页邮箱 provider 配置辅助
content/duck-mail.js Duck 邮箱自动获取
content/qq-mail.js QQ 邮箱验证码轮询
content/mail-163.js 163 / 163 VIP / 126 邮箱验证码轮询
diff --git a/background.js b/background.js
index 9ddb88f..b7fb329 100644
--- a/background.js
+++ b/background.js
@@ -551,6 +551,8 @@ const DEFAULT_STATE = {
lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。
lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。
localhostUrl: null, // 运行时捕获到的 localhost 回调地址,不要手动预填。
+ cpaOAuthState: null, // CPA OAuth state。
+ cpaManagementOrigin: null, // CPA 管理接口 origin。
sub2apiSessionId: null, // SUB2API OpenAI Auth 会话 ID。
sub2apiOAuthState: null, // SUB2API OpenAI Auth state。
sub2apiGroupId: null, // SUB2API 目标分组 ID。
@@ -6233,6 +6235,8 @@ function getDownstreamStateResets(step, state = {}) {
return {
...plusRuntimeResets,
oauthUrl: null,
+ cpaOAuthState: null,
+ cpaManagementOrigin: null,
sub2apiSessionId: null,
sub2apiOAuthState: null,
sub2apiGroupId: null,
@@ -7084,6 +7088,8 @@ async function handleStepData(step, payload) {
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
+ if (payload.cpaOAuthState !== undefined) updates.cpaOAuthState = payload.cpaOAuthState || null;
+ if (payload.cpaManagementOrigin !== undefined) updates.cpaManagementOrigin = payload.cpaManagementOrigin || null;
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
if (Object.keys(updates).length) {
diff --git a/background/contribution-oauth.js b/background/contribution-oauth.js
index 81a1131..3928dda 100644
--- a/background/contribution-oauth.js
+++ b/background/contribution-oauth.js
@@ -3,19 +3,15 @@
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundContributionOAuthModule() {
const API_BASE_URL = 'https://apikey.qzz.io/oauth/api';
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
- const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'expired', 'error']);
+ const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']);
const CALLBACK_FINAL_STATUSES = new Set(['submitted']);
const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']);
- const CONTRIBUTION_SOURCE_CPA = 'cpa';
- const CONTRIBUTION_SOURCE_SUB2API = 'sub2api';
- const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池';
- const CONTRIBUTION_SUB2API_PLUS_GROUP_NAME = 'openai-plus';
const RUNTIME_DEFAULTS = {
contributionMode: false,
contributionModeExpected: false,
- contributionSource: CONTRIBUTION_SOURCE_SUB2API,
- contributionTargetGroupName: CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME,
+ contributionSource: 'sub2api',
+ contributionTargetGroupName: 'codex号池',
contributionNickname: '',
contributionQq: '',
contributionSessionId: '',
@@ -44,7 +40,8 @@
} = deps;
let listenersBound = false;
- const inFlightCapturedCallbackTasks = new Map();
+ const pendingCallbackSubmissions = new Map();
+ const pendingCapturedCallbacks = new Map();
function normalizeString(value = '') {
return String(value || '').trim();
@@ -71,6 +68,9 @@
case 'auto_rejected':
case 'rejected':
return 'auto_rejected';
+ case 'manual_review_required':
+ case 'manual_review':
+ return 'manual_review_required';
case 'expired':
case 'timeout':
return 'expired';
@@ -111,63 +111,6 @@
return FINAL_STATUSES.has(normalizeContributionStatus(status));
}
- function runCapturedCallbackOnce(callbackUrl, executor) {
- const normalizedUrl = normalizeString(callbackUrl);
- if (!normalizedUrl) {
- return Promise.resolve().then(executor);
- }
-
- const existingTask = inFlightCapturedCallbackTasks.get(normalizedUrl);
- if (existingTask) {
- return existingTask;
- }
-
- let task = null;
- task = Promise.resolve()
- .then(executor)
- .finally(() => {
- if (inFlightCapturedCallbackTasks.get(normalizedUrl) === task) {
- inFlightCapturedCallbackTasks.delete(normalizedUrl);
- }
- });
- inFlightCapturedCallbackTasks.set(normalizedUrl, task);
- return task;
- }
-
- function normalizeContributionSource(value = '') {
- const normalized = normalizeString(value).toLowerCase();
- return normalized === CONTRIBUTION_SOURCE_SUB2API
- ? CONTRIBUTION_SOURCE_SUB2API
- : CONTRIBUTION_SOURCE_CPA;
- }
-
- function resolveContributionRouting(state = {}) {
- const currentStatus = normalizeContributionStatus(state.contributionStatus);
- const currentSource = normalizeContributionSource(state.contributionSource);
- const hasActiveSession = Boolean(
- normalizeString(state.contributionSessionId)
- && currentStatus
- && !FINAL_STATUSES.has(currentStatus)
- );
-
- if (hasActiveSession) {
- return {
- source: currentSource,
- targetGroupName: currentSource === CONTRIBUTION_SOURCE_SUB2API
- ? (normalizeString(state.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME)
- : '',
- };
- }
-
- const source = CONTRIBUTION_SOURCE_SUB2API;
- return {
- source,
- targetGroupName: Boolean(state.plusModeEnabled)
- ? CONTRIBUTION_SUB2API_PLUS_GROUP_NAME
- : (normalizeString(state.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME),
- };
- }
-
function getStatusLabel(status = '') {
switch (normalizeContributionStatus(status)) {
case 'started':
@@ -175,11 +118,13 @@
case 'waiting':
return '等待提交回调';
case 'processing':
- return '已提交回调,等待服务端确认';
+ return '已提交回调,等待 CPA 确认';
case 'auto_approved':
- return '贡献成功,服务端已确认';
+ return '贡献成功,CPA 已确认';
case 'auto_rejected':
return '贡献未通过确认';
+ case 'manual_review_required':
+ return '已提交,等待人工处理';
case 'expired':
return '贡献会话已超时';
case 'error':
@@ -315,6 +260,41 @@
return qq;
}
+ function isPlusModeState(state = {}) {
+ return Boolean(state?.plusModeEnabled);
+ }
+
+ function normalizeContributionModeSource(value = '') {
+ const normalized = normalizeString(value).toLowerCase();
+ return normalized === 'sub2api' ? 'sub2api' : 'cpa';
+ }
+
+ function resolveContributionModeRoutingState(state = {}) {
+ const currentStatus = normalizeString(state?.contributionStatus).toLowerCase();
+ const currentSource = normalizeContributionModeSource(state?.contributionSource);
+ const hasActiveSession = Boolean(
+ normalizeString(state?.contributionSessionId)
+ && currentStatus
+ && !FINAL_STATUSES.has(currentStatus)
+ );
+
+ if (hasActiveSession) {
+ return {
+ source: currentSource,
+ targetGroupName: currentSource === 'sub2api'
+ ? (normalizeString(state?.contributionTargetGroupName) || 'codex号池')
+ : '',
+ };
+ }
+
+ return {
+ source: 'sub2api',
+ targetGroupName: isPlusModeState(state)
+ ? 'openai-plus'
+ : (normalizeString(state?.contributionTargetGroupName) || 'codex号池'),
+ };
+ }
+
function buildStatusMessage(status, payload = {}) {
const label = getStatusLabel(status);
const details = [
@@ -494,68 +474,87 @@
return currentState;
}
- await applyRuntimeUpdates({
- contributionCallbackUrl: normalizedUrl,
- contributionCallbackStatus: 'submitting',
- contributionCallbackMessage: buildCallbackMessage('submitting'),
- });
-
- try {
- const payload = await fetchContributionJson('/submit-callback', {
- method: 'POST',
- body: {
- session_id: sessionId,
- callback_url: normalizedUrl,
- },
- });
-
- const nextStatus = 'submitted';
- await applyRuntimeUpdates({
- contributionCallbackUrl: normalizedUrl,
- contributionCallbackStatus: nextStatus,
- contributionCallbackMessage: buildCallbackMessage(nextStatus, payload),
- });
-
- if (typeof closeLocalhostCallbackTabs === 'function') {
- await closeLocalhostCallbackTabs(normalizedUrl).catch(() => {});
- }
-
- return await pollContributionStatus({ reason: options.reason || 'submit_callback' });
- } catch (error) {
- await applyRuntimeUpdates({
- contributionCallbackUrl: normalizedUrl,
- contributionCallbackStatus: 'failed',
- contributionCallbackMessage: `回调提交失败:${error.message}`,
- });
-
- if (typeof addLog === 'function') {
- await addLog(`贡献模式:回调提交失败:${error.message}`, 'warn');
- }
-
- throw error;
+ const dedupeKey = `${sessionId}::${normalizedUrl}`;
+ if (pendingCallbackSubmissions.has(dedupeKey)) {
+ return pendingCallbackSubmissions.get(dedupeKey);
}
+
+ const task = (async () => {
+ await applyRuntimeUpdates({
+ contributionCallbackUrl: normalizedUrl,
+ contributionCallbackStatus: 'submitting',
+ contributionCallbackMessage: buildCallbackMessage('submitting'),
+ });
+
+ try {
+ const payload = await fetchContributionJson('/submit-callback', {
+ method: 'POST',
+ body: {
+ session_id: sessionId,
+ callback_url: normalizedUrl,
+ },
+ });
+
+ const nextStatus = 'submitted';
+ await applyRuntimeUpdates({
+ contributionCallbackUrl: normalizedUrl,
+ contributionCallbackStatus: nextStatus,
+ contributionCallbackMessage: buildCallbackMessage(nextStatus, payload),
+ });
+
+ if (typeof closeLocalhostCallbackTabs === 'function') {
+ await closeLocalhostCallbackTabs(normalizedUrl).catch(() => {});
+ }
+
+ return await pollContributionStatus({ reason: options.reason || 'submit_callback' });
+ } catch (error) {
+ await applyRuntimeUpdates({
+ contributionCallbackUrl: normalizedUrl,
+ contributionCallbackStatus: 'failed',
+ contributionCallbackMessage: `回调提交失败:${error.message}`,
+ });
+
+ if (typeof addLog === 'function') {
+ await addLog(`贡献模式:回调提交失败:${error.message}`, 'warn');
+ }
+
+ throw error;
+ } finally {
+ pendingCallbackSubmissions.delete(dedupeKey);
+ }
+ })();
+
+ pendingCallbackSubmissions.set(dedupeKey, task);
+ return task;
}
async function handleCapturedCallback(rawUrl, metadata = {}) {
+ const currentState = await getState();
+ if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) {
+ return currentState;
+ }
+ if (!isContributionCallbackUrl(rawUrl, currentState)) {
+ return currentState;
+ }
+
const normalizedUrl = normalizeString(rawUrl);
- return runCapturedCallbackOnce(normalizedUrl, async () => {
- const currentState = await getState();
- if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) {
- return currentState;
- }
- if (!isContributionCallbackUrl(normalizedUrl, currentState)) {
- return currentState;
- }
-
- const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
- if (
- normalizedUrl
- && normalizeString(currentState.contributionCallbackUrl) === normalizedUrl
- && (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting')
- ) {
- return currentState;
- }
+ const callbackDedupeKey = `${normalizeString(currentState.contributionSessionId)}::${normalizedUrl}`;
+ if (pendingCapturedCallbacks.has(callbackDedupeKey)) {
+ return pendingCapturedCallbacks.get(callbackDedupeKey);
+ }
+ if (pendingCallbackSubmissions.has(callbackDedupeKey)) {
+ return pendingCallbackSubmissions.get(callbackDedupeKey);
+ }
+ const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
+ if (
+ normalizedUrl
+ && normalizeString(currentState.contributionCallbackUrl) === normalizedUrl
+ && (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting')
+ ) {
+ return currentState;
+ }
+ const task = (async () => {
await applyRuntimeUpdates({
contributionCallbackUrl: normalizedUrl,
contributionCallbackStatus: 'captured',
@@ -573,8 +572,13 @@
});
} catch {
return getState();
+ } finally {
+ pendingCapturedCallbacks.delete(callbackDedupeKey);
}
- });
+ })();
+
+ pendingCapturedCallbacks.set(callbackDedupeKey, task);
+ return task;
}
async function pollContributionStatus(options = {}) {
@@ -597,16 +601,6 @@
const callbackState = deriveCallbackState(mergedPayload, currentState);
const updates = {
contributionLastPollAt: Date.now(),
- contributionSource: normalizeContributionSource(
- mergedPayload.source
- || mergedPayload.source_kind
- || currentState.contributionSource
- ),
- contributionTargetGroupName: normalizeString(
- mergedPayload.target_group_name
- || mergedPayload.group_name
- || currentState.contributionTargetGroupName
- ),
contributionStatus: normalizedStatus,
contributionStatusMessage: buildStatusMessage(normalizedStatus, mergedPayload),
contributionCallbackUrl: callbackState.callbackUrl,
@@ -648,7 +642,6 @@
async function startContributionFlow(options = {}) {
const currentState = options.stateOverride || await getState();
const shouldOpenAuthTab = options.openAuthTab !== false;
- const routing = resolveContributionRouting(currentState);
if (!currentState.contributionMode) {
throw new Error('请先进入贡献模式。');
}
@@ -666,14 +659,15 @@
return pollContributionStatus({ reason: 'resume_existing' });
}
+ const routingState = resolveContributionModeRoutingState(currentState);
const payload = await fetchContributionJson('/start', {
method: 'POST',
body: {
nickname: buildNickname(currentState, options.nickname),
qq: buildContributionQq(currentState, options.qq),
email: normalizeString(currentState.email),
- source: routing.source,
- target_group_name: routing.targetGroupName,
+ source: routingState.source,
+ target_group_name: routingState.targetGroupName,
channel: 'codex-extension',
},
});
@@ -686,12 +680,6 @@
}
await applyRuntimeUpdates({
- contributionSource: normalizeContributionSource(payload.source || routing.source),
- contributionTargetGroupName: normalizeString(
- payload.target_group_name
- || payload.group_name
- || routing.targetGroupName
- ),
contributionSessionId: sessionId,
contributionAuthUrl: authUrl,
contributionAuthState: authState,
@@ -722,7 +710,7 @@
}
function onTabUpdated(tabId, changeInfo, tab) {
- const candidateUrl = normalizeString(changeInfo?.url);
+ const candidateUrl = normalizeString(changeInfo?.url || tab?.url);
if (!candidateUrl) {
return;
}
diff --git a/background/message-router.js b/background/message-router.js
index 1af516a..7ce7a6e 100644
--- a/background/message-router.js
+++ b/background/message-router.js
@@ -216,6 +216,27 @@
}
async function handleStepData(step, payload) {
+ if (step === 1) {
+ const updates = {};
+ if (payload.oauthUrl) {
+ updates.oauthUrl = payload.oauthUrl;
+ broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
+ }
+ if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null;
+ if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null;
+ if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
+ if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
+ if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
+ if (payload.cpaOAuthState !== undefined) updates.cpaOAuthState = payload.cpaOAuthState || null;
+ if (payload.cpaManagementOrigin !== undefined) updates.cpaManagementOrigin = payload.cpaManagementOrigin || null;
+ if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
+ if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
+ if (Object.keys(updates).length) {
+ await setState(updates);
+ }
+ return;
+ }
+
const stateForStep = await getState();
const stepKey = getStepKeyForState(step, stateForStep);
@@ -262,24 +283,6 @@
}
switch (step) {
- case 1: {
- const updates = {};
- if (payload.oauthUrl) {
- updates.oauthUrl = payload.oauthUrl;
- broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
- }
- if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null;
- if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null;
- if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
- if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
- if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
- if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
- if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
- if (Object.keys(updates).length) {
- await setState(updates);
- }
- break;
- }
case 2:
if (payload.email) {
await setEmailState(payload.email);
diff --git a/background/panel-bridge.js b/background/panel-bridge.js
index a667960..2b0f077 100644
--- a/background/panel-bridge.js
+++ b/background/panel-bridge.js
@@ -43,6 +43,78 @@
return message || `Codex2API 请求失败(HTTP ${responseStatus})。`;
}
+ function deriveCpaManagementOrigin(vpsUrl) {
+ const normalizedUrl = String(vpsUrl || '').trim();
+ if (!normalizedUrl) {
+ throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
+ }
+ let parsed;
+ try {
+ parsed = new URL(normalizedUrl);
+ } catch {
+ throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
+ }
+ return parsed.origin;
+ }
+
+ function getCpaApiErrorMessage(payload, responseStatus = 500) {
+ const candidates = [
+ payload?.error,
+ payload?.message,
+ payload?.detail,
+ payload?.reason,
+ ];
+ const message = candidates
+ .map((value) => String(value || '').trim())
+ .find(Boolean);
+ return message || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
+ }
+
+ async function fetchCpaManagementJson(origin, path, options = {}) {
+ const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
+
+ try {
+ const managementKey = String(options.managementKey || '').trim();
+ const headers = {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ };
+ if (managementKey) {
+ headers.Authorization = `Bearer ${managementKey}`;
+ headers['X-Management-Key'] = managementKey;
+ }
+
+ const response = await fetch(`${origin}${path}`, {
+ method: options.method || 'POST',
+ headers,
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
+ signal: controller.signal,
+ });
+
+ let payload = {};
+ try {
+ payload = await response.json();
+ } catch {
+ payload = {};
+ }
+
+ if (!response.ok) {
+ throw new Error(getCpaApiErrorMessage(payload, response.status));
+ }
+
+ return payload;
+ } catch (error) {
+ if (error?.name === 'AbortError') {
+ throw new Error('CPA 管理接口请求超时,请稍后重试。');
+ }
+ throw error;
+ } finally {
+ clearTimeout(timer);
+ }
+ }
+
async function fetchCodex2ApiJson(origin, path, options = {}) {
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
const controller = new AbortController();
@@ -97,49 +169,48 @@
if (!state.vpsUrl) {
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
}
-
- await addLog(`${logLabel}:正在打开 CPA 面板...`);
-
- const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
- await closeConflictingTabsForSource('vps-panel', state.vpsUrl);
-
- const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true });
- const tabId = tab.id;
- await rememberSourceLastUrl('vps-panel', state.vpsUrl);
-
- await addLog(`${logLabel}:CPA 面板已打开,正在等待页面进入目标地址...`);
- const matchedTab = await waitForTabUrlFamily('vps-panel', tabId, state.vpsUrl, {
- timeoutMs: 15000,
- retryDelayMs: 400,
- });
- if (!matchedTab) {
- await addLog(`${logLabel}:CPA 页面尚未完全进入目标地址,继续尝试连接内容脚本...`, 'warn');
+ const managementKey = String(state.vpsPassword || '').trim();
+ if (!managementKey) {
+ throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
}
- await ensureContentScriptReadyOnTab('vps-panel', tabId, {
- inject: injectFiles,
- timeoutMs: 45000,
- retryDelayMs: 900,
- logMessage: `${logLabel}:CPA 面板仍在加载,正在重试连接内容脚本...`,
+ const origin = deriveCpaManagementOrigin(state.vpsUrl);
+
+ await addLog(`${logLabel}:正在通过 CPA 管理接口获取 OAuth 授权链接...`);
+ const result = await fetchCpaManagementJson(origin, '/v0/management/codex-auth-url', {
+ method: 'GET',
+ managementKey,
});
- const result = await sendToContentScriptResilient('vps-panel', {
- type: 'REQUEST_OAUTH_URL',
- source: 'background',
- payload: {
- vpsPassword: state.vpsPassword,
- logStep: 7,
- },
- }, {
- timeoutMs: 30000,
- retryDelayMs: 700,
- logMessage: `${logLabel}:CPA 面板通信未就绪,正在等待页面恢复...`,
- });
+ const oauthUrl = String(
+ result?.url
+ || result?.auth_url
+ || result?.authUrl
+ || result?.data?.url
+ || result?.data?.auth_url
+ || result?.data?.authUrl
+ || ''
+ ).trim();
+ const oauthState = String(
+ result?.state
+ || result?.auth_state
+ || result?.authState
+ || result?.data?.state
+ || result?.data?.auth_state
+ || result?.data?.authState
+ || ''
+ ).trim()
+ || extractStateFromAuthUrl(oauthUrl);
- if (result?.error) {
- throw new Error(result.error);
+ if (!oauthUrl || !oauthUrl.startsWith('http')) {
+ throw new Error('CPA 管理接口未返回有效的 auth_url。');
}
- return result || {};
+
+ return {
+ oauthUrl,
+ cpaOAuthState: oauthState || null,
+ cpaManagementOrigin: origin,
+ };
}
async function requestCodex2ApiOAuthUrl(state, options = {}) {
diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js
index d12ca49..f24178e 100644
--- a/background/steps/oauth-login.js
+++ b/background/steps/oauth-login.js
@@ -40,17 +40,14 @@
return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message);
}
- function getVisibleStep(state, fallback = 7) {
- const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
- return visibleStep > 0 ? visibleStep : fallback;
- }
-
async function executeStep7(state) {
- const visibleStep = getVisibleStep(state, 7);
if (!state.email) {
throw new Error('缺少邮箱地址,请先完成步骤 3。');
}
+ const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
+ const completionStep = visibleStep > 0 ? visibleStep : 7;
+
let attempt = 0;
let lastError = null;
@@ -60,22 +57,22 @@
try {
const currentState = attempt === 1 ? state : await getState();
const password = currentState.password || currentState.customPassword || '';
- const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState, { visibleStep });
+ const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
if (typeof startOAuthFlowTimeoutWindow === 'function') {
- await startOAuthFlowTimeoutWindow({ step: visibleStep, oauthUrl });
+ await startOAuthFlowTimeoutWindow({ step: 7, oauthUrl });
}
const loginTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(180000, {
- step: visibleStep,
+ step: 7,
actionLabel: 'OAuth 登录并进入验证码页',
oauthUrl,
})
: 180000;
if (attempt === 1) {
- await addLog(`步骤 ${visibleStep}:正在打开最新 OAuth 链接并登录...`);
+ await addLog('步骤 7:正在打开最新 OAuth 链接并登录...');
} else {
- await addLog(`步骤 ${visibleStep}:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn');
+ await addLog(`步骤 7:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn');
}
await reuseOrCreateTab('signup-page', oauthUrl);
@@ -89,14 +86,13 @@
payload: {
email: currentState.email,
password,
- visibleStep,
},
},
{
timeoutMs: loginTimeoutMs,
responseTimeoutMs: loginTimeoutMs,
retryDelayMs: 700,
- logMessage: `步骤 ${visibleStep}:认证页正在切换,等待页面重新就绪后继续登录...`,
+ logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续登录...',
}
);
@@ -108,13 +104,14 @@
const completionPayload = {
loginVerificationRequestedAt: result.loginVerificationRequestedAt || null,
};
- if (result.skipLoginVerificationStep) {
- completionPayload.skipLoginVerificationStep = true;
+ if (Object.prototype.hasOwnProperty.call(result || {}, 'skipLoginVerificationStep')) {
+ completionPayload.skipLoginVerificationStep = Boolean(result.skipLoginVerificationStep);
}
- if (result.directOAuthConsentPage) {
- completionPayload.directOAuthConsentPage = true;
+ if (Object.prototype.hasOwnProperty.call(result || {}, 'directOAuthConsentPage')) {
+ completionPayload.directOAuthConsentPage = Boolean(result.directOAuthConsentPage);
}
- await completeStepFromBackground(visibleStep, completionPayload);
+
+ await completeStepFromBackground(completionStep, completionPayload);
return;
}
@@ -132,7 +129,7 @@
}
if (isManagementSecretConfigError(err)) {
await addLog(
- `步骤 ${visibleStep}:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
+ `步骤 7:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
'error'
);
throw err;
@@ -142,11 +139,11 @@
break;
}
- await addLog(`步骤 ${visibleStep}:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn');
+ await addLog(`步骤 7:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn');
}
}
- throw new Error(`步骤 ${visibleStep}:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
+ throw new Error(`步骤 7:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
}
return { executeStep7 };
diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js
index b88f295..1637dc8 100644
--- a/background/steps/platform-verify.js
+++ b/background/steps/platform-verify.js
@@ -26,31 +26,23 @@
return String(value || '').trim();
}
- function getVisibleStep(state, fallback = 10) {
+ function resolvePlatformVerifyStep(state = {}) {
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
- return visibleStep > 0 ? visibleStep : fallback;
+ return visibleStep >= 10 ? visibleStep : 10;
}
- function getConfirmStepForVisibleStep(visibleStep) {
- return visibleStep >= 13 ? 12 : 9;
- }
-
- function getAuthLoginStepForVisibleStep(visibleStep) {
- return visibleStep >= 13 ? 10 : 7;
- }
-
- function parseLocalhostCallback(rawUrl, visibleStep = 10, confirmStep = 9) {
+ function parseLocalhostCallback(rawUrl) {
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
- throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 ${confirmStep}。`);
+ throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 9。');
}
const code = normalizeString(parsed.searchParams.get('code'));
const state = normalizeString(parsed.searchParams.get('state'));
if (!code || !state) {
- throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmStep}。`);
+ throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。');
}
return {
@@ -72,6 +64,77 @@
return details || `Codex2API 请求失败(HTTP ${responseStatus})。`;
}
+ function deriveCpaManagementOrigin(vpsUrl) {
+ const normalizedUrl = normalizeString(vpsUrl);
+ if (!normalizedUrl) {
+ throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
+ }
+ let parsed;
+ try {
+ parsed = new URL(normalizedUrl);
+ } catch {
+ throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
+ }
+ return parsed.origin;
+ }
+
+ function getCpaApiErrorMessage(payload, responseStatus = 500) {
+ const details = [
+ payload?.error,
+ payload?.message,
+ payload?.detail,
+ payload?.reason,
+ ]
+ .map((value) => normalizeString(value))
+ .find(Boolean);
+ return details || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
+ }
+
+ async function fetchCpaManagementJson(origin, path, options = {}) {
+ const controller = new AbortController();
+ const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
+
+ try {
+ const managementKey = normalizeString(options.managementKey);
+ const headers = {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ };
+ if (managementKey) {
+ headers.Authorization = `Bearer ${managementKey}`;
+ headers['X-Management-Key'] = managementKey;
+ }
+
+ const response = await fetch(`${origin}${path}`, {
+ method: options.method || 'POST',
+ headers,
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
+ signal: controller.signal,
+ });
+
+ let payload = {};
+ try {
+ payload = await response.json();
+ } catch {
+ payload = {};
+ }
+
+ if (!response.ok) {
+ throw new Error(getCpaApiErrorMessage(payload, response.status));
+ }
+
+ return payload;
+ } catch (error) {
+ if (error?.name === 'AbortError') {
+ throw new Error('CPA 管理接口请求超时,请稍后重试。');
+ }
+ throw error;
+ } finally {
+ clearTimeout(timer);
+ }
+ }
+
async function fetchCodex2ApiJson(origin, path, options = {}) {
const controller = new AbortController();
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
@@ -122,95 +185,88 @@
}
async function executeCpaStep10(state) {
- const visibleStep = getVisibleStep(state, 10);
- const confirmStep = getConfirmStepForVisibleStep(visibleStep);
+ const platformVerifyStep = resolvePlatformVerifyStep(state);
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
- throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
+ throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
}
if (!state.localhostUrl) {
- throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
+ throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
}
if (!state.vpsUrl) {
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
}
if (shouldBypassStep9ForLocalCpa(state)) {
- await addLog(`步骤 ${visibleStep}:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。`, 'info');
- await completeStepFromBackground(visibleStep, {
+ await addLog('步骤 10:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。', 'info');
+ await completeStepFromBackground(platformVerifyStep, {
localhostUrl: state.localhostUrl,
verifiedStatus: 'local-auto',
});
return;
}
- await addLog(`步骤 ${visibleStep}:正在打开 CPA 面板...`);
-
- const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
- let tabId = await getTabId('vps-panel');
- const alive = tabId && await isTabAlive('vps-panel');
-
- if (!alive) {
- tabId = await reuseOrCreateTab('vps-panel', state.vpsUrl, {
- inject: injectFiles,
- reloadIfSameUrl: true,
- });
- } else {
- await closeConflictingTabsForSource('vps-panel', state.vpsUrl, { excludeTabIds: [tabId] });
- await chrome.tabs.update(tabId, { active: true });
- await rememberSourceLastUrl('vps-panel', state.vpsUrl);
+ const callback = parseLocalhostCallback(state.localhostUrl);
+ const expectedState = normalizeString(state.cpaOAuthState);
+ if (expectedState && expectedState !== callback.state) {
+ throw new Error('CPA 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
+ }
+ const managementKey = normalizeString(state.vpsPassword);
+ if (!managementKey) {
+ throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
}
- await ensureContentScriptReadyOnTab('vps-panel', tabId, {
- inject: injectFiles,
- timeoutMs: 45000,
- retryDelayMs: 900,
- logMessage: `步骤 ${visibleStep}:CPA 面板仍在加载,正在重试连接...`,
- });
+ await addLog('步骤 10:正在通过 CPA 管理接口提交回调地址...');
+ try {
+ const origin = normalizeString(state.cpaManagementOrigin) || deriveCpaManagementOrigin(state.vpsUrl);
+ const result = await fetchCpaManagementJson(origin, '/v0/management/oauth-callback', {
+ method: 'POST',
+ managementKey,
+ body: {
+ provider: 'codex',
+ redirect_url: callback.url,
+ },
+ });
- await addLog(`步骤 ${visibleStep}:正在填写回调地址...`);
- const result = await sendToContentScriptResilient('vps-panel', {
- type: 'EXECUTE_STEP',
- step: visibleStep,
- source: 'background',
- payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword, visibleStep },
- }, {
- timeoutMs: 125000,
- responseTimeoutMs: 125000,
- retryDelayMs: 700,
- logMessage: `步骤 ${visibleStep}:CPA 面板通信未就绪,正在等待页面恢复...`,
- });
-
- if (result?.error) {
- throw new Error(result.error);
+ const verifiedStatus = normalizeString(result?.message)
+ || normalizeString(result?.status_message)
+ || 'CPA 已通过接口提交回调';
+ await addLog(`步骤 10:${verifiedStatus}`, 'ok');
+ await completeStepFromBackground(platformVerifyStep, {
+ localhostUrl: callback.url,
+ verifiedStatus,
+ });
+ } catch (error) {
+ const reason = normalizeString(error?.message) || 'unknown error';
+ await addLog(`步骤 10:CPA 接口提交失败:${reason}`, 'error');
+ throw error;
}
}
async function executeCodex2ApiStep10(state) {
- const visibleStep = getVisibleStep(state, 10);
- const confirmStep = getConfirmStepForVisibleStep(visibleStep);
+ const platformVerifyStep = resolvePlatformVerifyStep(state);
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
- throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
+ throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
}
if (!state.localhostUrl) {
- throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
+ throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
}
if (!state.codex2apiSessionId) {
- throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`);
+ throw new Error('缺少 Codex2API 会话信息,请重新执行步骤 7。');
}
if (!normalizeString(state.codex2apiAdminKey)) {
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
}
- const callback = parseLocalhostCallback(state.localhostUrl, visibleStep, confirmStep);
+ const callback = parseLocalhostCallback(state.localhostUrl);
const expectedState = normalizeString(state.codex2apiOAuthState);
if (expectedState && expectedState !== callback.state) {
- throw new Error(`Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`);
+ throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
}
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
const origin = new URL(codex2apiUrl).origin;
- await addLog(`步骤 ${visibleStep}:正在向 Codex2API 提交回调并创建账号...`);
+ await addLog('步骤 10:正在向 Codex2API 提交回调并创建账号...');
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', {
adminKey: state.codex2apiAdminKey,
method: 'POST',
@@ -222,21 +278,19 @@
});
const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功';
- await addLog(`步骤 ${visibleStep}:${verifiedStatus}`, 'ok');
- await completeStepFromBackground(visibleStep, {
+ await addLog(`步骤 10:${verifiedStatus}`, 'ok');
+ await completeStepFromBackground(platformVerifyStep, {
localhostUrl: callback.url,
verifiedStatus,
});
}
async function executeSub2ApiStep10(state) {
- const visibleStep = getVisibleStep(state, 10);
- const confirmStep = getConfirmStepForVisibleStep(visibleStep);
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
- throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
+ throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
}
if (!state.localhostUrl) {
- throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
+ throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
}
if (!state.sub2apiSessionId) {
throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。');
@@ -251,7 +305,7 @@
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
- await addLog(`步骤 ${visibleStep}:正在打开 SUB2API 后台...`);
+ await addLog('步骤 10:正在打开 SUB2API 后台...');
let tabId = await getTabId('sub2api-panel');
const alive = tabId && await isTabAlive('sub2api-panel');
@@ -273,13 +327,12 @@
injectSource: 'sub2api-panel',
});
- await addLog(`步骤 ${visibleStep}:正在向 SUB2API 提交回调并创建账号...`);
+ await addLog('步骤 10:正在向 SUB2API 提交回调并创建账号...');
const result = await sendToContentScript('sub2api-panel', {
type: 'EXECUTE_STEP',
- step: visibleStep,
+ step: 10,
source: 'background',
payload: {
- visibleStep,
localhostUrl: state.localhostUrl,
sub2apiUrl,
sub2apiEmail: state.sub2apiEmail,
diff --git a/docs/使用教程.md b/docs/使用教程.md
index a9a4d84..92ef7c2 100644
--- a/docs/使用教程.md
+++ b/docs/使用教程.md
@@ -225,7 +225,7 @@
### 第七部分:`HeroSMS` 手机接码扩展使用教程(新增)
-本部分用于说明扩展中 `Step 9` 手机号验证链路的最新接码能力与推荐用法。
+本部分用于说明扩展中 `Step 9` 手机号验证链路的最新接码能力与推荐用法。
目标是减少“重复重发导致封禁”与“同国家持续拿不到码”的失败率,并让失败可以在 `Step 9` 内部自愈。
#### 一、入口与启用
@@ -236,7 +236,7 @@
#### 二、国家与优先级(新增能力)
-1. 在 `接码国家` 中多选国家(最多 `3` 个),按你的业务顺序排列。
+1. 在 `接码国家` 中多选国家(最多 `3` 个),按你的业务顺序排列。
2. 在 `国家优先级` 中选择策略:
- `国家优先`:严格按你选择的顺序先后尝试。
- `价格优先`:在候选国家里先选可用价格更低的国家,再尝试拿号。
@@ -280,11 +280,11 @@
#### 七、常见问题与处理
-1. `no numbers available across ...`
+1. `no numbers available across ...`
表示候选国家当前无号或价格上限过低。先提高价格上限,再调整国家顺序。
-2. `NO_NUMBERS` 频繁出现
+2. `NO_NUMBERS` 频繁出现
增加候选国家数量(最多 3 个),并优先启用 `价格优先`。
-3. 页面提示重发过多
+3. 页面提示重发过多
不要手工持续点重发,让流程自动换号/换国家即可。
### 第八部分:`PayPal` 注册与绑卡使用教程
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 3383903..bd7f742 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -2381,9 +2381,16 @@ function collectSettingsPayload() {
const heroSmsReuseEnabledValue = typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled
? normalizeHeroSmsReuseEnabledValue(inputHeroSmsReuseEnabled.checked)
: defaultHeroSmsReuseEnabled;
+ const normalizeHeroSmsAcquirePriorityValue = typeof normalizeHeroSmsAcquirePriority === 'function'
+ ? normalizeHeroSmsAcquirePriority
+ : (value) => (String(value || '').trim().toLowerCase() === 'price' ? 'price' : 'country');
const heroSmsAcquirePriorityValue = typeof selectHeroSmsAcquirePriority !== 'undefined' && selectHeroSmsAcquirePriority
- ? normalizeHeroSmsAcquirePriority(selectHeroSmsAcquirePriority.value)
- : DEFAULT_HERO_SMS_ACQUIRE_PRIORITY;
+ ? normalizeHeroSmsAcquirePriorityValue(selectHeroSmsAcquirePriority.value)
+ : normalizeHeroSmsAcquirePriorityValue(
+ typeof DEFAULT_HERO_SMS_ACQUIRE_PRIORITY !== 'undefined'
+ ? DEFAULT_HERO_SMS_ACQUIRE_PRIORITY
+ : 'country'
+ );
const heroSmsMaxPriceValue = typeof inputHeroSmsMaxPrice !== 'undefined' && inputHeroSmsMaxPrice
? normalizeHeroSmsMaxPriceValue(inputHeroSmsMaxPrice.value)
: '';
diff --git a/tests/background-cpa-state-plumbing.test.js b/tests/background-cpa-state-plumbing.test.js
new file mode 100644
index 0000000..c894bd5
--- /dev/null
+++ b/tests/background-cpa-state-plumbing.test.js
@@ -0,0 +1,39 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+test('background step-1 state plumbing persists and resets cpa oauth runtime keys', () => {
+ const source = fs.readFileSync('background.js', 'utf8');
+
+ assert.match(source, /cpaOAuthState:\s*null/);
+ assert.match(source, /cpaManagementOrigin:\s*null/);
+ assert.match(source, /payload\.cpaOAuthState[^\n]*updates\.cpaOAuthState/);
+ assert.match(source, /payload\.cpaManagementOrigin[^\n]*updates\.cpaManagementOrigin/);
+ assert.match(source, /if \(step <= 1\) \{[\s\S]*cpaOAuthState:\s*null,[\s\S]*cpaManagementOrigin:\s*null,/);
+});
+
+test('message router step-1 handler stores cpa oauth runtime keys', async () => {
+ const source = fs.readFileSync('background/message-router.js', 'utf8');
+ const globalScope = {};
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
+
+ const updates = [];
+ const router = api.createMessageRouter({
+ broadcastDataUpdate: () => {},
+ setState: async (payload) => {
+ updates.push(payload);
+ },
+ });
+
+ await router.handleStepData(1, {
+ cpaOAuthState: 'oauth-state-1',
+ cpaManagementOrigin: 'http://localhost:8317',
+ });
+
+ assert.deepStrictEqual(updates, [
+ {
+ cpaOAuthState: 'oauth-state-1',
+ cpaManagementOrigin: 'http://localhost:8317',
+ },
+ ]);
+});
diff --git a/tests/background-message-router-plus-final-step.test.js b/tests/background-message-router-plus-final-step.test.js
new file mode 100644
index 0000000..723f3f2
--- /dev/null
+++ b/tests/background-message-router-plus-final-step.test.js
@@ -0,0 +1,105 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+const source = fs.readFileSync('background/message-router.js', 'utf8');
+const globalScope = {};
+const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
+
+test('message router appends success record on Plus final step instead of hard-coded step 10', async () => {
+ const appendCalls = [];
+ const router = api.createMessageRouter({
+ addLog: async () => {},
+ appendAccountRunRecord: async (...args) => {
+ appendCalls.push(args);
+ },
+ batchUpdateLuckmailPurchases: async () => {},
+ buildLocalhostCleanupPrefix: () => '',
+ buildLuckmailSessionSettingsPayload: () => ({}),
+ buildPersistentSettingsPayload: () => ({}),
+ broadcastDataUpdate: () => {},
+ cancelScheduledAutoRun: async () => {},
+ checkIcloudSession: async () => {},
+ clearAutoRunTimerAlarm: async () => {},
+ clearLuckmailRuntimeState: async () => {},
+ clearStopRequest: () => {},
+ closeLocalhostCallbackTabs: async () => {},
+ closeTabsByUrlPrefix: async () => {},
+ deleteHotmailAccount: async () => {},
+ deleteHotmailAccounts: async () => {},
+ deleteIcloudAlias: async () => {},
+ deleteUsedIcloudAliases: async () => {},
+ disableUsedLuckmailPurchases: async () => {},
+ doesStepUseCompletionSignal: () => false,
+ ensureManualInteractionAllowed: async () => ({}),
+ executeStep: async () => {},
+ executeStepViaCompletionSignal: async () => {},
+ exportSettingsBundle: async () => ({}),
+ fetchGeneratedEmail: async () => '',
+ finalizeStep3Completion: async () => {},
+ finalizeIcloudAliasAfterSuccessfulFlow: async () => {},
+ findHotmailAccount: async () => null,
+ flushCommand: async () => {},
+ getCurrentLuckmailPurchase: () => null,
+ getPendingAutoRunTimerPlan: () => null,
+ getSourceLabel: () => '',
+ getState: async () => ({ plusModeEnabled: true, stepStatuses: { 13: 'pending' } }),
+ getLastStepIdForState: () => 13,
+ getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'oauth-login' : 'platform-verify' }),
+ getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
+ getTabId: async () => null,
+ getStopRequested: () => false,
+ handleAutoRunLoopUnhandledError: async () => {},
+ handleCloudflareSecurityBlocked: async () => '',
+ importSettingsBundle: async () => {},
+ invalidateDownstreamAfterStepRestart: async () => {},
+ isCloudflareSecurityBlockedError: () => false,
+ isAutoRunLockedState: () => false,
+ isHotmailProvider: () => false,
+ isLocalhostOAuthCallbackUrl: () => true,
+ isLuckmailProvider: () => false,
+ isStopError: () => false,
+ isTabAlive: async () => false,
+ launchAutoRunTimerPlan: async () => {},
+ listIcloudAliases: async () => [],
+ listLuckmailPurchasesForManagement: async () => [],
+ normalizeHotmailAccounts: (items) => items,
+ normalizeRunCount: (value) => value,
+ AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
+ notifyStepComplete: () => {},
+ notifyStepError: () => {},
+ patchHotmailAccount: async () => {},
+ patchMail2925Account: async () => {},
+ registerTab: async () => {},
+ requestStop: async () => {},
+ resetState: async () => {},
+ resumeAutoRun: async () => {},
+ scheduleAutoRun: async () => {},
+ selectLuckmailPurchase: async () => {},
+ setCurrentHotmailAccount: async () => {},
+ setCurrentMail2925Account: async () => {},
+ setContributionMode: async () => {},
+ setEmailState: async () => {},
+ setEmailStateSilently: async () => {},
+ setIcloudAliasPreservedState: async () => {},
+ setIcloudAliasUsedState: async () => {},
+ setLuckmailPurchaseDisabledState: async () => {},
+ setLuckmailPurchasePreservedState: async () => {},
+ setLuckmailPurchaseUsedState: async () => {},
+ setPersistentSettings: async () => {},
+ setState: async () => {},
+ setStepStatus: async () => {},
+ skipAutoRunCountdown: async () => false,
+ skipStep: async () => {},
+ startAutoRunLoop: async () => {},
+ syncHotmailAccounts: async () => {},
+ testHotmailAccountMailAccess: async () => {},
+ upsertHotmailAccount: async () => {},
+ verifyHotmailAccount: async () => {},
+ });
+
+ await router.handleMessage({ type: 'STEP_COMPLETE', step: 13, payload: {} }, {});
+
+ assert.equal(appendCalls.length, 1);
+ assert.equal(appendCalls[0][0], 'success');
+});
diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js
index 7b72c22..27e2a0a 100644
--- a/tests/background-message-router-step2-skip.test.js
+++ b/tests/background-message-router-step2-skip.test.js
@@ -65,6 +65,7 @@ function createRouter(overrides = {}) {
getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } },
getStepDefinitionForState: overrides.getStepDefinitionForState,
getStepIdsForState: overrides.getStepIdsForState,
+ getLastStepIdForState: overrides.getLastStepIdForState,
getTabId: overrides.getTabId || (async () => null),
getStopRequested: () => false,
handleAutoRunLoopUnhandledError: async () => {},
diff --git a/tests/background-panel-bridge-module.test.js b/tests/background-panel-bridge-module.test.js
index 353c990..3e655af 100644
--- a/tests/background-panel-bridge-module.test.js
+++ b/tests/background-panel-bridge-module.test.js
@@ -71,3 +71,48 @@ test('panel bridge can request codex2api oauth url via protocol', async () => {
globalThis.fetch = originalFetch;
}
});
+
+test('panel bridge can request cpa oauth url via management api', async () => {
+ const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = async (url, options = {}) => {
+ assert.equal(url, 'http://localhost:8317/v0/management/codex-auth-url');
+ assert.equal(options.method, 'GET');
+ assert.equal(options.headers.Authorization, 'Bearer cpa-key');
+ assert.equal(options.headers['X-Management-Key'], 'cpa-key');
+ return {
+ ok: true,
+ json: async () => ({
+ status: 'ok',
+ url: 'https://auth.openai.com/authorize?state=cpa-oauth-state',
+ state: 'cpa-oauth-state',
+ }),
+ };
+ };
+
+ try {
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
+ const bridge = api.createPanelBridge({
+ addLog: async () => {},
+ getPanelMode: () => 'cpa',
+ normalizeCodex2ApiUrl: (value) => value,
+ normalizeSub2ApiUrl: (value) => value,
+ DEFAULT_SUB2API_GROUP_NAME: 'codex',
+ SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
+ });
+
+ const result = await bridge.requestOAuthUrlFromPanel({
+ panelMode: 'cpa',
+ vpsUrl: 'http://localhost:8317/admin/oauth',
+ vpsPassword: 'cpa-key',
+ }, { logLabel: '步骤 7' });
+
+ assert.deepStrictEqual(result, {
+ oauthUrl: 'https://auth.openai.com/authorize?state=cpa-oauth-state',
+ cpaOAuthState: 'cpa-oauth-state',
+ cpaManagementOrigin: 'http://localhost:8317',
+ });
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+});
diff --git a/tests/background-platform-verify-cpa-api.test.js b/tests/background-platform-verify-cpa-api.test.js
new file mode 100644
index 0000000..0fd9002
--- /dev/null
+++ b/tests/background-platform-verify-cpa-api.test.js
@@ -0,0 +1,239 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const test = require('node:test');
+
+function createDeps(overrides = {}) {
+ const logs = [];
+ const completed = [];
+ const uiCalls = [];
+
+ const deps = {
+ addLog: async (message, level = 'info') => {
+ logs.push({ message, level });
+ },
+ chrome: {
+ tabs: {
+ update: async () => {},
+ },
+ },
+ closeConflictingTabsForSource: async () => {},
+ completeStepFromBackground: async (step, payload) => {
+ completed.push({ step, payload });
+ },
+ ensureContentScriptReadyOnTab: async () => {},
+ getPanelMode: () => 'cpa',
+ getTabId: async () => 0,
+ isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='),
+ isTabAlive: async () => false,
+ normalizeCodex2ApiUrl: (value) => value,
+ normalizeSub2ApiUrl: (value) => value,
+ rememberSourceLastUrl: async () => {},
+ reuseOrCreateTab: async () => 91,
+ sendToContentScript: async () => ({}),
+ sendToContentScriptResilient: async (source, message, options) => {
+ uiCalls.push({ source, message, options });
+ return {};
+ },
+ shouldBypassStep9ForLocalCpa: () => false,
+ SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
+ ...overrides,
+ };
+
+ return { deps, logs, completed, uiCalls };
+}
+
+test('platform verify module submits CPA callback via management API first', async () => {
+ const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
+ const originalFetch = globalThis.fetch;
+ let uiCalled = false;
+ globalThis.fetch = async (url, options = {}) => {
+ assert.equal(url, 'http://localhost:8317/v0/management/oauth-callback');
+ assert.equal(options.method, 'POST');
+ assert.equal(options.headers.Authorization, 'Bearer cpa-key');
+ assert.equal(options.headers['X-Management-Key'], 'cpa-key');
+ assert.deepStrictEqual(JSON.parse(options.body), {
+ provider: 'codex',
+ redirect_url: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
+ });
+ return {
+ ok: true,
+ json: async () => ({
+ message: 'CPA API 回调提交成功',
+ }),
+ };
+ };
+
+ try {
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
+ const { deps, logs, completed } = createDeps({
+ sendToContentScriptResilient: async () => {
+ uiCalled = true;
+ return {};
+ },
+ });
+ const executor = api.createStep10Executor(deps);
+
+ await executor.executeStep10({
+ panelMode: 'cpa',
+ localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
+ vpsUrl: 'http://localhost:8317/admin/oauth',
+ vpsPassword: 'cpa-key',
+ });
+
+ assert.equal(uiCalled, false);
+ assert.deepStrictEqual(completed, [
+ {
+ step: 10,
+ payload: {
+ localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
+ verifiedStatus: 'CPA API 回调提交成功',
+ },
+ },
+ ]);
+ assert.deepStrictEqual(logs, [
+ { message: '步骤 10:正在通过 CPA 管理接口提交回调地址...', level: 'info' },
+ { message: '步骤 10:CPA API 回调提交成功', level: 'ok' },
+ ]);
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+});
+
+test('platform verify module prefers cpaManagementOrigin when provided', async () => {
+ const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = async (url, options = {}) => {
+ assert.equal(url, 'http://localhost:9999/v0/management/oauth-callback');
+ assert.equal(options.method, 'POST');
+ assert.equal(options.headers.Authorization, 'Bearer cpa-key');
+ assert.equal(options.headers['X-Management-Key'], 'cpa-key');
+ return {
+ ok: true,
+ json: async () => ({
+ message: 'CPA API 回调提交成功',
+ }),
+ };
+ };
+
+ try {
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
+ const { deps, completed } = createDeps();
+ const executor = api.createStep10Executor(deps);
+
+ await executor.executeStep10({
+ panelMode: 'cpa',
+ localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
+ cpaManagementOrigin: 'http://localhost:9999',
+ vpsUrl: 'http://localhost:8317/admin/oauth',
+ vpsPassword: 'cpa-key',
+ });
+
+ assert.equal(completed.length, 1);
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+});
+
+test('platform verify module fails fast when CPA API submit fails', async () => {
+ const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = async () => ({
+ ok: false,
+ status: 500,
+ json: async () => ({ message: 'failed to persist oauth callback' }),
+ });
+
+ try {
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
+ const { deps, logs, completed, uiCalls } = createDeps();
+ const executor = api.createStep10Executor(deps);
+
+ await assert.rejects(
+ () => executor.executeStep10({
+ panelMode: 'cpa',
+ localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
+ vpsUrl: 'http://localhost:8317/admin/oauth',
+ vpsPassword: 'cpa-key',
+ }),
+ /failed to persist oauth callback/
+ );
+
+ assert.equal(uiCalls.length, 0);
+ assert.equal(completed.length, 0);
+ assert.equal(logs[0].message, '步骤 10:正在通过 CPA 管理接口提交回调地址...');
+ assert.match(logs[1].message, /步骤 10:CPA 接口提交失败:failed to persist oauth callback/);
+ assert.equal(logs[1].level, 'error');
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+});
+
+test('platform verify module requires management key for CPA API-only flow', async () => {
+ const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
+ const originalFetch = globalThis.fetch;
+ let fetchCalled = false;
+ globalThis.fetch = async () => {
+ fetchCalled = true;
+ return {
+ ok: true,
+ json: async () => ({}),
+ };
+ };
+
+ try {
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
+ const { deps, logs, completed, uiCalls } = createDeps();
+ const executor = api.createStep10Executor(deps);
+
+ await assert.rejects(
+ () => executor.executeStep10({
+ panelMode: 'cpa',
+ localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
+ vpsUrl: 'http://localhost:8317/admin/oauth',
+ vpsPassword: ' ',
+ }),
+ /尚未配置 CPA 管理密钥/
+ );
+
+ assert.equal(fetchCalled, false);
+ assert.equal(uiCalls.length, 0);
+ assert.equal(completed.length, 0);
+ assert.equal(logs.length, 0);
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+});
+
+test('platform verify module rejects callback when cpa oauth state mismatches', async () => {
+ const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
+ const originalFetch = globalThis.fetch;
+ let fetchCalled = false;
+ globalThis.fetch = async () => {
+ fetchCalled = true;
+ return {
+ ok: true,
+ json: async () => ({ message: 'should not happen' }),
+ };
+ };
+
+ try {
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
+ const { deps } = createDeps();
+ const executor = api.createStep10Executor(deps);
+
+ await assert.rejects(
+ () => executor.executeStep10({
+ panelMode: 'cpa',
+ localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=callback-state',
+ cpaOAuthState: 'expected-state',
+ vpsUrl: 'http://localhost:8317/admin/oauth',
+ vpsPassword: 'cpa-key',
+ }),
+ /CPA 回调 state 与当前授权会话不匹配/
+ );
+
+ assert.equal(fetchCalled, false);
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+});
diff --git a/项目完整链路说明.md b/项目完整链路说明.md
index c754ac6..e25f8c4 100644
--- a/项目完整链路说明.md
+++ b/项目完整链路说明.md
@@ -23,7 +23,6 @@
- 轮询登录验证码
- 自动确认 OAuth 同意页
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
-- 可选开启 Plus 模式:先完成 Plus Checkout、账单地址、PayPal 登录授权与订阅回跳确认,再复用 OAuth 后半段链路
## 2. 核心运行参与者
@@ -118,7 +117,6 @@
- 步骤顺序靠 `order`
- 步骤文件名靠语义
- 新增步骤时不需要重命名后续文件
-- 普通模式使用 10 步定义;Plus 模式使用 13 步定义,其中 Plus 可见步骤 10/11/12/13 复用原 OAuth 登录、登录验证码、OAuth 同意页与平台回调验证执行器,但按 Plus 可见步骤编号记录状态
## 4. 状态与存储链路
@@ -141,7 +139,6 @@
- 第 8 步固定的验证码页显示邮箱 `step8VerificationTargetEmail`
- 当前手机号验证激活记录 `currentPhoneActivation`
- 可复用的手机号验证激活记录 `reusablePhoneActivation`
-- Plus checkout / PayPal 运行态:`plusCheckoutTabId`、`plusCheckoutUrl`、`plusCheckoutCountry`、`plusCheckoutCurrency`、`plusBillingCountryText`、`plusBillingAddress`、`plusPaypalApprovedAt`、`plusReturnUrl`
- localhost 回调地址
- 自动运行轮次信息
- 当前自动运行 session 标识 `autoRunSessionId`
@@ -171,8 +168,7 @@
- 2925 当前选中的号池账号 ID `currentMail2925AccountId`
- Cloudflare / Temp Email 设置
- 接码开关,以及 HeroSMS 的 API Key 与默认国家设置
-- iCloud 相关偏好:Host、获取策略、成功后自动删除、目标邮箱类型与转发收码邮箱 provider
-- iCloud Hide My Email 别名缓存:`icloudAliasCache` / `icloudAliasCacheAt`,用于在 iCloud 会话或网络上下文短暂波动时回退展示最近可用列表
+- iCloud 相关偏好
- LuckMail API 配置
- 自动运行默认配置
- 账号运行历史 `accountRunHistory`(以邮箱为主键,保存该邮箱最近一次状态:成功/失败/停止)
@@ -457,7 +453,6 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
补充:
- HeroSMS 号码当前最多复用 3 次成功注册;超过上限后会清空可复用激活记录,下次重新申请新号码。
-- HeroSMS 新号码申请会先查询当前国家与 OpenAI 服务的最低价并携带固定价格参数;如果 `getNumber` 返回 `NO_NUMBERS`,会回退到 `getNumberV2`,后续用 `getStatusV2` 轮询验证码。
- 如果同一个号码在重发短信后 60 秒仍收不到验证码,后台会抛出“回到步骤 7 重新拿新号码”的恢复错误,而不是把当前号码无限重试下去。
### Step 10
@@ -646,7 +641,6 @@ Plus 模式可见步骤:
- `163`、`163 VIP`、`126` 都走同一条“网易网页邮箱”验证码链路。
- sidepanel 只负责切换 provider 与展示登录入口;后台根据 provider 选择对应网页邮箱首页。
- 内容脚本来源统一归类到 `mail-163`,这样 Step 4 / Step 8 继续复用同一套验证码读取与邮件清理逻辑。
-- `mail-provider-utils.js` 同时承接 iCloud 转发收码可选 provider 的归一化与入口配置,避免 background / sidepanel 重新复制 QQ、网易和 Gmail 的收码地址、label 与注入脚本。
- `manifest.json` 需要同时覆盖:
- `https://mail.163.com/*`
- `https://webmail.vip.163.com/*`
@@ -741,7 +735,6 @@ Plus 模式可见步骤:
组成:
- [icloud-utils.js](c:/Users/projectf/Downloads/codex注册扩展/icloud-utils.js)
-- [mail-provider-utils.js](c:/Users/projectf/Downloads/codex注册扩展/mail-provider-utils.js)
- [content/icloud-mail.js](c:/Users/projectf/Downloads/codex注册扩展/content/icloud-mail.js)
配置:
@@ -803,7 +796,6 @@ Hide My Email 获取与管理链路:
- UI 层不能凭输入值直接判断“代理已接管”,只能展示后台返回的 `ipProxyApplied*` 状态。
- 新增代理服务商时,应优先新增 provider 规则模块,并让共享解析/运行态继续走 `background/ip-proxy-core.js`。
- 修改代理字段、权限或链路时,需要同步更新 [docs/ip-proxy-module.md](c:/Users/projectf/Downloads/codex注册扩展/docs/ip-proxy-module.md)、当前完整链路说明和结构说明。
-
## 8. 自动运行完整链路
文件:
@@ -819,11 +811,9 @@ Hide My Email 获取与管理链路:
- 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
5. 执行 `runAutoSequenceFromStep`
- - 自动运行会按当前 `plusModeEnabled` 选择普通 10 步或 Plus 13 步可见步骤;Plus 模式下第 6~9 步走 checkout / PayPal,第 10~13 步复用 OAuth 后半段
- 步骤 7 内部仍保留登录态恢复的有限重试,但 `add-phone / 手机号页` 属于立即跳出的不可重试错误
- 步骤 8 若在验证码提交后进入 `add-phone / 手机号页`,会直接抛出 fatal 错误,不再先标记步骤成功
- - 普通模式一旦进入步骤 7~10,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 7 无限重开
- - Plus 模式在 checkout / PayPal 结束后,如果 OAuth 后半段遇到普通报错且认证流程未进入 `add-phone`,则自动回到 Plus 可见步骤 10 重新开始授权链路
+ - 一旦进入步骤 7~10,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 7 无限重开
- 如果命中 `add-phone / 手机号页` 这类 fatal 错误,则不会再做当前轮的内部重试;当开启自动重试/跳过失败时,会直接结束当前轮并继续下一轮,而不是把整条自动流程暂停
- 如果是手动停止,则立即退出自动流程,不会再触发“回到步骤 7 重开”
6. 如果失败,根据设置决定: