Plua模式开发
This commit is contained in:
+366
-52
@@ -18,12 +18,17 @@ importScripts(
|
|||||||
'background/logging-status.js',
|
'background/logging-status.js',
|
||||||
'background/steps/registry.js',
|
'background/steps/registry.js',
|
||||||
'data/step-definitions.js',
|
'data/step-definitions.js',
|
||||||
|
'data/address-sources.js',
|
||||||
'background/steps/open-chatgpt.js',
|
'background/steps/open-chatgpt.js',
|
||||||
'background/steps/submit-signup-email.js',
|
'background/steps/submit-signup-email.js',
|
||||||
'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/clear-login-cookies.js',
|
||||||
|
'background/steps/create-plus-checkout.js',
|
||||||
|
'background/steps/fill-plus-checkout.js',
|
||||||
|
'background/steps/paypal-approve.js',
|
||||||
|
'background/steps/plus-return-confirm.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',
|
||||||
@@ -37,12 +42,28 @@ importScripts(
|
|||||||
'content/activation-utils.js'
|
'content/activation-utils.js'
|
||||||
);
|
);
|
||||||
|
|
||||||
const SHARED_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.() || [];
|
const NORMAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: false }) || [];
|
||||||
const STEP_IDS = SHARED_STEP_DEFINITIONS
|
const PLUS_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: true }) || NORMAL_STEP_DEFINITIONS;
|
||||||
|
const ALL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getAllSteps?.() || [
|
||||||
|
...NORMAL_STEP_DEFINITIONS,
|
||||||
|
...PLUS_STEP_DEFINITIONS,
|
||||||
|
];
|
||||||
|
const STEP_IDS = Array.from(new Set(ALL_STEP_DEFINITIONS
|
||||||
|
.map((definition) => Number(definition?.id))
|
||||||
|
.filter(Number.isFinite)))
|
||||||
|
.sort((left, right) => left - right);
|
||||||
|
const NORMAL_STEP_IDS = NORMAL_STEP_DEFINITIONS
|
||||||
.map((definition) => Number(definition?.id))
|
.map((definition) => Number(definition?.id))
|
||||||
.filter(Number.isFinite)
|
.filter(Number.isFinite)
|
||||||
.sort((left, right) => left - right);
|
.sort((left, right) => left - right);
|
||||||
const LAST_STEP_ID = STEP_IDS[STEP_IDS.length - 1] || 10;
|
const PLUS_STEP_IDS = PLUS_STEP_DEFINITIONS
|
||||||
|
.map((definition) => Number(definition?.id))
|
||||||
|
.filter(Number.isFinite)
|
||||||
|
.sort((left, right) => left - right);
|
||||||
|
const LAST_STEP_ID = Math.max(
|
||||||
|
NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10,
|
||||||
|
PLUS_STEP_IDS[PLUS_STEP_IDS.length - 1] || 10
|
||||||
|
);
|
||||||
const FINAL_OAUTH_CHAIN_START_STEP = 7;
|
const FINAL_OAUTH_CHAIN_START_STEP = 7;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -218,6 +239,32 @@ const CONTRIBUTION_RUNTIME_DEFAULTS = self.MultiPageBackgroundContributionOAuth?
|
|||||||
const CONTRIBUTION_RUNTIME_KEYS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_KEYS
|
const CONTRIBUTION_RUNTIME_KEYS = self.MultiPageBackgroundContributionOAuth?.RUNTIME_KEYS
|
||||||
|| Object.keys(CONTRIBUTION_RUNTIME_DEFAULTS);
|
|| Object.keys(CONTRIBUTION_RUNTIME_DEFAULTS);
|
||||||
|
|
||||||
|
function isPlusModeState(state = {}) {
|
||||||
|
return Boolean(state?.plusModeEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStepDefinitionsForState(state = {}) {
|
||||||
|
return isPlusModeState(state) ? PLUS_STEP_DEFINITIONS : NORMAL_STEP_DEFINITIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStepIdsForState(state = {}) {
|
||||||
|
return isPlusModeState(state) ? PLUS_STEP_IDS : NORMAL_STEP_IDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastStepIdForState(state = {}) {
|
||||||
|
const ids = getStepIdsForState(state);
|
||||||
|
return ids[ids.length - 1] || 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthChainStartStepId(state = {}) {
|
||||||
|
return isPlusModeState(state) ? 10 : FINAL_OAUTH_CHAIN_START_STEP;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStepDefinitionForState(step, state = {}) {
|
||||||
|
const numericStep = Number(step);
|
||||||
|
return getStepDefinitionsForState(state).find((definition) => Number(definition.id) === numericStep) || null;
|
||||||
|
}
|
||||||
|
|
||||||
initializeSessionStorageAccess();
|
initializeSessionStorageAccess();
|
||||||
setupDeclarativeNetRequestRules();
|
setupDeclarativeNetRequestRules();
|
||||||
|
|
||||||
@@ -264,6 +311,9 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
codex2apiUrl: DEFAULT_CODEX2API_URL,
|
codex2apiUrl: DEFAULT_CODEX2API_URL,
|
||||||
codex2apiAdminKey: '',
|
codex2apiAdminKey: '',
|
||||||
customPassword: '',
|
customPassword: '',
|
||||||
|
plusModeEnabled: false,
|
||||||
|
paypalEmail: '',
|
||||||
|
paypalPassword: '',
|
||||||
autoRunSkipFailures: false,
|
autoRunSkipFailures: false,
|
||||||
autoRunFallbackThreadIntervalMinutes: 0,
|
autoRunFallbackThreadIntervalMinutes: 0,
|
||||||
autoRunDelayEnabled: false,
|
autoRunDelayEnabled: false,
|
||||||
@@ -350,6 +400,14 @@ const DEFAULT_STATE = {
|
|||||||
sub2apiProxyId: null, // SUB2API 本轮使用的代理 ID。
|
sub2apiProxyId: null, // SUB2API 本轮使用的代理 ID。
|
||||||
codex2apiSessionId: null, // Codex2API OAuth 会话 ID。
|
codex2apiSessionId: null, // Codex2API OAuth 会话 ID。
|
||||||
codex2apiOAuthState: null, // Codex2API OAuth state。
|
codex2apiOAuthState: null, // Codex2API OAuth state。
|
||||||
|
plusCheckoutTabId: null, // Plus checkout / PayPal 标签页 ID。
|
||||||
|
plusCheckoutUrl: null, // Plus checkout 运行时短链,不写入持久配置。
|
||||||
|
plusCheckoutCountry: 'DE',
|
||||||
|
plusCheckoutCurrency: 'EUR',
|
||||||
|
plusBillingCountryText: '',
|
||||||
|
plusBillingAddress: null,
|
||||||
|
plusPaypalApprovedAt: null,
|
||||||
|
plusReturnUrl: '',
|
||||||
flowStartTime: null, // 当前流程开始时间。
|
flowStartTime: null, // 当前流程开始时间。
|
||||||
tabRegistry: {}, // 程序维护的标签页注册表。
|
tabRegistry: {}, // 程序维护的标签页注册表。
|
||||||
sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
|
sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
|
||||||
@@ -973,9 +1031,14 @@ function normalizePersistentSettingValue(key, value) {
|
|||||||
return String(value || '').trim();
|
return String(value || '').trim();
|
||||||
case 'customPassword':
|
case 'customPassword':
|
||||||
return String(value || '');
|
return String(value || '');
|
||||||
|
case 'paypalEmail':
|
||||||
|
return String(value || '').trim();
|
||||||
|
case 'paypalPassword':
|
||||||
|
return String(value || '');
|
||||||
case 'autoRunSkipFailures':
|
case 'autoRunSkipFailures':
|
||||||
case 'autoRunDelayEnabled':
|
case 'autoRunDelayEnabled':
|
||||||
case 'phoneVerificationEnabled':
|
case 'phoneVerificationEnabled':
|
||||||
|
case 'plusModeEnabled':
|
||||||
return Boolean(value);
|
return Boolean(value);
|
||||||
case 'autoRunFallbackThreadIntervalMinutes':
|
case 'autoRunFallbackThreadIntervalMinutes':
|
||||||
return normalizeAutoRunFallbackThreadIntervalMinutes(value);
|
return normalizeAutoRunFallbackThreadIntervalMinutes(value);
|
||||||
@@ -1138,7 +1201,7 @@ async function getState() {
|
|||||||
getPersistedAliasState(),
|
getPersistedAliasState(),
|
||||||
accountRunHistoryHelpers?.getPersistedAccountRunHistory?.() || [],
|
accountRunHistoryHelpers?.getPersistedAccountRunHistory?.() || [],
|
||||||
]);
|
]);
|
||||||
return { ...DEFAULT_STATE, ...persistedSettings, ...persistedAliasState, accountRunHistory, ...state };
|
return { ...DEFAULT_STATE, ...persistedSettings, ...persistedAliasState, ...state, accountRunHistory };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeSessionStorageAccess() {
|
async function initializeSessionStorageAccess() {
|
||||||
@@ -3968,14 +4031,97 @@ async function waitForTabUrlMatch(tabId, matcher, options = {}) {
|
|||||||
return tabRuntime.waitForTabUrlMatch(tabId, matcher, options);
|
return tabRuntime.waitForTabUrlMatch(tabId, matcher, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForTabUrlMatchUntilStopped(tabId, matcher, options = {}) {
|
||||||
|
const retryDelayMs = Math.max(100, Math.floor(Number(options.retryDelayMs) || 300));
|
||||||
|
while (true) {
|
||||||
|
throwIfStopped();
|
||||||
|
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||||||
|
if (!tab) {
|
||||||
|
throw new Error('目标标签页已关闭,无法继续等待页面跳转。');
|
||||||
|
}
|
||||||
|
if (typeof matcher === 'function' && matcher(tab.url || '', tab)) {
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
await sleepWithStop(retryDelayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForTabComplete(tabId, options = {}) {
|
async function waitForTabComplete(tabId, options = {}) {
|
||||||
return tabRuntime.waitForTabComplete(tabId, options);
|
return tabRuntime.waitForTabComplete(tabId, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForTabCompleteUntilStopped(tabId, options = {}) {
|
||||||
|
const retryDelayMs = Math.max(100, Math.floor(Number(options.retryDelayMs) || 300));
|
||||||
|
while (true) {
|
||||||
|
throwIfStopped();
|
||||||
|
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||||||
|
if (!tab) {
|
||||||
|
throw new Error('目标标签页已关闭,无法继续等待页面加载完成。');
|
||||||
|
}
|
||||||
|
if (tab.status === 'complete') {
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
await sleepWithStop(retryDelayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
|
async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
|
||||||
return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options);
|
return tabRuntime.ensureContentScriptReadyOnTab(source, tabId, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureContentScriptReadyOnTabUntilStopped(source, tabId, options = {}) {
|
||||||
|
const {
|
||||||
|
inject = null,
|
||||||
|
injectSource = null,
|
||||||
|
retryDelayMs = 700,
|
||||||
|
logMessage = '',
|
||||||
|
} = options;
|
||||||
|
let logged = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
throwIfStopped();
|
||||||
|
const pong = await pingContentScriptOnTab(tabId);
|
||||||
|
if (pong?.ok && (!pong.source || pong.source === source)) {
|
||||||
|
await registerTab(source, tabId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inject || !inject.length) {
|
||||||
|
throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (injectSource) {
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
func: (injectedSource) => {
|
||||||
|
window.__MULTIPAGE_SOURCE = injectedSource;
|
||||||
|
},
|
||||||
|
args: [injectSource],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
files: inject,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTabUntilStopped] inject failed for ${source}:`, error?.message || error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pongAfterInject = await pingContentScriptOnTab(tabId);
|
||||||
|
if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) {
|
||||||
|
await registerTab(source, tabId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logMessage && !logged) {
|
||||||
|
logged = true;
|
||||||
|
await addLog(logMessage, 'warn');
|
||||||
|
}
|
||||||
|
await sleepWithStop(retryDelayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Command Queue (for content scripts not yet ready)
|
// Command Queue (for content scripts not yet ready)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -3998,6 +4144,21 @@ function sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs = g
|
|||||||
return tabRuntime.sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs);
|
return tabRuntime.sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendTabMessageUntilStopped(tabId, source, message, options = {}) {
|
||||||
|
const retryDelayMs = Math.max(100, Math.floor(Number(options.retryDelayMs) || 300));
|
||||||
|
while (true) {
|
||||||
|
throwIfStopped();
|
||||||
|
try {
|
||||||
|
return await chrome.tabs.sendMessage(tabId, message);
|
||||||
|
} catch (error) {
|
||||||
|
if (!isRetryableContentScriptTransportError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
await sleepWithStop(retryDelayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function queueCommand(source, message, timeout = 15000) {
|
function queueCommand(source, message, timeout = 15000) {
|
||||||
return tabRuntime.queueCommand(source, message, timeout);
|
return tabRuntime.queueCommand(source, message, timeout);
|
||||||
}
|
}
|
||||||
@@ -4091,6 +4252,8 @@ function getSourceLabel(source) {
|
|||||||
'hotmail-api': 'Hotmail(API对接/本地助手)',
|
'hotmail-api': 'Hotmail(API对接/本地助手)',
|
||||||
'luckmail-api': 'LuckMail(API 购邮)',
|
'luckmail-api': 'LuckMail(API 购邮)',
|
||||||
'cloudflare-temp-email': 'Cloudflare Temp Email',
|
'cloudflare-temp-email': 'Cloudflare Temp Email',
|
||||||
|
'plus-checkout': 'Plus Checkout',
|
||||||
|
'paypal-flow': 'PayPal 授权页',
|
||||||
};
|
};
|
||||||
return labels[source] || source || '未知来源';
|
return labels[source] || source || '未知来源';
|
||||||
}
|
}
|
||||||
@@ -4296,26 +4459,45 @@ function isStepDoneStatus(status) {
|
|||||||
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFirstUnfinishedStep(statuses = {}) {
|
function getFirstUnfinishedStep(statuses = {}, stateOverride = null) {
|
||||||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getFirstUnfinishedStep) {
|
const state = stateOverride || {};
|
||||||
return loggingStatus.getFirstUnfinishedStep(statuses);
|
const activeStepIds = typeof getStepIdsForState === 'function'
|
||||||
}
|
? getStepIdsForState(state)
|
||||||
for (const step of STEP_IDS) {
|
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||||||
|
? STEP_IDS
|
||||||
|
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||||||
|
for (const step of activeStepIds) {
|
||||||
if (!isStepDoneStatus(statuses[step] || 'pending')) return step;
|
if (!isStepDoneStatus(statuses[step] || 'pending')) return step;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSavedProgress(statuses = {}) {
|
function hasSavedProgress(statuses = {}, stateOverride = null) {
|
||||||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.hasSavedProgress) {
|
const state = stateOverride || {};
|
||||||
return loggingStatus.hasSavedProgress(statuses);
|
const merged = { ...DEFAULT_STATE.stepStatuses, ...statuses };
|
||||||
}
|
const activeStepIds = typeof getStepIdsForState === 'function'
|
||||||
return Object.values({ ...DEFAULT_STATE.stepStatuses, ...statuses }).some((status) => status !== 'pending');
|
? getStepIdsForState(state)
|
||||||
|
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||||||
|
? STEP_IDS
|
||||||
|
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||||||
|
return activeStepIds.some((step) => (merged[step] || 'pending') !== 'pending');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDownstreamStateResets(step) {
|
function getDownstreamStateResets(step) {
|
||||||
|
const plusRuntimeResets = {
|
||||||
|
plusCheckoutTabId: null,
|
||||||
|
plusCheckoutUrl: null,
|
||||||
|
plusCheckoutCountry: 'DE',
|
||||||
|
plusCheckoutCurrency: 'EUR',
|
||||||
|
plusBillingCountryText: '',
|
||||||
|
plusBillingAddress: null,
|
||||||
|
plusPaypalApprovedAt: null,
|
||||||
|
plusReturnUrl: '',
|
||||||
|
};
|
||||||
|
|
||||||
if (step <= 1) {
|
if (step <= 1) {
|
||||||
return {
|
return {
|
||||||
|
...plusRuntimeResets,
|
||||||
oauthUrl: null,
|
oauthUrl: null,
|
||||||
sub2apiSessionId: null,
|
sub2apiSessionId: null,
|
||||||
sub2apiOAuthState: null,
|
sub2apiOAuthState: null,
|
||||||
@@ -4337,6 +4519,7 @@ function getDownstreamStateResets(step) {
|
|||||||
}
|
}
|
||||||
if (step === 2) {
|
if (step === 2) {
|
||||||
return {
|
return {
|
||||||
|
...plusRuntimeResets,
|
||||||
password: null,
|
password: null,
|
||||||
lastEmailTimestamp: null,
|
lastEmailTimestamp: null,
|
||||||
signupVerificationRequestedAt: null,
|
signupVerificationRequestedAt: null,
|
||||||
@@ -4350,6 +4533,7 @@ function getDownstreamStateResets(step) {
|
|||||||
}
|
}
|
||||||
if (step === 3 || step === 4) {
|
if (step === 3 || step === 4) {
|
||||||
return {
|
return {
|
||||||
|
...plusRuntimeResets,
|
||||||
lastEmailTimestamp: null,
|
lastEmailTimestamp: null,
|
||||||
signupVerificationRequestedAt: null,
|
signupVerificationRequestedAt: null,
|
||||||
loginVerificationRequestedAt: null,
|
loginVerificationRequestedAt: null,
|
||||||
@@ -4362,6 +4546,17 @@ function getDownstreamStateResets(step) {
|
|||||||
}
|
}
|
||||||
if (step === 5 || step === 6 || step === 7 || step === 8) {
|
if (step === 5 || step === 6 || step === 7 || step === 8) {
|
||||||
return {
|
return {
|
||||||
|
...(step <= 6 ? plusRuntimeResets : {}),
|
||||||
|
...(step === 7 ? {
|
||||||
|
plusBillingCountryText: '',
|
||||||
|
plusBillingAddress: null,
|
||||||
|
plusPaypalApprovedAt: null,
|
||||||
|
plusReturnUrl: '',
|
||||||
|
} : {}),
|
||||||
|
...(step === 8 ? {
|
||||||
|
plusPaypalApprovedAt: null,
|
||||||
|
plusReturnUrl: '',
|
||||||
|
} : {}),
|
||||||
lastLoginCode: null,
|
lastLoginCode: null,
|
||||||
loginVerificationRequestedAt: null,
|
loginVerificationRequestedAt: null,
|
||||||
oauthFlowDeadlineAt: null,
|
oauthFlowDeadlineAt: null,
|
||||||
@@ -4371,6 +4566,7 @@ function getDownstreamStateResets(step) {
|
|||||||
}
|
}
|
||||||
if (step === 9) {
|
if (step === 9) {
|
||||||
return {
|
return {
|
||||||
|
plusReturnUrl: '',
|
||||||
localhostUrl: null,
|
localhostUrl: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -4383,7 +4579,15 @@ async function invalidateDownstreamAfterStepRestart(step, options = {}) {
|
|||||||
const statuses = { ...(state.stepStatuses || {}) };
|
const statuses = { ...(state.stepStatuses || {}) };
|
||||||
const changedSteps = [];
|
const changedSteps = [];
|
||||||
|
|
||||||
for (let downstream = step + 1; downstream <= LAST_STEP_ID; downstream++) {
|
const activeStepIds = typeof getStepIdsForState === 'function'
|
||||||
|
? getStepIdsForState(state)
|
||||||
|
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||||||
|
? STEP_IDS
|
||||||
|
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||||||
|
for (const downstream of activeStepIds) {
|
||||||
|
if (downstream <= step) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (statuses[downstream] !== 'pending') {
|
if (statuses[downstream] !== 'pending') {
|
||||||
statuses[downstream] = 'pending';
|
statuses[downstream] = 'pending';
|
||||||
changedSteps.push(downstream);
|
changedSteps.push(downstream);
|
||||||
@@ -4412,22 +4616,26 @@ function clearStopRequest() {
|
|||||||
stopRequested = false;
|
stopRequested = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRunningSteps(statuses = {}) {
|
function getRunningSteps(statuses = {}, stateOverride = null) {
|
||||||
if (typeof loggingStatus !== 'undefined' && loggingStatus?.getRunningSteps) {
|
const state = stateOverride || {};
|
||||||
return loggingStatus.getRunningSteps(statuses);
|
const merged = { ...DEFAULT_STATE.stepStatuses, ...statuses };
|
||||||
}
|
const activeStepIds = typeof getStepIdsForState === 'function'
|
||||||
return Object.entries({ ...DEFAULT_STATE.stepStatuses, ...statuses })
|
? getStepIdsForState(state)
|
||||||
.filter(([, status]) => status === 'running')
|
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||||||
.map(([step]) => Number(step))
|
? STEP_IDS
|
||||||
|
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||||||
|
return activeStepIds
|
||||||
|
.filter((step) => merged[step] === 'running')
|
||||||
.sort((a, b) => a - b);
|
.sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferStoppedRecordStep(state = {}) {
|
function inferStoppedRecordStep(state = {}) {
|
||||||
const statuses = { ...DEFAULT_STATE.stepStatuses, ...(state?.stepStatuses || {}) };
|
const statuses = { ...DEFAULT_STATE.stepStatuses, ...(state?.stepStatuses || {}) };
|
||||||
const stepIds = Object.keys(statuses)
|
const stepIds = typeof getStepIdsForState === 'function'
|
||||||
.map((step) => Number(step))
|
? getStepIdsForState(state)
|
||||||
.filter(Number.isFinite)
|
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||||||
.sort((left, right) => left - right);
|
? STEP_IDS
|
||||||
|
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||||||
|
|
||||||
const runningSteps = stepIds.filter((step) => statuses[step] === 'running');
|
const runningSteps = stepIds.filter((step) => statuses[step] === 'running');
|
||||||
if (runningSteps.length) {
|
if (runningSteps.length) {
|
||||||
@@ -4928,8 +5136,13 @@ async function ensureManualInteractionAllowed(actionLabel) {
|
|||||||
|
|
||||||
async function skipStep(step) {
|
async function skipStep(step) {
|
||||||
const state = await ensureManualInteractionAllowed('跳过步骤');
|
const state = await ensureManualInteractionAllowed('跳过步骤');
|
||||||
|
const activeStepIds = typeof getStepIdsForState === 'function'
|
||||||
|
? getStepIdsForState(state)
|
||||||
|
: (typeof STEP_IDS !== 'undefined' && Array.isArray(STEP_IDS) && STEP_IDS.length
|
||||||
|
? STEP_IDS
|
||||||
|
: Array.from({ length: typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10 }, (_, index) => index + 1));
|
||||||
|
|
||||||
if (!Number.isInteger(step) || !STEP_IDS.includes(step)) {
|
if (!Number.isInteger(step) || !activeStepIds.includes(step)) {
|
||||||
throw new Error(`无效步骤:${step}`);
|
throw new Error(`无效步骤:${step}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4942,10 +5155,12 @@ async function skipStep(step) {
|
|||||||
throw new Error(`步骤 ${step} 已完成,无需再跳过。`);
|
throw new Error(`步骤 ${step} 已完成,无需再跳过。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step > 1) {
|
const currentIndex = activeStepIds.indexOf(step);
|
||||||
const prevStatus = statuses[step - 1];
|
if (currentIndex > 0) {
|
||||||
|
const prevStep = activeStepIds[currentIndex - 1];
|
||||||
|
const prevStatus = statuses[prevStep];
|
||||||
if (!isStepDoneStatus(prevStatus)) {
|
if (!isStepDoneStatus(prevStatus)) {
|
||||||
throw new Error(`请先完成步骤 ${step - 1},再跳过步骤 ${step}。`);
|
throw new Error(`请先完成步骤 ${prevStep},再跳过步骤 ${step}。`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5216,7 +5431,7 @@ 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, 9]);
|
const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8, 9]);
|
||||||
const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 10]);
|
const STEP_COMPLETION_SIGNAL_STEPS = new Set([3, 5, 10, 12]);
|
||||||
|
|
||||||
function waitForStepComplete(step, timeoutMs = 120000) {
|
function waitForStepComplete(step, timeoutMs = 120000) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -5262,11 +5477,15 @@ async function completeStepFromBackground(step, payload = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const completionState = step === LAST_STEP_ID ? await getState() : null;
|
const latestState = await getState();
|
||||||
|
const lastStepId = typeof getLastStepIdForState === 'function'
|
||||||
|
? getLastStepIdForState(latestState)
|
||||||
|
: (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10);
|
||||||
|
const completionState = step === lastStepId ? latestState : 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 === LAST_STEP_ID) {
|
if (step === lastStepId) {
|
||||||
await appendAndBroadcastAccountRunRecord('success', completionState);
|
await appendAndBroadcastAccountRunRecord('success', completionState);
|
||||||
}
|
}
|
||||||
notifyStepComplete(step, payload);
|
notifyStepComplete(step, payload);
|
||||||
@@ -5345,7 +5564,7 @@ async function executeStepViaCompletionSignal(step, timeoutMs = AUTO_RUN_SIGNAL_
|
|||||||
|
|
||||||
async function waitForRunningStepsToFinish(payload = {}) {
|
async function waitForRunningStepsToFinish(payload = {}) {
|
||||||
let currentState = await getState();
|
let currentState = await getState();
|
||||||
let runningSteps = getRunningSteps(currentState.stepStatuses);
|
let runningSteps = getRunningSteps(currentState.stepStatuses, currentState);
|
||||||
if (!runningSteps.length) {
|
if (!runningSteps.length) {
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
@@ -5356,14 +5575,14 @@ async function waitForRunningStepsToFinish(payload = {}) {
|
|||||||
while (runningSteps.length) {
|
while (runningSteps.length) {
|
||||||
await sleepWithStop(250);
|
await sleepWithStop(250);
|
||||||
currentState = await getState();
|
currentState = await getState();
|
||||||
runningSteps = getRunningSteps(currentState.stepStatuses);
|
runningSteps = getRunningSteps(currentState.stepStatuses, currentState);
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog('自动继续:当前运行步骤已结束,准备按最新进度继续自动流程...', 'info');
|
await addLog('自动继续:当前运行步骤已结束,准备按最新进度继续自动流程...', 'info');
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
|
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10, 11, 12]);
|
||||||
let activeTopLevelAuthChainExecution = null;
|
let activeTopLevelAuthChainExecution = null;
|
||||||
|
|
||||||
function isAuthChainStep(step) {
|
function isAuthChainStep(step) {
|
||||||
@@ -5418,7 +5637,7 @@ async function acquireTopLevelAuthChainExecution(step) {
|
|||||||
|
|
||||||
async function markRunningStepsStopped() {
|
async function markRunningStepsStopped() {
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
const runningSteps = getRunningSteps(state.stepStatuses);
|
const runningSteps = getRunningSteps(state.stepStatuses, state);
|
||||||
|
|
||||||
for (const step of runningSteps) {
|
for (const step of runningSteps) {
|
||||||
await setStepStatus(step, 'stopped');
|
await setStepStatus(step, 'stopped');
|
||||||
@@ -5428,7 +5647,7 @@ async function markRunningStepsStopped() {
|
|||||||
async function requestStop(options = {}) {
|
async function requestStop(options = {}) {
|
||||||
const { logMessage = '已收到停止请求,正在取消当前操作...' } = options;
|
const { logMessage = '已收到停止请求,正在取消当前操作...' } = options;
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
const runningSteps = getRunningSteps(state.stepStatuses);
|
const runningSteps = getRunningSteps(state.stepStatuses, state);
|
||||||
const inferredStopStep = inferStoppedRecordStep(state);
|
const inferredStopStep = inferStoppedRecordStep(state);
|
||||||
const timerPlan = getPendingAutoRunTimerPlan(state);
|
const timerPlan = getPendingAutoRunTimerPlan(state);
|
||||||
|
|
||||||
@@ -5531,7 +5750,15 @@ async function executeStep(step, options = {}) {
|
|||||||
await setState({ flowStartTime: Date.now() });
|
await setState({ flowStartTime: Date.now() });
|
||||||
}
|
}
|
||||||
|
|
||||||
await stepRegistry.executeStep(step, state);
|
const activeStepRegistry = getStepRegistryForState(state);
|
||||||
|
if (!activeStepRegistry?.getStepDefinition?.(step)) {
|
||||||
|
throw new Error(`当前模式下不存在步骤:${step}`);
|
||||||
|
}
|
||||||
|
await activeStepRegistry.executeStep(step, {
|
||||||
|
...state,
|
||||||
|
visibleStep: Number(step),
|
||||||
|
stepDefinition: getStepDefinitionForState(step, state),
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
executionError = err;
|
executionError = err;
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
@@ -6266,8 +6493,14 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
|
|||||||
|
|
||||||
let restartFromStep1WithCurrentEmail = false;
|
let restartFromStep1WithCurrentEmail = false;
|
||||||
let step = Math.max(currentStartStep, 4);
|
let step = Math.max(currentStartStep, 4);
|
||||||
while (step <= LAST_STEP_ID) {
|
while (step <= (typeof getLastStepIdForState === 'function'
|
||||||
|
? getLastStepIdForState(await getState())
|
||||||
|
: (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10))) {
|
||||||
const latestState = await getState();
|
const latestState = await getState();
|
||||||
|
if (typeof getStepDefinitionForState === 'function' && !getStepDefinitionForState(step, latestState)) {
|
||||||
|
step += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const currentStatus = latestState.stepStatuses?.[step] || 'pending';
|
const currentStatus = latestState.stepStatuses?.[step] || 'pending';
|
||||||
if (isStepDoneStatus(currentStatus)) {
|
if (isStepDoneStatus(currentStatus)) {
|
||||||
await addLog(`自动运行:步骤 ${step} 当前状态为 ${currentStatus},将直接继续后续流程。`, 'info');
|
await addLog(`自动运行:步骤 ${step} 当前状态为 ${currentStatus},将直接继续后续流程。`, 'info');
|
||||||
@@ -6317,6 +6550,11 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
|
|||||||
const restartDecision = await getPostStep6AutoRestartDecision(step, err);
|
const restartDecision = await getPostStep6AutoRestartDecision(step, err);
|
||||||
if (restartDecision.shouldRestart) {
|
if (restartDecision.shouldRestart) {
|
||||||
postStep7RestartCount += 1;
|
postStep7RestartCount += 1;
|
||||||
|
const restartStep = restartDecision.restartStep
|
||||||
|
|| (typeof getAuthChainStartStepId === 'function'
|
||||||
|
? getAuthChainStartStepId(await getState())
|
||||||
|
: FINAL_OAUTH_CHAIN_START_STEP);
|
||||||
|
const resetAfterStep = Math.max(1, restartStep - 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
|
||||||
@@ -6325,13 +6563,13 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
|
|||||||
? `当前认证页:${authStateLabel}`
|
? `当前认证页:${authStateLabel}`
|
||||||
: '未获取到认证页状态';
|
: '未获取到认证页状态';
|
||||||
await addLog(
|
await addLog(
|
||||||
`步骤 ${step}:检测到报错且当前未进入 add-phone,正在回到步骤 7 重新开始授权流程(第 ${postStep7RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`,
|
`步骤 ${step}:检测到报错且当前未进入 add-phone,正在回到步骤 ${restartStep} 重新开始授权流程(第 ${postStep7RestartCount} 次重开)。${authStateSuffix};原因:${restartDecision.errorMessage || '未知错误'}`,
|
||||||
'warn'
|
'warn'
|
||||||
);
|
);
|
||||||
await invalidateDownstreamAfterStepRestart(6, {
|
await invalidateDownstreamAfterStepRestart(resetAfterStep, {
|
||||||
logLabel: `步骤 ${step} 报错后准备回到步骤 7 重试(第 ${postStep7RestartCount} 次重开)`,
|
logLabel: `步骤 ${step} 报错后准备回到步骤 ${restartStep} 重试(第 ${postStep7RestartCount} 次重开)`,
|
||||||
});
|
});
|
||||||
step = 7;
|
step = restartStep;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6664,6 +6902,54 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
|
|||||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
|
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
|
||||||
throwIfStopped,
|
throwIfStopped,
|
||||||
});
|
});
|
||||||
|
const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.createPlusCheckoutCreateExecutor({
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureContentScriptReadyOnTabUntilStopped,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
sendTabMessageUntilStopped,
|
||||||
|
setState,
|
||||||
|
sleepWithStop,
|
||||||
|
waitForTabCompleteUntilStopped,
|
||||||
|
});
|
||||||
|
const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?.createPlusCheckoutBillingExecutor({
|
||||||
|
addLog,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureContentScriptReadyOnTabUntilStopped,
|
||||||
|
generateRandomName,
|
||||||
|
getAddressSeedForCountry: self.MultiPageAddressSources?.getAddressSeedForCountry,
|
||||||
|
getTabId,
|
||||||
|
isTabAlive,
|
||||||
|
sendTabMessageUntilStopped,
|
||||||
|
setState,
|
||||||
|
sleepWithStop,
|
||||||
|
waitForTabCompleteUntilStopped,
|
||||||
|
waitForTabUrlMatchUntilStopped,
|
||||||
|
});
|
||||||
|
const payPalApproveExecutor = self.MultiPageBackgroundPayPalApprove?.createPayPalApproveExecutor({
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureContentScriptReadyOnTabUntilStopped,
|
||||||
|
getTabId,
|
||||||
|
isTabAlive,
|
||||||
|
sendTabMessageUntilStopped,
|
||||||
|
setState,
|
||||||
|
sleepWithStop,
|
||||||
|
waitForTabCompleteUntilStopped,
|
||||||
|
waitForTabUrlMatchUntilStopped,
|
||||||
|
});
|
||||||
|
const plusReturnConfirmExecutor = self.MultiPageBackgroundPlusReturnConfirm?.createPlusReturnConfirmExecutor({
|
||||||
|
addLog,
|
||||||
|
completeStepFromBackground,
|
||||||
|
getTabId,
|
||||||
|
isTabAlive,
|
||||||
|
setState,
|
||||||
|
sleepWithStop,
|
||||||
|
waitForTabCompleteUntilStopped,
|
||||||
|
waitForTabUrlMatchUntilStopped,
|
||||||
|
});
|
||||||
const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({
|
const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({
|
||||||
addLog,
|
addLog,
|
||||||
chrome,
|
chrome,
|
||||||
@@ -6683,7 +6969,6 @@ const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({
|
|||||||
shouldBypassStep9ForLocalCpa,
|
shouldBypassStep9ForLocalCpa,
|
||||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
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),
|
||||||
@@ -6691,6 +6976,10 @@ const stepExecutorsByKey = {
|
|||||||
'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),
|
||||||
'clear-login-cookies': () => step6Executor.executeStep6(),
|
'clear-login-cookies': () => step6Executor.executeStep6(),
|
||||||
|
'plus-checkout-create': (state) => plusCheckoutCreateExecutor.executePlusCheckoutCreate(state),
|
||||||
|
'plus-checkout-billing': (state) => plusCheckoutBillingExecutor.executePlusCheckoutBilling(state),
|
||||||
|
'paypal-approve': (state) => payPalApproveExecutor.executePayPalApprove(state),
|
||||||
|
'plus-checkout-return': (state) => plusReturnConfirmExecutor.executePlusReturnConfirm(state),
|
||||||
'oauth-login': (state) => step7Executor.executeStep7(state),
|
'oauth-login': (state) => step7Executor.executeStep7(state),
|
||||||
'fetch-login-code': (state) => step8Executor.executeStep8(state),
|
'fetch-login-code': (state) => step8Executor.executeStep8(state),
|
||||||
'confirm-oauth': (state) => step9Executor.executeStep9(state),
|
'confirm-oauth': (state) => step9Executor.executeStep9(state),
|
||||||
@@ -6741,6 +7030,9 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
|||||||
getPendingAutoRunTimerPlan,
|
getPendingAutoRunTimerPlan,
|
||||||
getSourceLabel,
|
getSourceLabel,
|
||||||
getState,
|
getState,
|
||||||
|
getStepDefinitionForState,
|
||||||
|
getStepIdsForState,
|
||||||
|
getLastStepIdForState,
|
||||||
getTabId,
|
getTabId,
|
||||||
getStopRequested: () => stopRequested,
|
getStopRequested: () => stopRequested,
|
||||||
handleCloudflareSecurityBlocked,
|
handleCloudflareSecurityBlocked,
|
||||||
@@ -6798,12 +7090,22 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
|||||||
upsertHotmailAccount,
|
upsertHotmailAccount,
|
||||||
verifyHotmailAccount,
|
verifyHotmailAccount,
|
||||||
});
|
});
|
||||||
const stepRegistry = self.MultiPageBackgroundStepRegistry?.createStepRegistry(
|
|
||||||
stepDefinitions.map((definition) => ({
|
function buildStepRegistry(definitions = []) {
|
||||||
...definition,
|
return self.MultiPageBackgroundStepRegistry?.createStepRegistry(
|
||||||
execute: stepExecutorsByKey[definition.key],
|
definitions.map((definition) => ({
|
||||||
}))
|
...definition,
|
||||||
);
|
execute: stepExecutorsByKey[definition.key],
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalStepRegistry = buildStepRegistry(NORMAL_STEP_DEFINITIONS);
|
||||||
|
const plusStepRegistry = buildStepRegistry(PLUS_STEP_DEFINITIONS);
|
||||||
|
|
||||||
|
function getStepRegistryForState(state = {}) {
|
||||||
|
return isPlusModeState(state) ? plusStepRegistry : normalStepRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
async function requestOAuthUrlFromPanel(state, options = {}) {
|
async function requestOAuthUrlFromPanel(state, options = {}) {
|
||||||
return panelBridge.requestOAuthUrlFromPanel(state, options);
|
return panelBridge.requestOAuthUrlFromPanel(state, options);
|
||||||
@@ -7262,11 +7564,19 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
|||||||
const normalizedStep = Number(step);
|
const normalizedStep = Number(step);
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
const shouldForceRestartFromStep7 = /restart step 7 with a new number/i.test(errorMessage);
|
const shouldForceRestartFromStep7 = /restart step 7 with a new number/i.test(errorMessage);
|
||||||
if (!Number.isFinite(normalizedStep) || normalizedStep < 7 || normalizedStep > LAST_STEP_ID) {
|
const latestState = await getState();
|
||||||
|
const authChainStartStep = typeof getAuthChainStartStepId === 'function'
|
||||||
|
? getAuthChainStartStepId(latestState)
|
||||||
|
: FINAL_OAUTH_CHAIN_START_STEP;
|
||||||
|
const lastStepId = typeof getLastStepIdForState === 'function'
|
||||||
|
? getLastStepIdForState(latestState)
|
||||||
|
: (typeof LAST_STEP_ID === 'number' ? LAST_STEP_ID : 10);
|
||||||
|
if (!Number.isFinite(normalizedStep) || normalizedStep < authChainStartStep || normalizedStep > lastStepId) {
|
||||||
return {
|
return {
|
||||||
shouldRestart: false,
|
shouldRestart: false,
|
||||||
blockedByAddPhone: false,
|
blockedByAddPhone: false,
|
||||||
forcedByPhoneVerificationTimeout: false,
|
forcedByPhoneVerificationTimeout: false,
|
||||||
|
restartStep: authChainStartStep,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
authState: null,
|
authState: null,
|
||||||
};
|
};
|
||||||
@@ -7277,6 +7587,7 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
|||||||
shouldRestart: true,
|
shouldRestart: true,
|
||||||
blockedByAddPhone: false,
|
blockedByAddPhone: false,
|
||||||
forcedByPhoneVerificationTimeout: true,
|
forcedByPhoneVerificationTimeout: true,
|
||||||
|
restartStep: authChainStartStep,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
authState: null,
|
authState: null,
|
||||||
};
|
};
|
||||||
@@ -7287,6 +7598,7 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
|||||||
shouldRestart: false,
|
shouldRestart: false,
|
||||||
blockedByAddPhone: true,
|
blockedByAddPhone: true,
|
||||||
forcedByPhoneVerificationTimeout: false,
|
forcedByPhoneVerificationTimeout: false,
|
||||||
|
restartStep: authChainStartStep,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
authState: null,
|
authState: null,
|
||||||
};
|
};
|
||||||
@@ -7310,6 +7622,7 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
|||||||
shouldRestart: false,
|
shouldRestart: false,
|
||||||
blockedByAddPhone: true,
|
blockedByAddPhone: true,
|
||||||
forcedByPhoneVerificationTimeout: false,
|
forcedByPhoneVerificationTimeout: false,
|
||||||
|
restartStep: authChainStartStep,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
authState,
|
authState,
|
||||||
};
|
};
|
||||||
@@ -7319,6 +7632,7 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
|||||||
shouldRestart: true,
|
shouldRestart: true,
|
||||||
blockedByAddPhone: false,
|
blockedByAddPhone: false,
|
||||||
forcedByPhoneVerificationTimeout: false,
|
forcedByPhoneVerificationTimeout: false,
|
||||||
|
restartStep: authChainStartStep,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
authState,
|
authState,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -311,7 +311,7 @@
|
|||||||
|
|
||||||
let successfulRuns = roundSummaries.filter((item) => item.status === 'success').length;
|
let successfulRuns = roundSummaries.filter((item) => item.status === 'success').length;
|
||||||
const initialState = await getState();
|
const initialState = await getState();
|
||||||
const initialPhase = continueCurrentOnFirstAttempt && getRunningSteps(initialState.stepStatuses).length
|
const initialPhase = continueCurrentOnFirstAttempt && getRunningSteps(initialState.stepStatuses, initialState).length
|
||||||
? 'waiting_step'
|
? 'waiting_step'
|
||||||
: 'running';
|
: 'running';
|
||||||
const showResumePosition = continueCurrentOnFirstAttempt || resumeCurrentRun > 1 || resumeAttemptRun > 1;
|
const showResumePosition = continueCurrentOnFirstAttempt || resumeCurrentRun > 1 || resumeAttemptRun > 1;
|
||||||
@@ -351,18 +351,18 @@
|
|||||||
|
|
||||||
if (reuseExistingProgress) {
|
if (reuseExistingProgress) {
|
||||||
let currentState = await getState();
|
let currentState = await getState();
|
||||||
if (getRunningSteps(currentState.stepStatuses).length) {
|
if (getRunningSteps(currentState.stepStatuses, currentState).length) {
|
||||||
currentState = await waitForRunningStepsToFinish({
|
currentState = await waitForRunningStepsToFinish({
|
||||||
currentRun: targetRun,
|
currentRun: targetRun,
|
||||||
totalRuns,
|
totalRuns,
|
||||||
attemptRun,
|
attemptRun,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const resumeStep = getFirstUnfinishedStep(currentState.stepStatuses);
|
const resumeStep = getFirstUnfinishedStep(currentState.stepStatuses, currentState);
|
||||||
if (resumeStep && hasSavedProgress(currentState.stepStatuses)) {
|
if (resumeStep && hasSavedProgress(currentState.stepStatuses, currentState)) {
|
||||||
startStep = resumeStep;
|
startStep = resumeStep;
|
||||||
useExistingProgress = true;
|
useExistingProgress = true;
|
||||||
} else if (hasSavedProgress(currentState.stepStatuses)) {
|
} else if (hasSavedProgress(currentState.stepStatuses, currentState)) {
|
||||||
await addLog('检测到当前流程已处理完成,本轮将改为从步骤 1 重新开始。', 'info');
|
await addLog('检测到当前流程已处理完成,本轮将改为从步骤 1 重新开始。', 'info');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,6 +373,9 @@
|
|||||||
vpsUrl: prevState.vpsUrl,
|
vpsUrl: prevState.vpsUrl,
|
||||||
vpsPassword: prevState.vpsPassword,
|
vpsPassword: prevState.vpsPassword,
|
||||||
customPassword: prevState.customPassword,
|
customPassword: prevState.customPassword,
|
||||||
|
plusModeEnabled: prevState.plusModeEnabled,
|
||||||
|
paypalEmail: prevState.paypalEmail,
|
||||||
|
paypalPassword: prevState.paypalPassword,
|
||||||
autoRunSkipFailures: prevState.autoRunSkipFailures,
|
autoRunSkipFailures: prevState.autoRunSkipFailures,
|
||||||
autoRunFallbackThreadIntervalMinutes: prevState.autoRunFallbackThreadIntervalMinutes,
|
autoRunFallbackThreadIntervalMinutes: prevState.autoRunFallbackThreadIntervalMinutes,
|
||||||
autoRunDelayEnabled: prevState.autoRunDelayEnabled,
|
autoRunDelayEnabled: prevState.autoRunDelayEnabled,
|
||||||
|
|||||||
+109
-62
@@ -40,6 +40,9 @@
|
|||||||
getPendingAutoRunTimerPlan,
|
getPendingAutoRunTimerPlan,
|
||||||
getSourceLabel,
|
getSourceLabel,
|
||||||
getState,
|
getState,
|
||||||
|
getStepDefinitionForState,
|
||||||
|
getStepIdsForState,
|
||||||
|
getLastStepIdForState,
|
||||||
getTabId,
|
getTabId,
|
||||||
getStopRequested,
|
getStopRequested,
|
||||||
handleAutoRunLoopUnhandledError,
|
handleAutoRunLoopUnhandledError,
|
||||||
@@ -127,7 +130,86 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStepKeyForState(step, state = {}) {
|
||||||
|
if (typeof getStepDefinitionForState === 'function') {
|
||||||
|
return String(getStepDefinitionForState(step, state)?.key || '').trim();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePlatformVerifyStepData(payload) {
|
||||||
|
if (payload.localhostUrl) {
|
||||||
|
await closeLocalhostCallbackTabs(payload.localhostUrl);
|
||||||
|
}
|
||||||
|
const latestState = await getState();
|
||||||
|
if (latestState.currentHotmailAccountId && isHotmailProvider(latestState)) {
|
||||||
|
await patchHotmailAccount(latestState.currentHotmailAccountId, {
|
||||||
|
used: true,
|
||||||
|
lastUsedAt: Date.now(),
|
||||||
|
});
|
||||||
|
await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok');
|
||||||
|
}
|
||||||
|
if (String(latestState.mailProvider || '').trim().toLowerCase() === '2925' && latestState.currentMail2925AccountId) {
|
||||||
|
await patchMail2925Account(latestState.currentMail2925AccountId, {
|
||||||
|
lastUsedAt: Date.now(),
|
||||||
|
lastError: '',
|
||||||
|
});
|
||||||
|
await addLog('当前 2925 账号已记录最近使用时间。', 'ok');
|
||||||
|
}
|
||||||
|
if (isLuckmailProvider(latestState)) {
|
||||||
|
const currentPurchase = getCurrentLuckmailPurchase(latestState);
|
||||||
|
if (currentPurchase?.id) {
|
||||||
|
await setLuckmailPurchaseUsedState(currentPurchase.id, true);
|
||||||
|
await addLog(`当前 LuckMail 邮箱 ${currentPurchase.email_address} 已在本地标记为已用。`, 'ok');
|
||||||
|
}
|
||||||
|
await clearLuckmailRuntimeState({ clearEmail: true });
|
||||||
|
await addLog('当前 LuckMail 邮箱运行态已清空,下轮将优先复用未用邮箱或重新购买邮箱。', 'ok');
|
||||||
|
}
|
||||||
|
const localhostPrefix = buildLocalhostCleanupPrefix(payload.localhostUrl);
|
||||||
|
if (localhostPrefix) {
|
||||||
|
await closeTabsByUrlPrefix(localhostPrefix, {
|
||||||
|
excludeUrls: [payload.localhostUrl],
|
||||||
|
excludeLocalhostCallbacks: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleStepData(step, payload) {
|
async function handleStepData(step, payload) {
|
||||||
|
const stateForStep = await getState();
|
||||||
|
const stepKey = getStepKeyForState(step, stateForStep);
|
||||||
|
|
||||||
|
if (stepKey === 'oauth-login') {
|
||||||
|
if (payload.loginVerificationRequestedAt) {
|
||||||
|
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepKey === 'fetch-login-code') {
|
||||||
|
await setState({
|
||||||
|
lastEmailTimestamp: payload.emailTimestamp || null,
|
||||||
|
loginVerificationRequestedAt: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepKey === 'confirm-oauth') {
|
||||||
|
if (payload.localhostUrl) {
|
||||||
|
if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) {
|
||||||
|
throw new Error(`步骤 ${step} 返回了无效的 localhost OAuth 回调地址。`);
|
||||||
|
}
|
||||||
|
await setState({ localhostUrl: payload.localhostUrl });
|
||||||
|
broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepKey === 'platform-verify') {
|
||||||
|
await handlePlatformVerifyStepData(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 1: {
|
case 1: {
|
||||||
const updates = {};
|
const updates = {};
|
||||||
@@ -181,11 +263,6 @@
|
|||||||
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
|
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 7:
|
|
||||||
if (payload.loginVerificationRequestedAt) {
|
|
||||||
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 4:
|
case 4:
|
||||||
await setState({
|
await setState({
|
||||||
lastEmailTimestamp: payload.emailTimestamp || null,
|
lastEmailTimestamp: payload.emailTimestamp || null,
|
||||||
@@ -200,59 +277,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 8:
|
|
||||||
await setState({
|
|
||||||
lastEmailTimestamp: payload.emailTimestamp || null,
|
|
||||||
loginVerificationRequestedAt: null,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 9:
|
|
||||||
if (payload.localhostUrl) {
|
|
||||||
if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) {
|
|
||||||
throw new Error('步骤 9 返回了无效的 localhost OAuth 回调地址。');
|
|
||||||
}
|
|
||||||
await setState({ localhostUrl: payload.localhostUrl });
|
|
||||||
broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 10: {
|
|
||||||
if (payload.localhostUrl) {
|
|
||||||
await closeLocalhostCallbackTabs(payload.localhostUrl);
|
|
||||||
}
|
|
||||||
const latestState = await getState();
|
|
||||||
if (latestState.currentHotmailAccountId && isHotmailProvider(latestState)) {
|
|
||||||
await patchHotmailAccount(latestState.currentHotmailAccountId, {
|
|
||||||
used: true,
|
|
||||||
lastUsedAt: Date.now(),
|
|
||||||
});
|
|
||||||
await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok');
|
|
||||||
}
|
|
||||||
if (String(latestState.mailProvider || '').trim().toLowerCase() === '2925' && latestState.currentMail2925AccountId) {
|
|
||||||
await patchMail2925Account(latestState.currentMail2925AccountId, {
|
|
||||||
lastUsedAt: Date.now(),
|
|
||||||
lastError: '',
|
|
||||||
});
|
|
||||||
await addLog('当前 2925 账号已记录最近使用时间。', 'ok');
|
|
||||||
}
|
|
||||||
if (isLuckmailProvider(latestState)) {
|
|
||||||
const currentPurchase = getCurrentLuckmailPurchase(latestState);
|
|
||||||
if (currentPurchase?.id) {
|
|
||||||
await setLuckmailPurchaseUsedState(currentPurchase.id, true);
|
|
||||||
await addLog(`当前 LuckMail 邮箱 ${currentPurchase.email_address} 已在本地标记为已用。`, 'ok');
|
|
||||||
}
|
|
||||||
await clearLuckmailRuntimeState({ clearEmail: true });
|
|
||||||
await addLog('当前 LuckMail 邮箱运行态已清空,下轮将优先复用未用邮箱或重新购买邮箱。', 'ok');
|
|
||||||
}
|
|
||||||
const localhostPrefix = buildLocalhostCleanupPrefix(payload.localhostUrl);
|
|
||||||
if (localhostPrefix) {
|
|
||||||
await closeTabsByUrlPrefix(localhostPrefix, {
|
|
||||||
excludeUrls: [payload.localhostUrl],
|
|
||||||
excludeLocalhostCallbacks: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -303,11 +327,15 @@
|
|||||||
return { ok: true, error: errorMessage };
|
return { ok: true, error: errorMessage };
|
||||||
}
|
}
|
||||||
|
|
||||||
const completionState = message.step === 10 ? await getState() : null;
|
const completionStateCandidate = await getState();
|
||||||
|
const lastStepId = typeof getLastStepIdForState === 'function'
|
||||||
|
? getLastStepIdForState(completionStateCandidate)
|
||||||
|
: 10;
|
||||||
|
const completionState = message.step === lastStepId ? completionStateCandidate : 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 === 10 && typeof appendAccountRunRecord === 'function') {
|
if (message.step === lastStepId && typeof appendAccountRunRecord === 'function') {
|
||||||
await appendAccountRunRecord('success', completionState);
|
await appendAccountRunRecord('success', completionState);
|
||||||
}
|
}
|
||||||
notifyStepComplete(message.step, message.payload);
|
notifyStepComplete(message.step, message.payload);
|
||||||
@@ -561,13 +589,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'SAVE_SETTING': {
|
case 'SAVE_SETTING': {
|
||||||
|
const currentState = await getState();
|
||||||
const updates = buildPersistentSettingsPayload(message.payload || {});
|
const updates = buildPersistentSettingsPayload(message.payload || {});
|
||||||
const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {});
|
const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {});
|
||||||
|
const modeChanged = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
|
||||||
|
&& Boolean(currentState?.plusModeEnabled) !== Boolean(updates.plusModeEnabled);
|
||||||
await setPersistentSettings(updates);
|
await setPersistentSettings(updates);
|
||||||
await setState({
|
const stateUpdates = {
|
||||||
...updates,
|
...updates,
|
||||||
...sessionUpdates,
|
...sessionUpdates,
|
||||||
});
|
};
|
||||||
|
if (modeChanged && typeof getStepIdsForState === 'function') {
|
||||||
|
const nextStateForSteps = { ...currentState, ...stateUpdates };
|
||||||
|
stateUpdates.stepStatuses = Object.fromEntries(
|
||||||
|
getStepIdsForState(nextStateForSteps).map((stepId) => [stepId, 'pending'])
|
||||||
|
);
|
||||||
|
stateUpdates.currentStep = 0;
|
||||||
|
}
|
||||||
|
await setState(stateUpdates);
|
||||||
|
if (modeChanged) {
|
||||||
|
await addLog(
|
||||||
|
Boolean(updates.plusModeEnabled)
|
||||||
|
? 'Plus 模式已开启,已切换为 Plus Checkout + PayPal 步骤。'
|
||||||
|
: 'Plus 模式已关闭,已恢复普通注册授权步骤。',
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
}
|
||||||
return { ok: true, state: await getState() };
|
return { ok: true, state: await getState() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,16 +33,23 @@
|
|||||||
setStep8TabUpdatedListener,
|
setStep8TabUpdatedListener,
|
||||||
} = deps;
|
} = deps;
|
||||||
|
|
||||||
|
function getVisibleStep(state, fallback = 9) {
|
||||||
|
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||||
|
return visibleStep > 0 ? visibleStep : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
async function executeStep9(state) {
|
async function executeStep9(state) {
|
||||||
|
const visibleStep = getVisibleStep(state, 9);
|
||||||
if (!state.oauthUrl) {
|
if (!state.oauthUrl) {
|
||||||
throw new Error('缺少登录用 OAuth 链接,请先完成步骤 7。');
|
const authLoginStep = visibleStep === 11 ? 10 : 7;
|
||||||
|
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog('步骤 9:正在监听 localhost 回调地址...');
|
await addLog(`步骤 ${visibleStep}:正在监听 localhost 回调地址...`);
|
||||||
|
|
||||||
const callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
const callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||||
? await getOAuthFlowStepTimeoutMs(240000, {
|
? await getOAuthFlowStepTimeoutMs(240000, {
|
||||||
step: 9,
|
step: visibleStep,
|
||||||
actionLabel: 'OAuth localhost 回调',
|
actionLabel: 'OAuth localhost 回调',
|
||||||
})
|
})
|
||||||
: 240000;
|
: 240000;
|
||||||
@@ -71,8 +78,8 @@
|
|||||||
cleanupListener();
|
cleanupListener();
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|
||||||
addLog(`步骤 9:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => {
|
addLog(`步骤 ${visibleStep}:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => {
|
||||||
return completeStepFromBackground(9, { localhostUrl: callbackUrl });
|
return completeStepFromBackground(visibleStep, { localhostUrl: callbackUrl });
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
resolve();
|
resolve();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
@@ -81,7 +88,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
rejectStep9(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 9 的点击可能被拦截了。'));
|
rejectStep9(new Error(`${Math.round(callbackTimeoutMs / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`));
|
||||||
}, callbackTimeoutMs);
|
}, callbackTimeoutMs);
|
||||||
|
|
||||||
setStep8PendingReject((error) => {
|
setStep8PendingReject((error) => {
|
||||||
@@ -111,10 +118,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('步骤 9:已切回认证页,正在准备调试器点击...');
|
await addLog(`步骤 ${visibleStep}:已切回认证页,正在准备调试器点击...`);
|
||||||
} else {
|
} else {
|
||||||
signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
|
signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||||
await addLog('步骤 9:已重新打开认证页,正在准备调试器点击...');
|
await addLog(`步骤 ${visibleStep}:已重新打开认证页,正在准备调试器点击...`);
|
||||||
}
|
}
|
||||||
|
|
||||||
throwIfStep8SettledOrStopped(resolved);
|
throwIfStep8SettledOrStopped(resolved);
|
||||||
@@ -124,11 +131,11 @@
|
|||||||
await ensureStep8SignupPageReady(signupTabId, {
|
await ensureStep8SignupPageReady(signupTabId, {
|
||||||
timeoutMs: typeof getOAuthFlowStepTimeoutMs === 'function'
|
timeoutMs: typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||||
? await getOAuthFlowStepTimeoutMs(15000, {
|
? await getOAuthFlowStepTimeoutMs(15000, {
|
||||||
step: 9,
|
step: visibleStep,
|
||||||
actionLabel: '等待 OAuth 同意页内容脚本就绪',
|
actionLabel: '等待 OAuth 同意页内容脚本就绪',
|
||||||
})
|
})
|
||||||
: 15000,
|
: 15000,
|
||||||
logMessage: '步骤 9:认证页内容脚本尚未就绪,正在等待页面恢复...',
|
logMessage: `步骤 ${visibleStep}:认证页内容脚本尚未就绪,正在等待页面恢复...`,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) {
|
for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) {
|
||||||
@@ -137,7 +144,7 @@
|
|||||||
signupTabId,
|
signupTabId,
|
||||||
typeof getOAuthFlowStepTimeoutMs === 'function'
|
typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||||
? await getOAuthFlowStepTimeoutMs(STEP8_READY_WAIT_TIMEOUT_MS, {
|
? await getOAuthFlowStepTimeoutMs(STEP8_READY_WAIT_TIMEOUT_MS, {
|
||||||
step: 9,
|
step: visibleStep,
|
||||||
actionLabel: '等待 OAuth 同意页出现',
|
actionLabel: '等待 OAuth 同意页出现',
|
||||||
})
|
})
|
||||||
: STEP8_READY_WAIT_TIMEOUT_MS
|
: STEP8_READY_WAIT_TIMEOUT_MS
|
||||||
@@ -149,12 +156,12 @@
|
|||||||
|
|
||||||
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(`步骤 9:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`);
|
await addLog(`步骤 ${visibleStep}:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`);
|
||||||
|
|
||||||
if (strategy.mode === 'debugger') {
|
if (strategy.mode === 'debugger') {
|
||||||
const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||||
? await getOAuthFlowStepTimeoutMs(15000, {
|
? await getOAuthFlowStepTimeoutMs(15000, {
|
||||||
step: 9,
|
step: visibleStep,
|
||||||
actionLabel: '定位 OAuth 同意页继续按钮',
|
actionLabel: '定位 OAuth 同意页继续按钮',
|
||||||
})
|
})
|
||||||
: 15000;
|
: 15000;
|
||||||
@@ -167,7 +174,7 @@
|
|||||||
} else {
|
} else {
|
||||||
const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||||
? await getOAuthFlowStepTimeoutMs(15000, {
|
? await getOAuthFlowStepTimeoutMs(15000, {
|
||||||
step: 9,
|
step: visibleStep,
|
||||||
actionLabel: '点击 OAuth 同意页继续按钮',
|
actionLabel: '点击 OAuth 同意页继续按钮',
|
||||||
})
|
})
|
||||||
: 15000;
|
: 15000;
|
||||||
@@ -186,7 +193,7 @@
|
|||||||
pageState.url,
|
pageState.url,
|
||||||
typeof getOAuthFlowStepTimeoutMs === 'function'
|
typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||||
? await getOAuthFlowStepTimeoutMs(15000, {
|
? await getOAuthFlowStepTimeoutMs(15000, {
|
||||||
step: 9,
|
step: visibleStep,
|
||||||
actionLabel: '等待 OAuth 同意页点击生效',
|
actionLabel: '等待 OAuth 同意页点击生效',
|
||||||
})
|
})
|
||||||
: 15000
|
: 15000
|
||||||
@@ -196,20 +203,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (effect.progressed) {
|
if (effect.progressed) {
|
||||||
await addLog(`步骤 9:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info');
|
await addLog(`步骤 ${visibleStep}:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (round >= STEP8_MAX_ROUNDS) {
|
if (round >= STEP8_MAX_ROUNDS) {
|
||||||
throw new Error(`步骤 9:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
|
throw new Error(`步骤 ${visibleStep}:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog(`步骤 9:${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn');
|
await addLog(`步骤 ${visibleStep}:${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn');
|
||||||
await reloadStep8ConsentPage(
|
await reloadStep8ConsentPage(
|
||||||
signupTabId,
|
signupTabId,
|
||||||
typeof getOAuthFlowStepTimeoutMs === 'function'
|
typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||||
? await getOAuthFlowStepTimeoutMs(30000, {
|
? await getOAuthFlowStepTimeoutMs(30000, {
|
||||||
step: 9,
|
step: visibleStep,
|
||||||
actionLabel: '刷新 OAuth 同意页',
|
actionLabel: '刷新 OAuth 同意页',
|
||||||
})
|
})
|
||||||
: 30000
|
: 30000
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
(function attachBackgroundPlusCheckoutCreate(root, factory) {
|
||||||
|
root.MultiPageBackgroundPlusCheckoutCreate = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusCheckoutCreateModule() {
|
||||||
|
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||||
|
const PLUS_CHECKOUT_ENTRY_URL = 'https://chatgpt.com/';
|
||||||
|
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/plus-checkout.js'];
|
||||||
|
|
||||||
|
function createPlusCheckoutCreateExecutor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureContentScriptReadyOnTabUntilStopped,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
sendTabMessageUntilStopped,
|
||||||
|
setState,
|
||||||
|
sleepWithStop,
|
||||||
|
waitForTabCompleteUntilStopped,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function executePlusCheckoutCreate() {
|
||||||
|
await addLog('步骤 6:正在打开 ChatGPT 会话页,准备创建 Plus Checkout...', 'info');
|
||||||
|
const tabId = await reuseOrCreateTab(PLUS_CHECKOUT_SOURCE, PLUS_CHECKOUT_ENTRY_URL, {
|
||||||
|
inject: PLUS_CHECKOUT_INJECT_FILES,
|
||||||
|
injectSource: PLUS_CHECKOUT_SOURCE,
|
||||||
|
reloadIfSameUrl: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForTabCompleteUntilStopped(tabId);
|
||||||
|
await sleepWithStop(1000);
|
||||||
|
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
|
||||||
|
inject: PLUS_CHECKOUT_INJECT_FILES,
|
||||||
|
injectSource: PLUS_CHECKOUT_SOURCE,
|
||||||
|
logMessage: '步骤 6:ChatGPT 页面仍在加载,等待 Plus Checkout 脚本就绪...',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
||||||
|
type: 'CREATE_PLUS_CHECKOUT',
|
||||||
|
source: 'background',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
if (!result?.checkoutUrl) {
|
||||||
|
throw new Error('步骤 6:Plus Checkout 创建后未返回支付链接。');
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog('步骤 6:Plus Checkout 已创建,正在打开支付页面...', 'ok');
|
||||||
|
await chrome.tabs.update(tabId, { url: result.checkoutUrl, active: true });
|
||||||
|
await waitForTabCompleteUntilStopped(tabId);
|
||||||
|
await sleepWithStop(1000);
|
||||||
|
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
|
||||||
|
inject: PLUS_CHECKOUT_INJECT_FILES,
|
||||||
|
injectSource: PLUS_CHECKOUT_SOURCE,
|
||||||
|
logMessage: '步骤 6:Checkout 页面仍在加载,等待页面脚本就绪...',
|
||||||
|
});
|
||||||
|
|
||||||
|
await setState({
|
||||||
|
plusCheckoutTabId: tabId,
|
||||||
|
plusCheckoutUrl: result.checkoutUrl,
|
||||||
|
plusCheckoutCountry: result.country || 'DE',
|
||||||
|
plusCheckoutCurrency: result.currency || 'EUR',
|
||||||
|
});
|
||||||
|
|
||||||
|
await completeStepFromBackground(6, {
|
||||||
|
plusCheckoutCountry: result.country || 'DE',
|
||||||
|
plusCheckoutCurrency: result.currency || 'EUR',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
executePlusCheckoutCreate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createPlusCheckoutCreateExecutor,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
(function attachBackgroundPlusCheckoutBilling(root, factory) {
|
||||||
|
root.MultiPageBackgroundPlusCheckoutBilling = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusCheckoutBillingModule() {
|
||||||
|
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||||
|
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/plus-checkout.js'];
|
||||||
|
|
||||||
|
function createPlusCheckoutBillingExecutor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureContentScriptReadyOnTabUntilStopped,
|
||||||
|
generateRandomName,
|
||||||
|
getAddressSeedForCountry,
|
||||||
|
getTabId,
|
||||||
|
isTabAlive,
|
||||||
|
sendTabMessageUntilStopped,
|
||||||
|
setState,
|
||||||
|
sleepWithStop,
|
||||||
|
waitForTabCompleteUntilStopped,
|
||||||
|
waitForTabUrlMatchUntilStopped,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function getCheckoutTabId(state = {}) {
|
||||||
|
const registeredTabId = await getTabId(PLUS_CHECKOUT_SOURCE);
|
||||||
|
if (registeredTabId && await isTabAlive(PLUS_CHECKOUT_SOURCE)) {
|
||||||
|
return registeredTabId;
|
||||||
|
}
|
||||||
|
const storedTabId = Number(state.plusCheckoutTabId) || 0;
|
||||||
|
if (storedTabId) {
|
||||||
|
return storedTabId;
|
||||||
|
}
|
||||||
|
throw new Error('步骤 7:未找到 Plus Checkout 标签页,请先完成步骤 6。');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executePlusCheckoutBilling(state = {}) {
|
||||||
|
const tabId = await getCheckoutTabId(state);
|
||||||
|
await addLog('步骤 7:正在等待 Plus Checkout 页面加载完成...', 'info');
|
||||||
|
await waitForTabCompleteUntilStopped(tabId);
|
||||||
|
await sleepWithStop(1000);
|
||||||
|
|
||||||
|
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
|
||||||
|
inject: PLUS_CHECKOUT_INJECT_FILES,
|
||||||
|
injectSource: PLUS_CHECKOUT_SOURCE,
|
||||||
|
logMessage: '步骤 7:Checkout 页面仍在加载,等待账单填写脚本就绪...',
|
||||||
|
});
|
||||||
|
|
||||||
|
const randomName = generateRandomName();
|
||||||
|
const fullName = [randomName.firstName, randomName.lastName].filter(Boolean).join(' ');
|
||||||
|
const addressSeed = getAddressSeedForCountry(state.plusCheckoutCountry || 'DE', {
|
||||||
|
fallbackCountry: 'DE',
|
||||||
|
});
|
||||||
|
if (!addressSeed) {
|
||||||
|
throw new Error('步骤 7:未找到可用的本地账单地址种子。');
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog(`步骤 7:正在选择 PayPal 并填写账单地址(${addressSeed.countryCode} / ${addressSeed.query})...`, 'info');
|
||||||
|
const result = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
||||||
|
type: 'FILL_PLUS_BILLING_AND_SUBMIT',
|
||||||
|
source: 'background',
|
||||||
|
payload: {
|
||||||
|
fullName,
|
||||||
|
addressSeed,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await setState({
|
||||||
|
plusCheckoutTabId: tabId,
|
||||||
|
plusBillingCountryText: result?.countryText || '',
|
||||||
|
plusBillingAddress: result?.structuredAddress || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await addLog('步骤 7:账单地址已提交,正在等待跳转到 PayPal...', 'info');
|
||||||
|
await waitForTabUrlMatchUntilStopped(tabId, (url) => /paypal\./i.test(url));
|
||||||
|
await waitForTabCompleteUntilStopped(tabId);
|
||||||
|
await sleepWithStop(1000);
|
||||||
|
|
||||||
|
await completeStepFromBackground(7, {
|
||||||
|
plusBillingCountryText: result?.countryText || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
executePlusCheckoutBilling,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createPlusCheckoutBillingExecutor,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -40,7 +40,13 @@
|
|||||||
return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message);
|
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) {
|
async function executeStep7(state) {
|
||||||
|
const visibleStep = getVisibleStep(state, 7);
|
||||||
if (!state.email) {
|
if (!state.email) {
|
||||||
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
||||||
}
|
}
|
||||||
@@ -56,20 +62,20 @@
|
|||||||
const password = currentState.password || currentState.customPassword || '';
|
const password = currentState.password || currentState.customPassword || '';
|
||||||
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
|
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
|
||||||
if (typeof startOAuthFlowTimeoutWindow === 'function') {
|
if (typeof startOAuthFlowTimeoutWindow === 'function') {
|
||||||
await startOAuthFlowTimeoutWindow({ step: 7, oauthUrl });
|
await startOAuthFlowTimeoutWindow({ step: visibleStep, oauthUrl });
|
||||||
}
|
}
|
||||||
const loginTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
const loginTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||||
? await getOAuthFlowStepTimeoutMs(180000, {
|
? await getOAuthFlowStepTimeoutMs(180000, {
|
||||||
step: 7,
|
step: visibleStep,
|
||||||
actionLabel: 'OAuth 登录并进入验证码页',
|
actionLabel: 'OAuth 登录并进入验证码页',
|
||||||
oauthUrl,
|
oauthUrl,
|
||||||
})
|
})
|
||||||
: 180000;
|
: 180000;
|
||||||
|
|
||||||
if (attempt === 1) {
|
if (attempt === 1) {
|
||||||
await addLog('步骤 7:正在打开最新 OAuth 链接并登录...');
|
await addLog(`步骤 ${visibleStep}:正在打开最新 OAuth 链接并登录...`);
|
||||||
} else {
|
} else {
|
||||||
await addLog(`步骤 7:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn');
|
await addLog(`步骤 ${visibleStep}:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn');
|
||||||
}
|
}
|
||||||
|
|
||||||
await reuseOrCreateTab('signup-page', oauthUrl);
|
await reuseOrCreateTab('signup-page', oauthUrl);
|
||||||
@@ -83,13 +89,14 @@
|
|||||||
payload: {
|
payload: {
|
||||||
email: currentState.email,
|
email: currentState.email,
|
||||||
password,
|
password,
|
||||||
|
visibleStep,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timeoutMs: loginTimeoutMs,
|
timeoutMs: loginTimeoutMs,
|
||||||
responseTimeoutMs: loginTimeoutMs,
|
responseTimeoutMs: loginTimeoutMs,
|
||||||
retryDelayMs: 700,
|
retryDelayMs: 700,
|
||||||
logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续登录...',
|
logMessage: `步骤 ${visibleStep}:认证页正在切换,等待页面重新就绪后继续登录...`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -98,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isStep6SuccessResult(result)) {
|
if (isStep6SuccessResult(result)) {
|
||||||
await completeStepFromBackground(7, {
|
await completeStepFromBackground(visibleStep, {
|
||||||
loginVerificationRequestedAt: result.loginVerificationRequestedAt || null,
|
loginVerificationRequestedAt: result.loginVerificationRequestedAt || null,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -118,7 +125,7 @@
|
|||||||
}
|
}
|
||||||
if (isManagementSecretConfigError(err)) {
|
if (isManagementSecretConfigError(err)) {
|
||||||
await addLog(
|
await addLog(
|
||||||
`步骤 7:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
|
`步骤 ${visibleStep}:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
|
||||||
'error'
|
'error'
|
||||||
);
|
);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -128,11 +135,11 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog(`步骤 7:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn');
|
await addLog(`步骤 ${visibleStep}:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`步骤 7:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
|
throw new Error(`步骤 ${visibleStep}:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { executeStep7 };
|
return { executeStep7 };
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
(function attachBackgroundPayPalApprove(root, factory) {
|
||||||
|
root.MultiPageBackgroundPayPalApprove = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPayPalApproveModule() {
|
||||||
|
const PAYPAL_SOURCE = 'paypal-flow';
|
||||||
|
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||||
|
const PAYPAL_INJECT_FILES = ['content/utils.js', 'content/paypal-flow.js'];
|
||||||
|
|
||||||
|
function createPayPalApproveExecutor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureContentScriptReadyOnTabUntilStopped,
|
||||||
|
getTabId,
|
||||||
|
isTabAlive,
|
||||||
|
sendTabMessageUntilStopped,
|
||||||
|
setState,
|
||||||
|
sleepWithStop,
|
||||||
|
waitForTabCompleteUntilStopped,
|
||||||
|
waitForTabUrlMatchUntilStopped,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function resolvePayPalTabId(state = {}) {
|
||||||
|
const paypalTabId = await getTabId(PAYPAL_SOURCE);
|
||||||
|
if (paypalTabId && await isTabAlive(PAYPAL_SOURCE)) {
|
||||||
|
return paypalTabId;
|
||||||
|
}
|
||||||
|
const checkoutTabId = await getTabId(PLUS_CHECKOUT_SOURCE);
|
||||||
|
if (checkoutTabId) {
|
||||||
|
return checkoutTabId;
|
||||||
|
}
|
||||||
|
const storedTabId = Number(state.plusCheckoutTabId) || 0;
|
||||||
|
if (storedTabId) {
|
||||||
|
return storedTabId;
|
||||||
|
}
|
||||||
|
throw new Error('步骤 8:未找到 PayPal 标签页,请先完成步骤 7。');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensurePayPalReady(tabId, logMessage = '') {
|
||||||
|
await waitForTabUrlMatchUntilStopped(tabId, (url) => /paypal\./i.test(url));
|
||||||
|
await waitForTabCompleteUntilStopped(tabId);
|
||||||
|
await sleepWithStop(1000);
|
||||||
|
await ensureContentScriptReadyOnTabUntilStopped(PAYPAL_SOURCE, tabId, {
|
||||||
|
inject: PAYPAL_INJECT_FILES,
|
||||||
|
injectSource: PAYPAL_SOURCE,
|
||||||
|
logMessage: logMessage || '步骤 8:PayPal 页面仍在加载,等待脚本就绪...',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPayPalState(tabId) {
|
||||||
|
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||||
|
type: 'PAYPAL_GET_STATE',
|
||||||
|
source: 'background',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
return result || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dismissPrompts(tabId) {
|
||||||
|
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||||
|
type: 'PAYPAL_DISMISS_PROMPTS',
|
||||||
|
source: 'background',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
return result || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitLogin(tabId, state = {}) {
|
||||||
|
if (!state.paypalPassword) {
|
||||||
|
throw new Error('步骤 8:未配置 PayPal 密码,请先在侧边栏填写。');
|
||||||
|
}
|
||||||
|
await addLog('步骤 8:正在填写 PayPal 登录信息并提交...', 'info');
|
||||||
|
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||||
|
type: 'PAYPAL_SUBMIT_LOGIN',
|
||||||
|
source: 'background',
|
||||||
|
payload: {
|
||||||
|
email: state.paypalEmail || '',
|
||||||
|
password: state.paypalPassword || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickApprove(tabId) {
|
||||||
|
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||||
|
type: 'PAYPAL_CLICK_APPROVE',
|
||||||
|
source: 'background',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
return Boolean(result?.clicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executePayPalApprove(state = {}) {
|
||||||
|
const tabId = await resolvePayPalTabId(state);
|
||||||
|
await ensurePayPalReady(tabId);
|
||||||
|
await setState({ plusCheckoutTabId: tabId });
|
||||||
|
|
||||||
|
let loggedWaiting = false;
|
||||||
|
while (true) {
|
||||||
|
const currentUrl = (await chrome.tabs.get(tabId).catch(() => null))?.url || '';
|
||||||
|
if (currentUrl && !/paypal\./i.test(currentUrl)) {
|
||||||
|
await addLog('步骤 8:PayPal 已跳转离开授权页,准备进入回跳确认。', 'ok');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensurePayPalReady(tabId, '步骤 8:PayPal 页面正在切换,等待脚本重新就绪...');
|
||||||
|
const pageState = await getPayPalState(tabId);
|
||||||
|
|
||||||
|
if (pageState.needsLogin) {
|
||||||
|
await submitLogin(tabId, state);
|
||||||
|
await waitForTabCompleteUntilStopped(tabId);
|
||||||
|
await sleepWithStop(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageState.hasPasskeyPrompt) {
|
||||||
|
await addLog('步骤 8:检测到 PayPal 通行密钥提示,正在关闭...', 'info');
|
||||||
|
await dismissPrompts(tabId);
|
||||||
|
await sleepWithStop(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dismissed = await dismissPrompts(tabId).catch(() => ({ clicked: 0 }));
|
||||||
|
if (dismissed.clicked) {
|
||||||
|
await sleepWithStop(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageState.approveReady) {
|
||||||
|
await addLog('步骤 8:正在点击 PayPal“同意并继续”...', 'info');
|
||||||
|
const clicked = await clickApprove(tabId);
|
||||||
|
if (clicked) {
|
||||||
|
await setState({ plusPaypalApprovedAt: Date.now() });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loggedWaiting) {
|
||||||
|
loggedWaiting = true;
|
||||||
|
await addLog('步骤 8:等待 PayPal 授权按钮或下一步页面出现...', 'info');
|
||||||
|
}
|
||||||
|
await sleepWithStop(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
await completeStepFromBackground(8, {
|
||||||
|
plusPaypalApprovedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
executePayPalApprove,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createPayPalApproveExecutor,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -26,6 +26,15 @@
|
|||||||
return String(value || '').trim();
|
return String(value || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVisibleStep(state, fallback = 10) {
|
||||||
|
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||||
|
return visibleStep > 0 ? visibleStep : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfirmStepForVisibleStep(visibleStep) {
|
||||||
|
return visibleStep === 12 ? 11 : 9;
|
||||||
|
}
|
||||||
|
|
||||||
function parseLocalhostCallback(rawUrl) {
|
function parseLocalhostCallback(rawUrl) {
|
||||||
let parsed;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
@@ -109,26 +118,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function executeCpaStep10(state) {
|
async function executeCpaStep10(state) {
|
||||||
|
const visibleStep = getVisibleStep(state, 10);
|
||||||
|
const confirmStep = getConfirmStepForVisibleStep(visibleStep);
|
||||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
|
||||||
}
|
}
|
||||||
if (!state.localhostUrl) {
|
if (!state.localhostUrl) {
|
||||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
|
||||||
}
|
}
|
||||||
if (!state.vpsUrl) {
|
if (!state.vpsUrl) {
|
||||||
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
|
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldBypassStep9ForLocalCpa(state)) {
|
if (shouldBypassStep9ForLocalCpa(state)) {
|
||||||
await addLog('步骤 10:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。', 'info');
|
await addLog(`步骤 ${visibleStep}:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。`, 'info');
|
||||||
await completeStepFromBackground(10, {
|
await completeStepFromBackground(visibleStep, {
|
||||||
localhostUrl: state.localhostUrl,
|
localhostUrl: state.localhostUrl,
|
||||||
verifiedStatus: 'local-auto',
|
verifiedStatus: 'local-auto',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog('步骤 10:正在打开 CPA 面板...');
|
await addLog(`步骤 ${visibleStep}:正在打开 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');
|
||||||
@@ -149,20 +160,20 @@
|
|||||||
inject: injectFiles,
|
inject: injectFiles,
|
||||||
timeoutMs: 45000,
|
timeoutMs: 45000,
|
||||||
retryDelayMs: 900,
|
retryDelayMs: 900,
|
||||||
logMessage: '步骤 10:CPA 面板仍在加载,正在重试连接...',
|
logMessage: `步骤 ${visibleStep}:CPA 面板仍在加载,正在重试连接...`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await addLog('步骤 10:正在填写回调地址...');
|
await addLog(`步骤 ${visibleStep}:正在填写回调地址...`);
|
||||||
const result = await sendToContentScriptResilient('vps-panel', {
|
const result = await sendToContentScriptResilient('vps-panel', {
|
||||||
type: 'EXECUTE_STEP',
|
type: 'EXECUTE_STEP',
|
||||||
step: 10,
|
step: visibleStep,
|
||||||
source: 'background',
|
source: 'background',
|
||||||
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
|
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword, visibleStep },
|
||||||
}, {
|
}, {
|
||||||
timeoutMs: 125000,
|
timeoutMs: 125000,
|
||||||
responseTimeoutMs: 125000,
|
responseTimeoutMs: 125000,
|
||||||
retryDelayMs: 700,
|
retryDelayMs: 700,
|
||||||
logMessage: '步骤 10:CPA 面板通信未就绪,正在等待页面恢复...',
|
logMessage: `步骤 ${visibleStep}:CPA 面板通信未就绪,正在等待页面恢复...`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@@ -171,14 +182,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function executeCodex2ApiStep10(state) {
|
async function executeCodex2ApiStep10(state) {
|
||||||
|
const visibleStep = getVisibleStep(state, 10);
|
||||||
|
const confirmStep = getConfirmStepForVisibleStep(visibleStep);
|
||||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
|
||||||
}
|
}
|
||||||
if (!state.localhostUrl) {
|
if (!state.localhostUrl) {
|
||||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
|
||||||
}
|
}
|
||||||
if (!state.codex2apiSessionId) {
|
if (!state.codex2apiSessionId) {
|
||||||
throw new Error('缺少 Codex2API 会话信息,请重新执行步骤 7。');
|
throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${visibleStep === 12 ? 10 : 7}。`);
|
||||||
}
|
}
|
||||||
if (!normalizeString(state.codex2apiAdminKey)) {
|
if (!normalizeString(state.codex2apiAdminKey)) {
|
||||||
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
|
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
|
||||||
@@ -193,7 +206,7 @@
|
|||||||
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
|
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
|
||||||
const origin = new URL(codex2apiUrl).origin;
|
const origin = new URL(codex2apiUrl).origin;
|
||||||
|
|
||||||
await addLog('步骤 10:正在向 Codex2API 提交回调并创建账号...');
|
await addLog(`步骤 ${visibleStep}:正在向 Codex2API 提交回调并创建账号...`);
|
||||||
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', {
|
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', {
|
||||||
adminKey: state.codex2apiAdminKey,
|
adminKey: state.codex2apiAdminKey,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -205,19 +218,21 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功';
|
const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功';
|
||||||
await addLog(`步骤 10:${verifiedStatus}`, 'ok');
|
await addLog(`步骤 ${visibleStep}:${verifiedStatus}`, 'ok');
|
||||||
await completeStepFromBackground(10, {
|
await completeStepFromBackground(visibleStep, {
|
||||||
localhostUrl: callback.url,
|
localhostUrl: callback.url,
|
||||||
verifiedStatus,
|
verifiedStatus,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeSub2ApiStep10(state) {
|
async function executeSub2ApiStep10(state) {
|
||||||
|
const visibleStep = getVisibleStep(state, 10);
|
||||||
|
const confirmStep = getConfirmStepForVisibleStep(visibleStep);
|
||||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
|
||||||
}
|
}
|
||||||
if (!state.localhostUrl) {
|
if (!state.localhostUrl) {
|
||||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
|
||||||
}
|
}
|
||||||
if (!state.sub2apiSessionId) {
|
if (!state.sub2apiSessionId) {
|
||||||
throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。');
|
throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。');
|
||||||
@@ -232,7 +247,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('步骤 10:正在打开 SUB2API 后台...');
|
await addLog(`步骤 ${visibleStep}:正在打开 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');
|
||||||
@@ -254,12 +269,13 @@
|
|||||||
injectSource: 'sub2api-panel',
|
injectSource: 'sub2api-panel',
|
||||||
});
|
});
|
||||||
|
|
||||||
await addLog('步骤 10:正在向 SUB2API 提交回调并创建账号...');
|
await addLog(`步骤 ${visibleStep}:正在向 SUB2API 提交回调并创建账号...`);
|
||||||
const result = await sendToContentScript('sub2api-panel', {
|
const result = await sendToContentScript('sub2api-panel', {
|
||||||
type: 'EXECUTE_STEP',
|
type: 'EXECUTE_STEP',
|
||||||
step: 10,
|
step: visibleStep,
|
||||||
source: 'background',
|
source: 'background',
|
||||||
payload: {
|
payload: {
|
||||||
|
visibleStep,
|
||||||
localhostUrl: state.localhostUrl,
|
localhostUrl: state.localhostUrl,
|
||||||
sub2apiUrl,
|
sub2apiUrl,
|
||||||
sub2apiEmail: state.sub2apiEmail,
|
sub2apiEmail: state.sub2apiEmail,
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
(function attachBackgroundPlusReturnConfirm(root, factory) {
|
||||||
|
root.MultiPageBackgroundPlusReturnConfirm = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusReturnConfirmModule() {
|
||||||
|
const PAYPAL_SOURCE = 'paypal-flow';
|
||||||
|
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||||
|
|
||||||
|
function createPlusReturnConfirmExecutor(deps = {}) {
|
||||||
|
const {
|
||||||
|
addLog,
|
||||||
|
completeStepFromBackground,
|
||||||
|
getTabId,
|
||||||
|
isTabAlive,
|
||||||
|
setState,
|
||||||
|
sleepWithStop,
|
||||||
|
waitForTabCompleteUntilStopped,
|
||||||
|
waitForTabUrlMatchUntilStopped,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
async function resolveReturnTabId(state = {}) {
|
||||||
|
const paypalTabId = await getTabId(PAYPAL_SOURCE);
|
||||||
|
if (paypalTabId && await isTabAlive(PAYPAL_SOURCE)) {
|
||||||
|
return paypalTabId;
|
||||||
|
}
|
||||||
|
const checkoutTabId = await getTabId(PLUS_CHECKOUT_SOURCE);
|
||||||
|
if (checkoutTabId) {
|
||||||
|
return checkoutTabId;
|
||||||
|
}
|
||||||
|
const storedTabId = Number(state.plusCheckoutTabId) || 0;
|
||||||
|
if (storedTabId) {
|
||||||
|
return storedTabId;
|
||||||
|
}
|
||||||
|
throw new Error('步骤 9:未找到 Plus / PayPal 标签页,无法确认订阅回跳。');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isReturnUrl(url = '') {
|
||||||
|
return /https:\/\/(?:chatgpt\.com|chat\.openai\.com|openai\.com)\//i.test(String(url || ''))
|
||||||
|
&& !/paypal\./i.test(String(url || ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executePlusReturnConfirm(state = {}) {
|
||||||
|
const tabId = await resolveReturnTabId(state);
|
||||||
|
await addLog('步骤 9:正在等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面...', 'info');
|
||||||
|
const tab = await waitForTabUrlMatchUntilStopped(tabId, isReturnUrl);
|
||||||
|
await waitForTabCompleteUntilStopped(tabId);
|
||||||
|
await sleepWithStop(1000);
|
||||||
|
|
||||||
|
await setState({
|
||||||
|
plusCheckoutTabId: tabId,
|
||||||
|
plusReturnUrl: tab?.url || '',
|
||||||
|
});
|
||||||
|
await completeStepFromBackground(9, {
|
||||||
|
plusReturnUrl: tab?.url || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
executePlusReturnConfirm,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createPlusReturnConfirmExecutor,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
// content/paypal-flow.js — PayPal login and approval helper.
|
||||||
|
|
||||||
|
console.log('[MultiPage:paypal-flow] Content script loaded on', location.href);
|
||||||
|
|
||||||
|
const PAYPAL_FLOW_LISTENER_SENTINEL = 'data-multipage-paypal-flow-listener';
|
||||||
|
|
||||||
|
if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1') {
|
||||||
|
document.documentElement.setAttribute(PAYPAL_FLOW_LISTENER_SENTINEL, '1');
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
if (
|
||||||
|
message.type === 'PAYPAL_GET_STATE'
|
||||||
|
|| message.type === 'PAYPAL_SUBMIT_LOGIN'
|
||||||
|
|| message.type === 'PAYPAL_DISMISS_PROMPTS'
|
||||||
|
|| message.type === 'PAYPAL_CLICK_APPROVE'
|
||||||
|
) {
|
||||||
|
resetStopState();
|
||||||
|
handlePayPalCommand(message).then((result) => {
|
||||||
|
sendResponse({ ok: true, ...(result || {}) });
|
||||||
|
}).catch((err) => {
|
||||||
|
if (isStopError(err)) {
|
||||||
|
sendResponse({ stopped: true, error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendResponse({ error: err.message });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('[MultiPage:paypal-flow] 消息监听已存在,跳过重复注册');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePayPalCommand(message) {
|
||||||
|
switch (message.type) {
|
||||||
|
case 'PAYPAL_GET_STATE':
|
||||||
|
return inspectPayPalState();
|
||||||
|
case 'PAYPAL_SUBMIT_LOGIN':
|
||||||
|
return submitPayPalLogin(message.payload || {});
|
||||||
|
case 'PAYPAL_DISMISS_PROMPTS':
|
||||||
|
return dismissPayPalPrompts();
|
||||||
|
case 'PAYPAL_CLICK_APPROVE':
|
||||||
|
return clickPayPalApprove();
|
||||||
|
default:
|
||||||
|
throw new Error(`paypal-flow.js 不处理消息:${message.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitUntil(predicate, options = {}) {
|
||||||
|
const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 250));
|
||||||
|
while (true) {
|
||||||
|
throwIfStopped();
|
||||||
|
const value = await predicate();
|
||||||
|
if (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
await sleep(intervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForDocumentComplete() {
|
||||||
|
await waitUntil(() => document.readyState === 'complete', { intervalMs: 200 });
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisibleElement(el) {
|
||||||
|
if (!el) return false;
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return style.display !== 'none'
|
||||||
|
&& style.visibility !== 'hidden'
|
||||||
|
&& Number(rect.width) > 0
|
||||||
|
&& Number(rect.height) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeText(text = '') {
|
||||||
|
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActionText(el) {
|
||||||
|
return normalizeText([
|
||||||
|
el?.textContent,
|
||||||
|
el?.value,
|
||||||
|
el?.getAttribute?.('aria-label'),
|
||||||
|
el?.getAttribute?.('title'),
|
||||||
|
el?.getAttribute?.('placeholder'),
|
||||||
|
el?.getAttribute?.('name'),
|
||||||
|
el?.id,
|
||||||
|
].filter(Boolean).join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleControls(selector) {
|
||||||
|
return Array.from(document.querySelectorAll(selector)).filter(isVisibleElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnabledControl(el) {
|
||||||
|
return Boolean(el)
|
||||||
|
&& !el.disabled
|
||||||
|
&& el.getAttribute?.('aria-disabled') !== 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClickableByText(patterns) {
|
||||||
|
const normalizedPatterns = (Array.isArray(patterns) ? patterns : [patterns]).filter(Boolean);
|
||||||
|
const candidates = getVisibleControls('button, a, [role="button"], input[type="button"], input[type="submit"]');
|
||||||
|
return candidates.find((el) => {
|
||||||
|
const text = getActionText(el);
|
||||||
|
return normalizedPatterns.some((pattern) => pattern.test(text));
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findInputByPatterns(patterns) {
|
||||||
|
const inputs = getVisibleControls('input')
|
||||||
|
.filter((input) => {
|
||||||
|
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||||||
|
return !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
|
||||||
|
});
|
||||||
|
return inputs.find((input) => {
|
||||||
|
const text = getActionText(input);
|
||||||
|
return patterns.some((pattern) => pattern.test(text));
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findEmailInput() {
|
||||||
|
return findInputByPatterns([
|
||||||
|
/email|login|user|账号|邮箱/i,
|
||||||
|
]) || getVisibleControls('input[type="email"]').find(isVisibleElement) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPasswordInput() {
|
||||||
|
return findInputByPatterns([
|
||||||
|
/password|pass|密码/i,
|
||||||
|
]) || getVisibleControls('input[type="password"]').find(isVisibleElement) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLoginNextButton() {
|
||||||
|
return findClickableByText([
|
||||||
|
/next|continue|login|log\s*in|sign\s*in/i,
|
||||||
|
/下一步|继续|登录|登入/i,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findApproveButton() {
|
||||||
|
return findClickableByText([
|
||||||
|
/同意并继续|同意|继续|授权|确认并继续/i,
|
||||||
|
/agree\s*(?:and)?\s*continue|continue|accept|authorize|agree|pay\s*now/i,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPasskeyPromptButtons() {
|
||||||
|
const promptPatterns = [
|
||||||
|
/passkey|通行密钥|安全密钥|下次登录|faster|save/i,
|
||||||
|
];
|
||||||
|
const bodyText = normalizeText(document.body?.innerText || '');
|
||||||
|
const likelyPrompt = promptPatterns.some((pattern) => pattern.test(bodyText));
|
||||||
|
if (!likelyPrompt) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelOrClose = getVisibleControls('button, a, [role="button"]')
|
||||||
|
.filter((el) => {
|
||||||
|
const text = getActionText(el);
|
||||||
|
return /取消|稍后|不保存|不用|关闭|cancel|not now|maybe later|skip|close|x/i.test(text)
|
||||||
|
|| el.getAttribute?.('aria-label')?.match(/close|关闭/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconCloseButtons = getVisibleControls('button, [role="button"]')
|
||||||
|
.filter((el) => {
|
||||||
|
const text = getActionText(el);
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return (/^×$|^x$/i.test(text) || /close|关闭/i.test(text))
|
||||||
|
&& rect.width <= 64
|
||||||
|
&& rect.height <= 64;
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...cancelOrClose, ...iconCloseButtons];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPasskeyPrompt() {
|
||||||
|
return findPasskeyPromptButtons().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitPayPalLogin(payload = {}) {
|
||||||
|
await waitForDocumentComplete();
|
||||||
|
|
||||||
|
const email = normalizeText(payload.email || '');
|
||||||
|
const password = String(payload.password || '');
|
||||||
|
if (!password) {
|
||||||
|
throw new Error('PayPal 密码为空,请先在侧边栏配置。');
|
||||||
|
}
|
||||||
|
|
||||||
|
let passwordInput = findPasswordInput();
|
||||||
|
const emailInput = findEmailInput();
|
||||||
|
|
||||||
|
if (!passwordInput && emailInput && email) {
|
||||||
|
fillInput(emailInput, email);
|
||||||
|
const nextButton = findLoginNextButton();
|
||||||
|
if (nextButton && isEnabledControl(nextButton)) {
|
||||||
|
simulateClick(nextButton);
|
||||||
|
}
|
||||||
|
passwordInput = await waitUntil(() => findPasswordInput(), { intervalMs: 250 });
|
||||||
|
} else if (!passwordInput && emailInput && !email) {
|
||||||
|
throw new Error('PayPal 账号为空,请先在侧边栏配置。');
|
||||||
|
} else if (emailInput && email && !String(emailInput.value || '').trim()) {
|
||||||
|
fillInput(emailInput, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordInput = passwordInput || await waitUntil(() => findPasswordInput(), { intervalMs: 250 });
|
||||||
|
fillInput(passwordInput, password);
|
||||||
|
await sleep(1000);
|
||||||
|
|
||||||
|
const loginButton = await waitUntil(() => {
|
||||||
|
const button = findClickableByText([
|
||||||
|
/login|log\s*in|sign\s*in|continue/i,
|
||||||
|
/登录|登入|继续/i,
|
||||||
|
]);
|
||||||
|
return button && isEnabledControl(button) ? button : null;
|
||||||
|
}, { intervalMs: 250 });
|
||||||
|
|
||||||
|
simulateClick(loginButton);
|
||||||
|
return { submitted: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dismissPayPalPrompts() {
|
||||||
|
await waitForDocumentComplete();
|
||||||
|
const buttons = findPasskeyPromptButtons();
|
||||||
|
let clicked = 0;
|
||||||
|
for (const button of buttons) {
|
||||||
|
if (!isVisibleElement(button) || !isEnabledControl(button)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
simulateClick(button);
|
||||||
|
clicked += 1;
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
clicked,
|
||||||
|
hasPromptAfterClick: hasPasskeyPrompt(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickPayPalApprove() {
|
||||||
|
await waitForDocumentComplete();
|
||||||
|
await dismissPayPalPrompts().catch(() => ({ clicked: 0 }));
|
||||||
|
|
||||||
|
const button = findApproveButton();
|
||||||
|
if (!button || !isEnabledControl(button)) {
|
||||||
|
return {
|
||||||
|
clicked: false,
|
||||||
|
state: inspectPayPalState(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
simulateClick(button);
|
||||||
|
return {
|
||||||
|
clicked: true,
|
||||||
|
buttonText: getActionText(button),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspectPayPalState() {
|
||||||
|
const emailInput = findEmailInput();
|
||||||
|
const passwordInput = findPasswordInput();
|
||||||
|
const approveButton = findApproveButton();
|
||||||
|
return {
|
||||||
|
url: location.href,
|
||||||
|
readyState: document.readyState,
|
||||||
|
needsLogin: Boolean(emailInput || passwordInput),
|
||||||
|
hasEmailInput: Boolean(emailInput),
|
||||||
|
hasPasswordInput: Boolean(passwordInput),
|
||||||
|
approveReady: Boolean(approveButton && isEnabledControl(approveButton)),
|
||||||
|
approveButtonText: approveButton ? getActionText(approveButton) : '',
|
||||||
|
hasPasskeyPrompt: hasPasskeyPrompt(),
|
||||||
|
bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
// content/plus-checkout.js — ChatGPT Plus checkout helper.
|
||||||
|
|
||||||
|
console.log('[MultiPage:plus-checkout] Content script loaded on', location.href);
|
||||||
|
|
||||||
|
const PLUS_CHECKOUT_LISTENER_SENTINEL = 'data-multipage-plus-checkout-listener';
|
||||||
|
const PLUS_CHECKOUT_PAYLOAD = {
|
||||||
|
entry_point: 'all_plans_pricing_modal',
|
||||||
|
plan_name: 'chatgptplusplan',
|
||||||
|
billing_details: {
|
||||||
|
country: 'DE',
|
||||||
|
currency: 'EUR',
|
||||||
|
},
|
||||||
|
checkout_ui_mode: 'custom',
|
||||||
|
promo_campaign: {
|
||||||
|
promo_campaign_id: 'plus-1-month-free',
|
||||||
|
is_coupon_from_query_param: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== '1') {
|
||||||
|
document.documentElement.setAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL, '1');
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
if (
|
||||||
|
message.type === 'CREATE_PLUS_CHECKOUT'
|
||||||
|
|| message.type === 'FILL_PLUS_BILLING_AND_SUBMIT'
|
||||||
|
|| message.type === 'PLUS_CHECKOUT_GET_STATE'
|
||||||
|
) {
|
||||||
|
resetStopState();
|
||||||
|
handlePlusCheckoutCommand(message).then((result) => {
|
||||||
|
sendResponse({ ok: true, ...(result || {}) });
|
||||||
|
}).catch((err) => {
|
||||||
|
if (isStopError(err)) {
|
||||||
|
sendResponse({ stopped: true, error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendResponse({ error: err.message });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('[MultiPage:plus-checkout] 消息监听已存在,跳过重复注册');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePlusCheckoutCommand(message) {
|
||||||
|
switch (message.type) {
|
||||||
|
case 'CREATE_PLUS_CHECKOUT':
|
||||||
|
return createPlusCheckoutSession();
|
||||||
|
case 'FILL_PLUS_BILLING_AND_SUBMIT':
|
||||||
|
return fillPlusBillingAndSubmit(message.payload || {});
|
||||||
|
case 'PLUS_CHECKOUT_GET_STATE':
|
||||||
|
return inspectPlusCheckoutState();
|
||||||
|
default:
|
||||||
|
throw new Error(`plus-checkout.js 不处理消息:${message.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitUntil(predicate, options = {}) {
|
||||||
|
const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 250));
|
||||||
|
const label = String(options.label || '条件').trim() || '条件';
|
||||||
|
while (true) {
|
||||||
|
throwIfStopped();
|
||||||
|
const value = await predicate();
|
||||||
|
if (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
await sleep(intervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForDocumentComplete() {
|
||||||
|
await waitUntil(() => document.readyState === 'complete', {
|
||||||
|
label: '页面加载完成',
|
||||||
|
intervalMs: 200,
|
||||||
|
});
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisibleElement(el) {
|
||||||
|
if (!el) return false;
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return style.display !== 'none'
|
||||||
|
&& style.visibility !== 'hidden'
|
||||||
|
&& Number(rect.width) > 0
|
||||||
|
&& Number(rect.height) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeText(text = '') {
|
||||||
|
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActionText(el) {
|
||||||
|
return normalizeText([
|
||||||
|
el?.textContent,
|
||||||
|
el?.value,
|
||||||
|
el?.getAttribute?.('aria-label'),
|
||||||
|
el?.getAttribute?.('title'),
|
||||||
|
el?.getAttribute?.('placeholder'),
|
||||||
|
el?.getAttribute?.('name'),
|
||||||
|
el?.id,
|
||||||
|
].filter(Boolean).join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldText(el) {
|
||||||
|
const id = el?.id || '';
|
||||||
|
const labels = [];
|
||||||
|
if (id) {
|
||||||
|
labels.push(...Array.from(document.querySelectorAll(`label[for="${CSS.escape(id)}"]`)).map((label) => label.textContent));
|
||||||
|
}
|
||||||
|
const wrappingLabel = el?.closest?.('label');
|
||||||
|
if (wrappingLabel) {
|
||||||
|
labels.push(wrappingLabel.textContent);
|
||||||
|
}
|
||||||
|
const container = el?.closest?.('[data-testid], [class], div, section, fieldset');
|
||||||
|
if (container) {
|
||||||
|
labels.push(container.textContent);
|
||||||
|
}
|
||||||
|
return normalizeText([
|
||||||
|
getActionText(el),
|
||||||
|
...labels,
|
||||||
|
].filter(Boolean).join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleControls(selector) {
|
||||||
|
return Array.from(document.querySelectorAll(selector)).filter(isVisibleElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClickableByText(patterns) {
|
||||||
|
const normalizedPatterns = (Array.isArray(patterns) ? patterns : [patterns])
|
||||||
|
.filter(Boolean);
|
||||||
|
const candidates = getVisibleControls('button, a, [role="button"], input[type="button"], input[type="submit"], [tabindex]');
|
||||||
|
return candidates.find((el) => {
|
||||||
|
const text = getActionText(el);
|
||||||
|
return normalizedPatterns.some((pattern) => pattern.test(text));
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnabledControl(el) {
|
||||||
|
return Boolean(el)
|
||||||
|
&& !el.disabled
|
||||||
|
&& el.getAttribute?.('aria-disabled') !== 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleTextInputs() {
|
||||||
|
return getVisibleControls('input, textarea')
|
||||||
|
.filter((el) => {
|
||||||
|
const type = String(el.getAttribute('type') || el.type || '').trim().toLowerCase();
|
||||||
|
return !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findInputByFieldText(patterns, options = {}) {
|
||||||
|
const inputs = getVisibleTextInputs();
|
||||||
|
const excluded = options.exclude || (() => false);
|
||||||
|
return inputs.find((input) => {
|
||||||
|
if (excluded(input)) return false;
|
||||||
|
const text = getFieldText(input);
|
||||||
|
return patterns.some((pattern) => pattern.test(text));
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPlusCheckoutSession() {
|
||||||
|
await waitForDocumentComplete();
|
||||||
|
log('Plus:正在读取 ChatGPT 登录会话...');
|
||||||
|
|
||||||
|
const sessionResponse = await fetch('/api/auth/session', {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
const session = await sessionResponse.json().catch(() => ({}));
|
||||||
|
const accessToken = session?.accessToken;
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('请先登录 ChatGPT,当前页面未返回可用 accessToken。');
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Plus:正在创建 checkout 会话...');
|
||||||
|
const response = await fetch('https://chatgpt.com/backend-api/payments/checkout', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(PLUS_CHECKOUT_PAYLOAD),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok || !data?.checkout_session_id) {
|
||||||
|
const detail = data?.detail || data?.message || `HTTP ${response.status}`;
|
||||||
|
throw new Error(`创建 Plus Checkout 失败:${detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
checkoutUrl: `https://chatgpt.com/checkout/openai_ie/${data.checkout_session_id}`,
|
||||||
|
country: PLUS_CHECKOUT_PAYLOAD.billing_details.country,
|
||||||
|
currency: PLUS_CHECKOUT_PAYLOAD.billing_details.currency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectPayPalPaymentMethod() {
|
||||||
|
const paypalPattern = /paypal/i;
|
||||||
|
const existingSelected = findClickableByText([/paypal/i]);
|
||||||
|
if (existingSelected) {
|
||||||
|
simulateClick(existingSelected);
|
||||||
|
await sleep(600);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const radios = getVisibleControls('input[type="radio"], [role="radio"]');
|
||||||
|
const paypalRadio = radios.find((el) => paypalPattern.test(getFieldText(el)));
|
||||||
|
if (paypalRadio) {
|
||||||
|
simulateClick(paypalRadio);
|
||||||
|
await sleep(600);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Plus Checkout:未找到 PayPal 付款方式。');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fillFullName(fullName) {
|
||||||
|
const value = normalizeText(fullName);
|
||||||
|
if (!value) return false;
|
||||||
|
const input = findInputByFieldText([
|
||||||
|
/full\s*name|name\s*on|cardholder|billing\s*name/i,
|
||||||
|
/姓名|全名|持卡人/i,
|
||||||
|
]);
|
||||||
|
if (!input) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fillInput(input, value);
|
||||||
|
await sleep(300);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCountryText() {
|
||||||
|
const countryInput = findInputByFieldText([
|
||||||
|
/country|region/i,
|
||||||
|
/国家|地区/i,
|
||||||
|
]);
|
||||||
|
if (countryInput?.value) {
|
||||||
|
return countryInput.value;
|
||||||
|
}
|
||||||
|
const countrySelect = getVisibleControls('select').find((select) => /country|region|国家|地区/i.test(getFieldText(select)));
|
||||||
|
if (countrySelect) {
|
||||||
|
const option = countrySelect.selectedOptions?.[0];
|
||||||
|
return option?.textContent || countrySelect.value || '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikelyAddressSearchInput(input) {
|
||||||
|
const text = getFieldText(input);
|
||||||
|
if (/name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|全名|姓名|邮箱|电话|密码|国家|地区|邮编|城市|省|州/i.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/address|street|billing|search|line\s*1|地址|街道|账单/i.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findAddressSearchInput() {
|
||||||
|
return waitUntil(() => {
|
||||||
|
const direct = findInputByFieldText([
|
||||||
|
/address|street|billing|search|line\s*1/i,
|
||||||
|
/地址|街道|账单/i,
|
||||||
|
], {
|
||||||
|
exclude: (input) => /city|state|province|postal|zip|country|城市|省|州|邮编|国家|地区/i.test(getFieldText(input)),
|
||||||
|
});
|
||||||
|
if (direct) return direct;
|
||||||
|
const candidates = getVisibleTextInputs().filter(isLikelyAddressSearchInput);
|
||||||
|
return candidates[0] || null;
|
||||||
|
}, {
|
||||||
|
label: '地址搜索输入框',
|
||||||
|
intervalMs: 250,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAddressSuggestions() {
|
||||||
|
const selectors = [
|
||||||
|
'[role="listbox"] [role="option"]',
|
||||||
|
'[role="option"]',
|
||||||
|
'.pac-container .pac-item',
|
||||||
|
'[data-testid*="address" i] [role="option"]',
|
||||||
|
'li',
|
||||||
|
];
|
||||||
|
const seen = new Set();
|
||||||
|
const results = [];
|
||||||
|
for (const selector of selectors) {
|
||||||
|
for (const el of Array.from(document.querySelectorAll(selector))) {
|
||||||
|
if (!isVisibleElement(el)) continue;
|
||||||
|
const text = normalizeText(el.textContent || el.getAttribute?.('aria-label') || '');
|
||||||
|
if (!text || text.length < 3) continue;
|
||||||
|
const key = `${selector}:${text}`;
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
results.push(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectAddressSuggestion(seed) {
|
||||||
|
const addressInput = await findAddressSearchInput();
|
||||||
|
fillInput(addressInput, seed.query || 'Berlin Mitte');
|
||||||
|
await sleep(800);
|
||||||
|
|
||||||
|
const suggestions = await waitUntil(() => {
|
||||||
|
const options = getAddressSuggestions();
|
||||||
|
return options.length ? options : null;
|
||||||
|
}, {
|
||||||
|
label: '地址推荐列表',
|
||||||
|
intervalMs: 250,
|
||||||
|
});
|
||||||
|
|
||||||
|
const suggestionIndex = Math.max(0, Math.min(
|
||||||
|
suggestions.length - 1,
|
||||||
|
Math.floor(Number(seed.suggestionIndex) || 0)
|
||||||
|
));
|
||||||
|
const target = suggestions[suggestionIndex] || suggestions[0];
|
||||||
|
simulateClick(target);
|
||||||
|
await sleep(1200);
|
||||||
|
return {
|
||||||
|
selectedText: normalizeText(target.textContent || ''),
|
||||||
|
suggestionIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStructuredAddressFields() {
|
||||||
|
const address1 = findInputByFieldText([
|
||||||
|
/address\s*(?:line)?\s*1|street/i,
|
||||||
|
/地址\s*1|街道|详细地址/i,
|
||||||
|
]);
|
||||||
|
const address2 = findInputByFieldText([
|
||||||
|
/address\s*(?:line)?\s*2|apt|suite|unit/i,
|
||||||
|
/地址\s*2|公寓|单元|门牌/i,
|
||||||
|
]);
|
||||||
|
const city = findInputByFieldText([
|
||||||
|
/city|town|suburb/i,
|
||||||
|
/城市|市区/i,
|
||||||
|
]);
|
||||||
|
const region = findInputByFieldText([
|
||||||
|
/state|province|region|county/i,
|
||||||
|
/省|州|地区/i,
|
||||||
|
]);
|
||||||
|
const postalCode = findInputByFieldText([
|
||||||
|
/postal|zip|postcode/i,
|
||||||
|
/邮编|邮政/i,
|
||||||
|
]);
|
||||||
|
return { address1, address2, city, region, postalCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillIfEmpty(input, value) {
|
||||||
|
if (!input || !value) return false;
|
||||||
|
if (String(input.value || '').trim()) return false;
|
||||||
|
fillInput(input, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureStructuredAddress(seed) {
|
||||||
|
const fallback = seed?.fallback || {};
|
||||||
|
const fields = await waitUntil(() => {
|
||||||
|
const currentFields = getStructuredAddressFields();
|
||||||
|
if (currentFields.address1 || currentFields.city || currentFields.postalCode) {
|
||||||
|
return currentFields;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, {
|
||||||
|
label: '结构化账单地址字段',
|
||||||
|
intervalMs: 250,
|
||||||
|
});
|
||||||
|
|
||||||
|
fillIfEmpty(fields.address1, fallback.address1);
|
||||||
|
fillIfEmpty(fields.city, fallback.city);
|
||||||
|
fillIfEmpty(fields.region, fallback.region);
|
||||||
|
fillIfEmpty(fields.postalCode, fallback.postalCode);
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const latest = getStructuredAddressFields();
|
||||||
|
const missing = [];
|
||||||
|
if (!String(latest.address1?.value || '').trim()) missing.push('地址1');
|
||||||
|
if (!String(latest.city?.value || '').trim()) missing.push('城市');
|
||||||
|
if (!String(latest.postalCode?.value || '').trim()) missing.push('邮编');
|
||||||
|
if (missing.length) {
|
||||||
|
throw new Error(`Plus Checkout:账单地址字段未填写完整:${missing.join('、')}。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
address1: latest.address1?.value || '',
|
||||||
|
city: latest.city?.value || '',
|
||||||
|
region: latest.region?.value || '',
|
||||||
|
postalCode: latest.postalCode?.value || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSubscribeButton() {
|
||||||
|
return findClickableByText([
|
||||||
|
/订阅|继续|确认|支付/i,
|
||||||
|
/subscribe|continue|confirm|pay|start\s*subscription|place\s*order/i,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fillPlusBillingAndSubmit(payload = {}) {
|
||||||
|
await waitForDocumentComplete();
|
||||||
|
await selectPayPalPaymentMethod();
|
||||||
|
await fillFullName(payload.fullName || '');
|
||||||
|
|
||||||
|
const countryText = readCountryText();
|
||||||
|
const seed = payload.addressSeed || {
|
||||||
|
query: 'Berlin Mitte',
|
||||||
|
suggestionIndex: 1,
|
||||||
|
fallback: {
|
||||||
|
address1: 'Unter den Linden',
|
||||||
|
city: 'Berlin',
|
||||||
|
region: 'Berlin',
|
||||||
|
postalCode: '10117',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const selected = await selectAddressSuggestion(seed);
|
||||||
|
const structuredAddress = await ensureStructuredAddress(seed);
|
||||||
|
|
||||||
|
const subscribeButton = await waitUntil(() => {
|
||||||
|
const button = findSubscribeButton();
|
||||||
|
return button && isEnabledControl(button) ? button : null;
|
||||||
|
}, {
|
||||||
|
label: '订阅按钮',
|
||||||
|
intervalMs: 250,
|
||||||
|
});
|
||||||
|
|
||||||
|
simulateClick(subscribeButton);
|
||||||
|
return {
|
||||||
|
countryText,
|
||||||
|
selectedAddressText: selected.selectedText,
|
||||||
|
structuredAddress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspectPlusCheckoutState() {
|
||||||
|
const structuredAddress = getStructuredAddressFields();
|
||||||
|
return {
|
||||||
|
url: location.href,
|
||||||
|
readyState: document.readyState,
|
||||||
|
countryText: readCountryText(),
|
||||||
|
hasPayPal: Boolean(findClickableByText([/paypal/i])),
|
||||||
|
hasSubscribeButton: Boolean(findSubscribeButton()),
|
||||||
|
addressFieldValues: {
|
||||||
|
address1: structuredAddress.address1?.value || '',
|
||||||
|
city: structuredAddress.city?.value || '',
|
||||||
|
region: structuredAddress.region?.value || '',
|
||||||
|
postalCode: structuredAddress.postalCode?.value || '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -32,9 +32,10 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|
|||||||
handleCommand(message).then((result) => {
|
handleCommand(message).then((result) => {
|
||||||
sendResponse({ ok: true, ...(result || {}) });
|
sendResponse({ ok: true, ...(result || {}) });
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
const reportedStep = Number(message.payload?.visibleStep) || message.step;
|
||||||
if (isStopError(err)) {
|
if (isStopError(err)) {
|
||||||
if (message.step) {
|
if (reportedStep) {
|
||||||
log(`步骤 ${message.step || 8}:已被用户停止。`, 'warn');
|
log(`步骤 ${reportedStep || 8}:已被用户停止。`, 'warn');
|
||||||
}
|
}
|
||||||
sendResponse({ stopped: true, error: err.message });
|
sendResponse({ stopped: true, error: err.message });
|
||||||
return;
|
return;
|
||||||
@@ -46,8 +47,8 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.step) {
|
if (reportedStep) {
|
||||||
reportError(message.step, err.message);
|
reportError(reportedStep, err.message);
|
||||||
}
|
}
|
||||||
sendResponse({ error: err.message });
|
sendResponse({ error: err.message });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ async function handleStep(step, payload = {}) {
|
|||||||
case 1:
|
case 1:
|
||||||
return step1_generateOpenAiAuthUrl(payload);
|
return step1_generateOpenAiAuthUrl(payload);
|
||||||
case 10:
|
case 10:
|
||||||
return step9_submitOpenAiCallback(payload);
|
case 12:
|
||||||
|
return step9_submitOpenAiCallback({ ...(payload || {}), visibleStep: step });
|
||||||
default:
|
default:
|
||||||
throw new Error(`sub2api-panel.js 不处理步骤 ${step}`);
|
throw new Error(`sub2api-panel.js 不处理步骤 ${step}`);
|
||||||
}
|
}
|
||||||
@@ -501,6 +502,7 @@ async function step1_generateOpenAiAuthUrl(payload = {}, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function step9_submitOpenAiCallback(payload = {}) {
|
async function step9_submitOpenAiCallback(payload = {}) {
|
||||||
|
const visibleStep = Number(payload?.visibleStep) || 10;
|
||||||
const callback = parseLocalhostCallback(payload.localhostUrl || '');
|
const callback = parseLocalhostCallback(payload.localhostUrl || '');
|
||||||
const backgroundState = await getBackgroundState();
|
const backgroundState = await getBackgroundState();
|
||||||
const flowEmail = String(backgroundState.email || '').trim();
|
const flowEmail = String(backgroundState.email || '').trim();
|
||||||
@@ -574,7 +576,7 @@ async function step9_submitOpenAiCallback(payload = {}) {
|
|||||||
createPayload.extra = extra;
|
createPayload.extra = extra;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`步骤 10:授权码交换成功,正在创建 SUB2API 账号(名称:${accountName})...`);
|
log(`步骤 ${visibleStep}:授权码交换成功,正在创建 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,
|
||||||
@@ -582,8 +584,8 @@ async function step9_submitOpenAiCallback(payload = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`;
|
const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`;
|
||||||
log(`步骤 10:${verifiedStatus}`, 'ok');
|
log(`步骤 ${visibleStep}:${verifiedStatus}`, 'ok');
|
||||||
reportComplete(10, {
|
reportComplete(visibleStep, {
|
||||||
localhostUrl: callback.url,
|
localhostUrl: callback.url,
|
||||||
verifiedStatus,
|
verifiedStatus,
|
||||||
});
|
});
|
||||||
|
|||||||
+16
-12
@@ -86,7 +86,9 @@ 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 10: return await step9_vpsVerify(payload);
|
case 10:
|
||||||
|
case 12:
|
||||||
|
return await step9_vpsVerify({ ...(payload || {}), visibleStep: step });
|
||||||
default:
|
default:
|
||||||
throw new Error(`vps-panel.js 不处理步骤 ${step}`);
|
throw new Error(`vps-panel.js 不处理步骤 ${step}`);
|
||||||
}
|
}
|
||||||
@@ -1009,27 +1011,29 @@ async function step1_getOAuthLink(payload, options = {}) {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function step9_vpsVerify(payload) {
|
async function step9_vpsVerify(payload) {
|
||||||
await ensureOAuthManagementPage(payload?.vpsPassword, 9);
|
const visibleStep = Number(payload?.visibleStep) || 10;
|
||||||
|
const confirmStep = visibleStep === 12 ? 11 : 9;
|
||||||
|
await ensureOAuthManagementPage(payload?.vpsPassword, confirmStep);
|
||||||
|
|
||||||
// 优先从 payload 读取 localhostUrl;没有时再回退到全局状态
|
// 优先从 payload 读取 localhostUrl;没有时再回退到全局状态
|
||||||
let localhostUrl = payload?.localhostUrl;
|
let localhostUrl = payload?.localhostUrl;
|
||||||
if (localhostUrl && !isLocalhostOAuthCallbackUrl(localhostUrl)) {
|
if (localhostUrl && !isLocalhostOAuthCallbackUrl(localhostUrl)) {
|
||||||
throw new Error('步骤 10 只接受真实的 localhost OAuth 回调地址,请重新执行步骤 9。');
|
throw new Error(`步骤 ${visibleStep} 只接受真实的 localhost OAuth 回调地址,请重新执行步骤 ${confirmStep}。`);
|
||||||
}
|
}
|
||||||
if (!localhostUrl) {
|
if (!localhostUrl) {
|
||||||
log('步骤 10:payload 中没有 localhostUrl,正在从状态中读取...');
|
log(`步骤 ${visibleStep}:payload 中没有 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('步骤 10 只接受真实的 localhost OAuth 回调地址,请重新执行步骤 9。');
|
throw new Error(`步骤 ${visibleStep} 只接受真实的 localhost OAuth 回调地址,请重新执行步骤 ${confirmStep}。`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!localhostUrl) {
|
if (!localhostUrl) {
|
||||||
throw new Error('未找到 localhost 回调地址,请先完成步骤 8。');
|
throw new Error(`未找到 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
|
||||||
}
|
}
|
||||||
log(`步骤 10:已获取 localhostUrl:${localhostUrl.slice(0, 60)}...`);
|
log(`步骤 ${visibleStep}:已获取 localhostUrl:${localhostUrl.slice(0, 60)}...`);
|
||||||
|
|
||||||
log('步骤 10:正在查找回调地址输入框...');
|
log(`步骤 ${visibleStep}:正在查找回调地址输入框...`);
|
||||||
|
|
||||||
// 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=...">
|
||||||
@@ -1046,7 +1050,7 @@ async function step9_vpsVerify(payload) {
|
|||||||
|
|
||||||
await humanPause(600, 1500);
|
await humanPause(600, 1500);
|
||||||
fillInput(urlInput, localhostUrl);
|
fillInput(urlInput, localhostUrl);
|
||||||
log(`步骤 10:已填写回调地址:${localhostUrl.slice(0, 80)}...`);
|
log(`步骤 ${visibleStep}:已填写回调地址:${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;
|
||||||
@@ -1067,9 +1071,9 @@ async function step9_vpsVerify(payload) {
|
|||||||
|
|
||||||
await humanPause(450, 1200);
|
await humanPause(450, 1200);
|
||||||
simulateClick(submitBtn);
|
simulateClick(submitBtn);
|
||||||
log('步骤 10:已点击回调提交按钮,正在等待认证结果...');
|
log(`步骤 ${visibleStep}:已点击回调提交按钮,正在等待认证结果...`);
|
||||||
|
|
||||||
const verifiedStatus = await waitForExactSuccessBadge();
|
const verifiedStatus = await waitForExactSuccessBadge();
|
||||||
log(`步骤 10:${verifiedStatus}`, 'ok');
|
log(`步骤 ${visibleStep}:${verifiedStatus}`, 'ok');
|
||||||
reportComplete(10, { localhostUrl, verifiedStatus });
|
reportComplete(visibleStep, { localhostUrl, verifiedStatus });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
(function attachAddressSources(root, factory) {
|
||||||
|
root.MultiPageAddressSources = factory();
|
||||||
|
})(typeof self !== 'undefined' ? self : globalThis, function createAddressSourcesModule() {
|
||||||
|
const COUNTRY_ALIASES = {
|
||||||
|
AU: ['au', 'aus', 'australia', '澳大利亚'],
|
||||||
|
DE: ['de', 'deu', 'germany', 'deutschland', '德国'],
|
||||||
|
FR: ['fr', 'fra', 'france', '法国'],
|
||||||
|
US: ['us', 'usa', 'united states', 'united states of america', 'america', '美国'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ADDRESS_SEEDS = {
|
||||||
|
AU: [
|
||||||
|
{
|
||||||
|
query: 'New South Wales',
|
||||||
|
suggestionIndex: 1,
|
||||||
|
fallback: {
|
||||||
|
address1: 'Thyne Reid Drive',
|
||||||
|
city: 'Thredbo',
|
||||||
|
region: 'New South Wales',
|
||||||
|
postalCode: '2625',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: 'Sydney NSW',
|
||||||
|
suggestionIndex: 1,
|
||||||
|
fallback: {
|
||||||
|
address1: 'George Street',
|
||||||
|
city: 'Sydney',
|
||||||
|
region: 'New South Wales',
|
||||||
|
postalCode: '2000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
DE: [
|
||||||
|
{
|
||||||
|
query: 'Berlin Mitte',
|
||||||
|
suggestionIndex: 1,
|
||||||
|
fallback: {
|
||||||
|
address1: 'Unter den Linden',
|
||||||
|
city: 'Berlin',
|
||||||
|
region: 'Berlin',
|
||||||
|
postalCode: '10117',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: 'Munich Altstadt',
|
||||||
|
suggestionIndex: 1,
|
||||||
|
fallback: {
|
||||||
|
address1: 'Marienplatz',
|
||||||
|
city: 'Munich',
|
||||||
|
region: 'Bavaria',
|
||||||
|
postalCode: '80331',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
FR: [
|
||||||
|
{
|
||||||
|
query: 'Paris France',
|
||||||
|
suggestionIndex: 1,
|
||||||
|
fallback: {
|
||||||
|
address1: 'Rue de Rivoli',
|
||||||
|
city: 'Paris',
|
||||||
|
region: 'Ile-de-France',
|
||||||
|
postalCode: '75001',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: 'Lyon France',
|
||||||
|
suggestionIndex: 1,
|
||||||
|
fallback: {
|
||||||
|
address1: 'Rue de la Republique',
|
||||||
|
city: 'Lyon',
|
||||||
|
region: 'Auvergne-Rhone-Alpes',
|
||||||
|
postalCode: '69002',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
US: [
|
||||||
|
{
|
||||||
|
query: 'New York NY',
|
||||||
|
suggestionIndex: 1,
|
||||||
|
fallback: {
|
||||||
|
address1: 'Broadway',
|
||||||
|
city: 'New York',
|
||||||
|
region: 'New York',
|
||||||
|
postalCode: '10007',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeCountryCode(value = '') {
|
||||||
|
const normalized = String(value || '').trim().toLowerCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [code, aliases] of Object.entries(COUNTRY_ALIASES)) {
|
||||||
|
if (aliases.some((alias) => normalized === alias || normalized.includes(alias))) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const compact = normalized.replace(/[^a-z]/g, '').toUpperCase();
|
||||||
|
return ADDRESS_SEEDS[compact] ? compact : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAddressSeedForCountry(countryValue = '', options = {}) {
|
||||||
|
const fallbackCountry = normalizeCountryCode(options.fallbackCountry || 'DE') || 'DE';
|
||||||
|
const countryCode = normalizeCountryCode(countryValue) || fallbackCountry;
|
||||||
|
const candidates = ADDRESS_SEEDS[countryCode] || ADDRESS_SEEDS[fallbackCountry] || [];
|
||||||
|
const seed = candidates[0] || null;
|
||||||
|
if (!seed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
countryCode,
|
||||||
|
query: seed.query,
|
||||||
|
suggestionIndex: Math.max(0, Math.floor(Number(seed.suggestionIndex) || 0)),
|
||||||
|
fallback: { ...(seed.fallback || {}) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ADDRESS_SEEDS,
|
||||||
|
getAddressSeedForCountry,
|
||||||
|
normalizeCountryCode,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
(function attachStepDefinitions(root, factory) {
|
(function attachStepDefinitions(root, factory) {
|
||||||
root.MultiPageStepDefinitions = factory();
|
root.MultiPageStepDefinitions = factory();
|
||||||
})(typeof self !== 'undefined' ? self : globalThis, function createStepDefinitionsModule() {
|
})(typeof self !== 'undefined' ? self : globalThis, function createStepDefinitionsModule() {
|
||||||
const STEP_DEFINITIONS = [
|
const NORMAL_STEP_DEFINITIONS = [
|
||||||
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' },
|
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' },
|
||||||
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' },
|
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' },
|
||||||
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' },
|
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' },
|
||||||
@@ -14,19 +14,77 @@
|
|||||||
{ id: 10, order: 100, key: 'platform-verify', title: '平台回调验证' },
|
{ id: 10, order: 100, key: 'platform-verify', title: '平台回调验证' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function getSteps() {
|
const PLUS_STEP_DEFINITIONS = [
|
||||||
return STEP_DEFINITIONS.map((step) => ({ ...step }));
|
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' },
|
||||||
|
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' },
|
||||||
|
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' },
|
||||||
|
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' },
|
||||||
|
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' },
|
||||||
|
{ id: 6, order: 60, key: 'plus-checkout-create', title: '创建 Plus Checkout' },
|
||||||
|
{ id: 7, order: 70, key: 'plus-checkout-billing', title: '填写账单并提交订阅' },
|
||||||
|
{ id: 8, order: 80, key: 'paypal-approve', title: 'PayPal 登录与授权' },
|
||||||
|
{ id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认' },
|
||||||
|
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' },
|
||||||
|
{ id: 11, order: 110, key: 'confirm-oauth', title: '自动确认 OAuth' },
|
||||||
|
{ id: 12, order: 120, key: 'platform-verify', title: '平台回调验证' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function isPlusModeEnabled(options = {}) {
|
||||||
|
return Boolean(options?.plusModeEnabled || options?.plusMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStepById(id) {
|
function getModeStepDefinitions(options = {}) {
|
||||||
|
return isPlusModeEnabled(options) ? PLUS_STEP_DEFINITIONS : NORMAL_STEP_DEFINITIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneSteps(steps = []) {
|
||||||
|
return steps.map((step) => ({ ...step }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSteps(options = {}) {
|
||||||
|
return cloneSteps(getModeStepDefinitions(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllSteps() {
|
||||||
|
const keyed = new Map();
|
||||||
|
for (const step of [...NORMAL_STEP_DEFINITIONS, ...PLUS_STEP_DEFINITIONS]) {
|
||||||
|
keyed.set(`${step.id}:${step.key}`, step);
|
||||||
|
}
|
||||||
|
return cloneSteps(Array.from(keyed.values()).sort((left, right) => {
|
||||||
|
const leftOrder = Number.isFinite(left.order) ? left.order : left.id;
|
||||||
|
const rightOrder = Number.isFinite(right.order) ? right.order : right.id;
|
||||||
|
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
||||||
|
return left.id - right.id;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStepIds(options = {}) {
|
||||||
|
return getModeStepDefinitions(options)
|
||||||
|
.map((step) => Number(step.id))
|
||||||
|
.filter(Number.isFinite)
|
||||||
|
.sort((left, right) => left - right);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastStepId(options = {}) {
|
||||||
|
const ids = getStepIds(options);
|
||||||
|
return ids[ids.length - 1] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStepById(id, options = {}) {
|
||||||
const numericId = Number(id);
|
const numericId = Number(id);
|
||||||
const match = STEP_DEFINITIONS.find((step) => step.id === numericId);
|
const match = getModeStepDefinitions(options).find((step) => step.id === numericId);
|
||||||
return match ? { ...match } : null;
|
return match ? { ...match } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
STEP_DEFINITIONS,
|
STEP_DEFINITIONS: NORMAL_STEP_DEFINITIONS,
|
||||||
|
NORMAL_STEP_DEFINITIONS,
|
||||||
|
PLUS_STEP_DEFINITIONS,
|
||||||
|
getAllSteps,
|
||||||
|
getLastStepId,
|
||||||
getStepById,
|
getStepById,
|
||||||
|
getStepIds,
|
||||||
getSteps,
|
getSteps,
|
||||||
|
isPlusModeEnabled,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -212,6 +212,28 @@
|
|||||||
<button id="btn-toggle-password" class="input-icon-btn" type="button" aria-label="显示密码" title="显示密码"></button>
|
<button id="btn-toggle-password" class="input-icon-btn" type="button" aria-label="显示密码" title="显示密码"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="data-row" id="row-plus-mode">
|
||||||
|
<span class="data-label">Plus 模式</span>
|
||||||
|
<div class="data-inline setting-pair">
|
||||||
|
<div class="setting-group setting-group-primary">
|
||||||
|
<label class="toggle-switch" for="input-plus-mode-enabled" title="开启后使用 Plus Checkout + PayPal 授权流程">
|
||||||
|
<input type="checkbox" id="input-plus-mode-enabled" />
|
||||||
|
<span class="toggle-switch-track" aria-hidden="true">
|
||||||
|
<span class="toggle-switch-thumb"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<span class="setting-caption">PayPal 订阅链路</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-paypal-email" style="display:none;">
|
||||||
|
<span class="data-label">PayPal 账号</span>
|
||||||
|
<input type="text" id="input-paypal-email" class="data-input" placeholder="请输入 PayPal 登录邮箱" />
|
||||||
|
</div>
|
||||||
|
<div class="data-row" id="row-paypal-password" style="display:none;">
|
||||||
|
<span class="data-label">PayPal 密码</span>
|
||||||
|
<input type="password" id="input-paypal-password" class="data-input" placeholder="请输入 PayPal 登录密码" />
|
||||||
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">邮箱服务</span>
|
<span class="data-label">邮箱服务</span>
|
||||||
<div class="data-inline">
|
<div class="data-inline">
|
||||||
|
|||||||
+105
-5
@@ -99,6 +99,12 @@ const inputCodex2ApiUrl = document.getElementById('input-codex2api-url');
|
|||||||
const rowCodex2ApiAdminKey = document.getElementById('row-codex2api-admin-key');
|
const rowCodex2ApiAdminKey = document.getElementById('row-codex2api-admin-key');
|
||||||
const inputCodex2ApiAdminKey = document.getElementById('input-codex2api-admin-key');
|
const inputCodex2ApiAdminKey = document.getElementById('input-codex2api-admin-key');
|
||||||
const rowCustomPassword = document.getElementById('row-custom-password');
|
const rowCustomPassword = document.getElementById('row-custom-password');
|
||||||
|
const rowPlusMode = document.getElementById('row-plus-mode');
|
||||||
|
const inputPlusModeEnabled = document.getElementById('input-plus-mode-enabled');
|
||||||
|
const rowPaypalEmail = document.getElementById('row-paypal-email');
|
||||||
|
const inputPaypalEmail = document.getElementById('input-paypal-email');
|
||||||
|
const rowPaypalPassword = document.getElementById('row-paypal-password');
|
||||||
|
const inputPaypalPassword = document.getElementById('input-paypal-password');
|
||||||
const selectMailProvider = document.getElementById('select-mail-provider');
|
const selectMailProvider = document.getElementById('select-mail-provider');
|
||||||
const btnMailLogin = document.getElementById('btn-mail-login');
|
const btnMailLogin = document.getElementById('btn-mail-login');
|
||||||
const rowCustomMailProviderPool = document.getElementById('row-custom-mail-provider-pool');
|
const rowCustomMailProviderPool = document.getElementById('row-custom-mail-provider-pool');
|
||||||
@@ -244,11 +250,12 @@ const btnAutoStartCancel = document.getElementById('btn-auto-start-cancel');
|
|||||||
const btnAutoStartRestart = document.getElementById('btn-auto-start-restart');
|
const btnAutoStartRestart = document.getElementById('btn-auto-start-restart');
|
||||||
const btnAutoStartContinue = document.getElementById('btn-auto-start-continue');
|
const btnAutoStartContinue = document.getElementById('btn-auto-start-continue');
|
||||||
const autoHintText = document.querySelector('.auto-hint');
|
const autoHintText = document.querySelector('.auto-hint');
|
||||||
const stepDefinitions = (window.MultiPageStepDefinitions?.getSteps?.() || []).sort((left, right) => left.order - right.order);
|
|
||||||
const STEP_IDS = stepDefinitions.map((step) => Number(step.id)).filter(Number.isFinite);
|
|
||||||
const STEP_DEFAULT_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending']));
|
|
||||||
const SKIPPABLE_STEPS = new Set(STEP_IDS);
|
|
||||||
const stepsList = document.querySelector('.steps-list');
|
const stepsList = document.querySelector('.steps-list');
|
||||||
|
let currentPlusModeEnabled = false;
|
||||||
|
let stepDefinitions = getStepDefinitionsForMode(false);
|
||||||
|
let STEP_IDS = stepDefinitions.map((step) => Number(step.id)).filter(Number.isFinite);
|
||||||
|
let STEP_DEFAULT_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending']));
|
||||||
|
let SKIPPABLE_STEPS = new Set(STEP_IDS);
|
||||||
const AUTO_DELAY_MIN_MINUTES = 1;
|
const AUTO_DELAY_MIN_MINUTES = 1;
|
||||||
const AUTO_DELAY_MAX_MINUTES = 1440;
|
const AUTO_DELAY_MAX_MINUTES = 1440;
|
||||||
const AUTO_DELAY_DEFAULT_MINUTES = 30;
|
const AUTO_DELAY_DEFAULT_MINUTES = 30;
|
||||||
@@ -269,6 +276,24 @@ const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
|
|||||||
const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed';
|
const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed';
|
||||||
const AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-skip-failures-prompt-dismissed';
|
const AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-skip-failures-prompt-dismissed';
|
||||||
const AUTO_RUN_FALLBACK_RISK_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-run-fallback-risk-prompt-dismissed';
|
const AUTO_RUN_FALLBACK_RISK_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-run-fallback-risk-prompt-dismissed';
|
||||||
|
|
||||||
|
function getStepDefinitionsForMode(plusModeEnabled = false) {
|
||||||
|
return (window.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled }) || [])
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftOrder = Number.isFinite(left.order) ? left.order : left.id;
|
||||||
|
const rightOrder = Number.isFinite(right.order) ? right.order : right.id;
|
||||||
|
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
||||||
|
return left.id - right.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildStepDefinitionState(plusModeEnabled = false) {
|
||||||
|
currentPlusModeEnabled = Boolean(plusModeEnabled);
|
||||||
|
stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled);
|
||||||
|
STEP_IDS = stepDefinitions.map((step) => Number(step.id)).filter(Number.isFinite);
|
||||||
|
STEP_DEFAULT_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending']));
|
||||||
|
SKIPPABLE_STEPS = new Set(STEP_IDS);
|
||||||
|
}
|
||||||
const CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY = 'multipage-contribution-content-prompt-dismissed-version';
|
const CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY = 'multipage-contribution-content-prompt-dismissed-version';
|
||||||
const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 3;
|
const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 3;
|
||||||
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
|
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
|
||||||
@@ -1100,7 +1125,8 @@ function isDoneStatus(status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getStepStatuses(state = latestState) {
|
function getStepStatuses(state = latestState) {
|
||||||
return { ...STEP_DEFAULT_STATUSES, ...(state?.stepStatuses || {}) };
|
const merged = { ...STEP_DEFAULT_STATUSES, ...(state?.stepStatuses || {}) };
|
||||||
|
return Object.fromEntries(STEP_IDS.map((stepId) => [stepId, merged[stepId] || 'pending']));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFirstUnfinishedStep(state = latestState) {
|
function getFirstUnfinishedStep(state = latestState) {
|
||||||
@@ -1690,6 +1716,15 @@ function collectSettingsPayload() {
|
|||||||
sub2apiDefaultProxyName: inputSub2ApiDefaultProxy.value.trim(),
|
sub2apiDefaultProxyName: inputSub2ApiDefaultProxy.value.trim(),
|
||||||
codex2apiUrl: inputCodex2ApiUrl.value.trim(),
|
codex2apiUrl: inputCodex2ApiUrl.value.trim(),
|
||||||
codex2apiAdminKey: inputCodex2ApiAdminKey.value.trim(),
|
codex2apiAdminKey: inputCodex2ApiAdminKey.value.trim(),
|
||||||
|
plusModeEnabled: typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
|
||||||
|
? Boolean(inputPlusModeEnabled.checked)
|
||||||
|
: Boolean(latestState?.plusModeEnabled),
|
||||||
|
paypalEmail: typeof inputPaypalEmail !== 'undefined' && inputPaypalEmail
|
||||||
|
? inputPaypalEmail.value.trim()
|
||||||
|
: String(latestState?.paypalEmail || ''),
|
||||||
|
paypalPassword: typeof inputPaypalPassword !== 'undefined' && inputPaypalPassword
|
||||||
|
? inputPaypalPassword.value
|
||||||
|
: String(latestState?.paypalPassword || ''),
|
||||||
...(contributionModeEnabled ? {} : {
|
...(contributionModeEnabled ? {} : {
|
||||||
customPassword: inputPassword.value,
|
customPassword: inputPassword.value,
|
||||||
}),
|
}),
|
||||||
@@ -1924,6 +1959,21 @@ function updatePhoneVerificationSettingsUI() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePlusModeUI() {
|
||||||
|
const enabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
|
||||||
|
? Boolean(inputPlusModeEnabled.checked)
|
||||||
|
: false;
|
||||||
|
[
|
||||||
|
typeof rowPaypalEmail !== 'undefined' ? rowPaypalEmail : null,
|
||||||
|
typeof rowPaypalPassword !== 'undefined' ? rowPaypalPassword : null,
|
||||||
|
].forEach((row) => {
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.style.display = enabled ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setSettingsCardLocked(locked) {
|
function setSettingsCardLocked(locked) {
|
||||||
if (!settingsCard) {
|
if (!settingsCard) {
|
||||||
return;
|
return;
|
||||||
@@ -2104,6 +2154,9 @@ function applyAutoRunStatus(payload = currentAutoRun) {
|
|||||||
|
|
||||||
function initializeManualStepActions() {
|
function initializeManualStepActions() {
|
||||||
document.querySelectorAll('.step-row').forEach((row) => {
|
document.querySelectorAll('.step-row').forEach((row) => {
|
||||||
|
if (row.querySelector('.step-actions')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const step = Number(row.dataset.step);
|
const step = Number(row.dataset.step);
|
||||||
const statusEl = row.querySelector('.step-status');
|
const statusEl = row.querySelector('.step-status');
|
||||||
if (!statusEl) return;
|
if (!statusEl) return;
|
||||||
@@ -2147,6 +2200,21 @@ function renderStepsList() {
|
|||||||
if (stepsProgress) {
|
if (stepsProgress) {
|
||||||
stepsProgress.textContent = `0 / ${STEP_IDS.length}`;
|
stepsProgress.textContent = `0 / ${STEP_IDS.length}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeManualStepActions();
|
||||||
|
updateProgressCounter();
|
||||||
|
updateButtonStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
|
||||||
|
const nextPlusModeEnabled = Boolean(plusModeEnabled);
|
||||||
|
const shouldRender = Boolean(options.render) || nextPlusModeEnabled !== currentPlusModeEnabled;
|
||||||
|
if (!shouldRender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildStepDefinitionState(nextPlusModeEnabled);
|
||||||
|
renderStepsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -2154,11 +2222,23 @@ function renderStepsList() {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function applySettingsState(state) {
|
function applySettingsState(state) {
|
||||||
|
if (typeof syncStepDefinitionsForMode === 'function') {
|
||||||
|
syncStepDefinitionsForMode(Boolean(state?.plusModeEnabled), { render: true });
|
||||||
|
}
|
||||||
syncLatestState(state);
|
syncLatestState(state);
|
||||||
syncAutoRunState(state);
|
syncAutoRunState(state);
|
||||||
|
|
||||||
inputEmail.value = state?.email || '';
|
inputEmail.value = state?.email || '';
|
||||||
syncPasswordField(state || {});
|
syncPasswordField(state || {});
|
||||||
|
if (typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled) {
|
||||||
|
inputPlusModeEnabled.checked = Boolean(state?.plusModeEnabled);
|
||||||
|
}
|
||||||
|
if (typeof inputPaypalEmail !== 'undefined' && inputPaypalEmail) {
|
||||||
|
inputPaypalEmail.value = state?.paypalEmail || '';
|
||||||
|
}
|
||||||
|
if (typeof inputPaypalPassword !== 'undefined' && inputPaypalPassword) {
|
||||||
|
inputPaypalPassword.value = state?.paypalPassword || '';
|
||||||
|
}
|
||||||
inputVpsUrl.value = state?.vpsUrl || '';
|
inputVpsUrl.value = state?.vpsUrl || '';
|
||||||
inputVpsPassword.value = state?.vpsPassword || '';
|
inputVpsPassword.value = state?.vpsPassword || '';
|
||||||
setLocalCpaStep9Mode(state?.localCpaStep9Mode);
|
setLocalCpaStep9Mode(state?.localCpaStep9Mode);
|
||||||
@@ -2277,6 +2357,9 @@ function applySettingsState(state) {
|
|||||||
updateFallbackThreadIntervalInputState();
|
updateFallbackThreadIntervalInputState();
|
||||||
updateAccountRunHistorySettingsUI();
|
updateAccountRunHistorySettingsUI();
|
||||||
updatePhoneVerificationSettingsUI();
|
updatePhoneVerificationSettingsUI();
|
||||||
|
if (typeof updatePlusModeUI === 'function') {
|
||||||
|
updatePlusModeUI();
|
||||||
|
}
|
||||||
updatePanelModeUI();
|
updatePanelModeUI();
|
||||||
updateMailProviderUI();
|
updateMailProviderUI();
|
||||||
if (isLuckmailProvider(state?.mailProvider)) {
|
if (isLuckmailProvider(state?.mailProvider)) {
|
||||||
@@ -4651,6 +4734,23 @@ inputPassword.addEventListener('blur', () => {
|
|||||||
saveSettings({ silent: true }).catch(() => { });
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
inputPlusModeEnabled?.addEventListener('change', () => {
|
||||||
|
updatePlusModeUI();
|
||||||
|
syncStepDefinitionsForMode(Boolean(inputPlusModeEnabled.checked), { render: true });
|
||||||
|
markSettingsDirty(true);
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
|
[inputPaypalEmail, inputPaypalPassword].forEach((input) => {
|
||||||
|
input?.addEventListener('input', () => {
|
||||||
|
markSettingsDirty(true);
|
||||||
|
scheduleSettingsAutoSave();
|
||||||
|
});
|
||||||
|
input?.addEventListener('blur', () => {
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
selectMailProvider.addEventListener('change', async () => {
|
selectMailProvider.addEventListener('change', async () => {
|
||||||
const previousProvider = latestState?.mailProvider || '';
|
const previousProvider = latestState?.mailProvider || '';
|
||||||
const previousMail2925Mode = latestState?.mail2925Mode;
|
const previousMail2925Mode = latestState?.mail2925Mode;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
test('address sources normalize supported countries and return local seeds', () => {
|
||||||
|
const source = fs.readFileSync('data/address-sources.js', 'utf8');
|
||||||
|
const globalScope = {};
|
||||||
|
const api = new Function('self', `${source}; return self.MultiPageAddressSources;`)(globalScope);
|
||||||
|
|
||||||
|
assert.equal(api.normalizeCountryCode('Deutschland'), 'DE');
|
||||||
|
assert.equal(api.normalizeCountryCode('澳大利亚'), 'AU');
|
||||||
|
assert.equal(api.normalizeCountryCode('unknown'), '');
|
||||||
|
|
||||||
|
const deSeed = api.getAddressSeedForCountry('DE');
|
||||||
|
assert.equal(deSeed.countryCode, 'DE');
|
||||||
|
assert.equal(deSeed.suggestionIndex, 1);
|
||||||
|
assert.equal(Boolean(deSeed.query), true);
|
||||||
|
assert.equal(Boolean(deSeed.fallback.city), true);
|
||||||
|
|
||||||
|
const fallbackSeed = api.getAddressSeedForCountry('unknown', { fallbackCountry: 'AU' });
|
||||||
|
assert.equal(fallbackSeed.countryCode, 'AU');
|
||||||
|
assert.equal(fallbackSeed.fallback.region, 'New South Wales');
|
||||||
|
});
|
||||||
@@ -118,6 +118,9 @@ function isRetryableContentScriptTransportError() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const stepRegistry = {
|
const stepRegistry = {
|
||||||
|
getStepDefinition(step) {
|
||||||
|
return { id: step, key: 'test-step' };
|
||||||
|
},
|
||||||
async executeStep(step) {
|
async executeStep(step) {
|
||||||
events.registryCalls.push(step);
|
events.registryCalls.push(step);
|
||||||
if (step === 8) {
|
if (step === 8) {
|
||||||
@@ -127,6 +130,12 @@ const stepRegistry = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
function getStepRegistryForState() {
|
||||||
|
return stepRegistry;
|
||||||
|
}
|
||||||
|
function getStepDefinitionForState(step) {
|
||||||
|
return { id: step, key: 'test-step' };
|
||||||
|
}
|
||||||
|
|
||||||
${extractFunction('isStopError')}
|
${extractFunction('isStopError')}
|
||||||
${extractFunction('throwIfStopped')}
|
${extractFunction('throwIfStopped')}
|
||||||
@@ -212,10 +221,19 @@ function isRetryableContentScriptTransportError() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const stepRegistry = {
|
const stepRegistry = {
|
||||||
|
getStepDefinition(step) {
|
||||||
|
return { id: step, key: 'test-step' };
|
||||||
|
},
|
||||||
async executeStep() {
|
async executeStep() {
|
||||||
throw new Error('BROWSER_SWITCH_REQUIRED::请更换浏览器进行注册登录。');
|
throw new Error('BROWSER_SWITCH_REQUIRED::请更换浏览器进行注册登录。');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
function getStepRegistryForState() {
|
||||||
|
return stepRegistry;
|
||||||
|
}
|
||||||
|
function getStepDefinitionForState(step) {
|
||||||
|
return { id: step, key: 'test-step' };
|
||||||
|
}
|
||||||
|
|
||||||
${extractFunction('isStopError')}
|
${extractFunction('isStopError')}
|
||||||
${extractFunction('throwIfStopped')}
|
${extractFunction('throwIfStopped')}
|
||||||
|
|||||||
@@ -7,5 +7,10 @@ test('background imports step registry and shared step definitions', () => {
|
|||||||
assert.match(source, /background\/steps\/registry\.js/);
|
assert.match(source, /background\/steps\/registry\.js/);
|
||||||
assert.match(source, /data\/step-definitions\.js/);
|
assert.match(source, /data\/step-definitions\.js/);
|
||||||
assert.match(source, /MultiPageStepDefinitions\?\.getSteps/);
|
assert.match(source, /MultiPageStepDefinitions\?\.getSteps/);
|
||||||
assert.match(source, /stepRegistry\.executeStep\(step,\s*state\)/);
|
assert.match(source, /getStepRegistryForState\(state\)/);
|
||||||
|
assert.match(source, /activeStepRegistry\.executeStep\(step,\s*\{/);
|
||||||
|
assert.match(source, /background\/steps\/create-plus-checkout\.js/);
|
||||||
|
assert.match(source, /background\/steps\/fill-plus-checkout\.js/);
|
||||||
|
assert.match(source, /background\/steps\/paypal-approve\.js/);
|
||||||
|
assert.match(source, /background\/steps\/plus-return-confirm\.js/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ 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('step definitions module exposes ordered shared step metadata', () => {
|
test('step definitions module exposes ordered normal and Plus step metadata', () => {
|
||||||
const source = fs.readFileSync('data/step-definitions.js', 'utf8');
|
const source = fs.readFileSync('data/step-definitions.js', 'utf8');
|
||||||
const globalScope = {};
|
const globalScope = {};
|
||||||
|
|
||||||
const api = new Function('self', `${source}; return self.MultiPageStepDefinitions;`)(globalScope);
|
const api = new Function('self', `${source}; return self.MultiPageStepDefinitions;`)(globalScope);
|
||||||
const steps = api.getSteps();
|
const steps = api.getSteps();
|
||||||
|
const plusSteps = api.getSteps({ plusModeEnabled: true });
|
||||||
|
|
||||||
assert.equal(Array.isArray(steps), true);
|
assert.equal(Array.isArray(steps), true);
|
||||||
assert.equal(steps.length >= 10, true);
|
assert.equal(steps.length, 10);
|
||||||
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)
|
||||||
@@ -30,6 +31,27 @@ test('step definitions module exposes ordered shared step metadata', () => {
|
|||||||
'platform-verify',
|
'platform-verify',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
plusSteps.map((step) => step.key),
|
||||||
|
[
|
||||||
|
'open-chatgpt',
|
||||||
|
'submit-signup-email',
|
||||||
|
'fill-password',
|
||||||
|
'fetch-signup-code',
|
||||||
|
'fill-profile',
|
||||||
|
'plus-checkout-create',
|
||||||
|
'plus-checkout-billing',
|
||||||
|
'paypal-approve',
|
||||||
|
'plus-checkout-return',
|
||||||
|
'oauth-login',
|
||||||
|
'confirm-oauth',
|
||||||
|
'platform-verify',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert.equal(plusSteps.some((step) => step.key === 'clear-login-cookies'), false);
|
||||||
|
assert.equal(plusSteps.some((step) => step.key === 'fetch-login-code'), false);
|
||||||
|
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
|
||||||
|
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 12);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
|
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
|
||||||
@@ -41,3 +63,10 @@ test('sidepanel html loads shared step definitions before sidepanel bootstrap',
|
|||||||
assert.notEqual(sidepanelIndex, -1);
|
assert.notEqual(sidepanelIndex, -1);
|
||||||
assert.ok(definitionsIndex < sidepanelIndex);
|
assert.ok(definitionsIndex < sidepanelIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sidepanel html exposes Plus mode and PayPal settings', () => {
|
||||||
|
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||||
|
assert.match(html, /id="input-plus-mode-enabled"/);
|
||||||
|
assert.match(html, /id="input-paypal-email"/);
|
||||||
|
assert.match(html, /id="input-paypal-password"/);
|
||||||
|
});
|
||||||
|
|||||||
+37
-1
@@ -23,6 +23,7 @@
|
|||||||
- 轮询登录验证码
|
- 轮询登录验证码
|
||||||
- 自动确认 OAuth 同意页
|
- 自动确认 OAuth 同意页
|
||||||
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
|
- 把 localhost 回调提交到 CPA、SUB2API 或 Codex2API
|
||||||
|
- 可选开启 Plus 模式:先完成 Plus Checkout、账单地址、PayPal 登录授权与订阅回跳确认,再复用 OAuth 后半段链路
|
||||||
|
|
||||||
## 2. 核心运行参与者
|
## 2. 核心运行参与者
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表
|
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表
|
||||||
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Pro` 与 legacy `v` 两个版本族,排序时优先保持版本族语义一致,同时会在读取缓存后重新排序,避免旧缓存把 `v` 版本误显示为比 `Pro` 更新
|
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Pro` 与 legacy `v` 两个版本族,排序时优先保持版本族语义一致,同时会在读取缓存后重新排序,避免旧缓存把 `v` 版本误显示为比 `Pro` 更新
|
||||||
- 展示一个单独的“接码”开关;开启后才展示 HeroSMS 的接码国家与 API Key 设置,用于 OAuth 登录链路命中手机号验证页时直接续跑手机验证
|
- 展示一个单独的“接码”开关;开启后才展示 HeroSMS 的接码国家与 API Key 设置,用于 OAuth 登录链路命中手机号验证页时直接续跑手机验证
|
||||||
|
- 展示 `Plus 模式` 开关与 PayPal 账号/密码配置;开启后步骤列表切换为 Plus 模式 12 步定义,普通模式的 Cookie 清理与登录验证码步骤不再显示或执行
|
||||||
- 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作
|
- 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作
|
||||||
|
|
||||||
### 2.2 Background Service Worker
|
### 2.2 Background Service Worker
|
||||||
@@ -116,6 +118,7 @@
|
|||||||
- 步骤顺序靠 `order`
|
- 步骤顺序靠 `order`
|
||||||
- 步骤文件名靠语义
|
- 步骤文件名靠语义
|
||||||
- 新增步骤时不需要重命名后续文件
|
- 新增步骤时不需要重命名后续文件
|
||||||
|
- 普通模式使用 10 步定义;Plus 模式使用 12 步定义,其中 Plus 可见步骤 10/11/12 复用原 OAuth 登录、OAuth 同意页与平台回调验证执行器,但按 Plus 可见步骤编号记录状态
|
||||||
|
|
||||||
## 4. 状态与存储链路
|
## 4. 状态与存储链路
|
||||||
|
|
||||||
@@ -138,6 +141,7 @@
|
|||||||
- 第 8 步固定的验证码页显示邮箱 `step8VerificationTargetEmail`
|
- 第 8 步固定的验证码页显示邮箱 `step8VerificationTargetEmail`
|
||||||
- 当前手机号验证激活记录 `currentPhoneActivation`
|
- 当前手机号验证激活记录 `currentPhoneActivation`
|
||||||
- 可复用的手机号验证激活记录 `reusablePhoneActivation`
|
- 可复用的手机号验证激活记录 `reusablePhoneActivation`
|
||||||
|
- Plus checkout / PayPal 运行态:`plusCheckoutTabId`、`plusCheckoutUrl`、`plusCheckoutCountry`、`plusCheckoutCurrency`、`plusBillingCountryText`、`plusBillingAddress`、`plusPaypalApprovedAt`、`plusReturnUrl`
|
||||||
- localhost 回调地址
|
- localhost 回调地址
|
||||||
- 自动运行轮次信息
|
- 自动运行轮次信息
|
||||||
- 当前自动运行 session 标识 `autoRunSessionId`
|
- 当前自动运行 session 标识 `autoRunSessionId`
|
||||||
@@ -156,6 +160,8 @@
|
|||||||
|
|
||||||
- CPA / SUB2API 配置
|
- CPA / SUB2API 配置
|
||||||
- Codex2API 配置
|
- Codex2API 配置
|
||||||
|
- Plus 模式开关 `plusModeEnabled`
|
||||||
|
- PayPal 登录配置 `paypalEmail / paypalPassword`
|
||||||
- 邮箱 provider 配置
|
- 邮箱 provider 配置
|
||||||
- Hotmail 账号池
|
- Hotmail 账号池
|
||||||
- 2925 账号池
|
- 2925 账号池
|
||||||
@@ -479,6 +485,34 @@ Codex2API 补充:
|
|||||||
- 步骤 10 会先尝试提交已捕获的 callback URL,随后轮询公开贡献状态,直到进入最终态
|
- 步骤 10 会先尝试提交已捕获的 callback URL,随后轮询公开贡献状态,直到进入最终态
|
||||||
- `auto_approved` 与 `manual_review_required` 视为主流程完成;`auto_rejected / expired / error` 视为当前轮失败
|
- `auto_approved` 与 `manual_review_required` 视为主流程完成;`auto_rejected / expired / error` 视为当前轮失败
|
||||||
|
|
||||||
|
## 6.1 Plus 模式链路
|
||||||
|
|
||||||
|
Plus 模式通过 `plusModeEnabled` 开启,目标是在普通注册资料完成后,不执行原 Step 6 Cookie 清理,也不执行原 Step 8 登录验证码步骤,而是先完成 Plus Checkout 与 PayPal 授权,再复用现有 OAuth 后半段。
|
||||||
|
|
||||||
|
Plus 模式可见步骤:
|
||||||
|
|
||||||
|
1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。
|
||||||
|
2. 第 6 步 `创建 Plus Checkout`:打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session,并打开 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`。
|
||||||
|
3. 第 7 步 `填写账单并提交订阅`:选择 PayPal,生成账单全名,从 `data/address-sources.js` 读取同国家 seed query,触发 checkout 内置 Google 地址推荐,选择推荐项并校验地址第 1 行、城市、州/省、邮编等结构化字段,再点击“订阅”。
|
||||||
|
4. 第 8 步 `PayPal 登录与授权`:在 PayPal 页面填写 `paypalEmail / paypalPassword`,登录前固定等待 1 秒,关闭可见通行密钥提示,点击“同意并继续”。
|
||||||
|
5. 第 9 步 `订阅回跳确认`:等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。
|
||||||
|
6. 第 10 步:复用原 Step 7 OAuth 登录执行器,但状态和日志按 Plus 可见第 10 步记录。
|
||||||
|
7. 第 11 步:复用原 Step 9 OAuth 同意页点击和 localhost callback 捕获执行器,但状态和日志按 Plus 可见第 11 步记录。
|
||||||
|
8. 第 12 步:复用原 Step 10 CPA / SUB2API / Codex2API 平台回调验证执行器,但状态和日志按 Plus 可见第 12 步记录。
|
||||||
|
|
||||||
|
隐藏与跳过规则:
|
||||||
|
|
||||||
|
- 原 Step 6 `清理登录 Cookies`:Plus 模式下隐藏且不执行,因为 Plus Checkout 创建依赖当前 ChatGPT 登录态。
|
||||||
|
- 原 Step 8 `获取登录验证码`:Plus 模式下隐藏且默认跳过,Plus 后半段直接从可见第 10 步进入第 11 步。
|
||||||
|
- 原 Step 9 不能跳过;它负责点击 OAuth 同意页并捕获 `localhostUrl`。
|
||||||
|
- 原 Step 10 不能跳过;它依赖 `localhostUrl` 完成平台侧账号创建或回调验证。
|
||||||
|
|
||||||
|
等待模型:
|
||||||
|
|
||||||
|
- Plus Checkout 和 PayPal 专用步骤使用“无限等待但可停止”的后台等待 helper。
|
||||||
|
- 每次页面加载完成后固定等待 1 秒,再继续下一次输入、点击或状态判断。
|
||||||
|
- 第一版不实时抓取外部地址网站;地址自动化只依赖本地 seed query 和 checkout 页面内置 Google 地址推荐。
|
||||||
|
|
||||||
## 7. 邮箱与 provider 链路
|
## 7. 邮箱与 provider 链路
|
||||||
|
|
||||||
### 7.1 2026-04-17 补充:Gmail / 2925 统一别名邮箱链路
|
### 7.1 2026-04-17 补充:Gmail / 2925 统一别名邮箱链路
|
||||||
@@ -716,9 +750,11 @@ Codex2API 补充:
|
|||||||
- 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态
|
- 如果当前 `Mail = 自定义邮箱` 且配置了 `customMailProviderPool`,会先按当前目标轮次把号池中的对应邮箱写回运行态
|
||||||
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
|
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
|
||||||
5. 执行 `runAutoSequenceFromStep`
|
5. 执行 `runAutoSequenceFromStep`
|
||||||
|
- 自动运行会按当前 `plusModeEnabled` 选择普通 10 步或 Plus 12 步可见步骤;Plus 模式下第 6~9 步走 checkout / PayPal,第 10~12 步复用 OAuth 后半段
|
||||||
- 步骤 7 内部仍保留登录态恢复的有限重试,但 `add-phone / 手机号页` 属于立即跳出的不可重试错误
|
- 步骤 7 内部仍保留登录态恢复的有限重试,但 `add-phone / 手机号页` 属于立即跳出的不可重试错误
|
||||||
- 步骤 8 若在验证码提交后进入 `add-phone / 手机号页`,会直接抛出 fatal 错误,不再先标记步骤成功
|
- 步骤 8 若在验证码提交后进入 `add-phone / 手机号页`,会直接抛出 fatal 错误,不再先标记步骤成功
|
||||||
- 一旦进入步骤 7~10,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 7 无限重开
|
- 普通模式一旦进入步骤 7~10,遇到普通报错且认证流程未进入 `add-phone`,则自动回到步骤 7 无限重开
|
||||||
|
- Plus 模式在 checkout / PayPal 结束后,如果 OAuth 后半段遇到普通报错且认证流程未进入 `add-phone`,则自动回到 Plus 可见步骤 10 重新开始授权链路
|
||||||
- 如果命中 `add-phone / 手机号页` 这类 fatal 错误,则不会再做当前轮的内部重试;当开启自动重试/跳过失败时,会直接结束当前轮并继续下一轮,而不是把整条自动流程暂停
|
- 如果命中 `add-phone / 手机号页` 这类 fatal 错误,则不会再做当前轮的内部重试;当开启自动重试/跳过失败时,会直接结束当前轮并继续下一轮,而不是把整条自动流程暂停
|
||||||
- 如果是手动停止,则立即退出自动流程,不会再触发“回到步骤 7 重开”
|
- 如果是手动停止,则立即退出自动流程,不会再触发“回到步骤 7 重开”
|
||||||
6. 如果失败,根据设置决定:
|
6. 如果失败,根据设置决定:
|
||||||
|
|||||||
+12
-4
@@ -43,7 +43,7 @@
|
|||||||
## `background/`
|
## `background/`
|
||||||
|
|
||||||
- `background/account-run-history.js`:邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,支持清理记录,并按默认本地 helper 地址自动尝试把完整快照同步到本地 helper;若 helper 未启动,则静默跳过。
|
- `background/account-run-history.js`:邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,支持清理记录,并按默认本地 helper 地址自动尝试把完整快照同步到本地 helper;若 helper 未启动,则静默跳过。
|
||||||
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 `gmailBaseEmail`、`mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失别名基邮箱与 2925 切号上下文。
|
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 Plus 模式与 PayPal 配置、`gmailBaseEmail`、`mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失关键持久配置。
|
||||||
- `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。
|
- `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。
|
||||||
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。
|
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。
|
||||||
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。
|
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定,并会把 Step 2 的“手机号输入模式未切成功”与真正的 auth `add-phone` 页面区分开,避免自动运行误停机。
|
||||||
@@ -60,13 +60,17 @@
|
|||||||
|
|
||||||
- `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。
|
- `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。
|
||||||
- `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成。
|
- `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成。
|
||||||
|
- `background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 Plus checkout session,打开 `chatgpt.com/checkout/openai_ie/{checkout_session_id}` 短链,并记录 checkout 运行态。
|
||||||
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责登录验证码阶段的邮箱轮询、验证码回填与回退控制;验证码获取后直接提交,不再在提交前回放步骤 7 刷新 OAuth;对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱;当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。
|
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责登录验证码阶段的邮箱轮询、验证码回填与回退控制;验证码获取后直接提交,不再在提交前回放步骤 7 刷新 OAuth;对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱;当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。
|
||||||
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。
|
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录。
|
||||||
|
- `background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 checkout 页面选择 PayPal、生成账单姓名、读取本地地址 seed、触发地址推荐、提交订阅并等待跳转到 PayPal。
|
||||||
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。
|
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。
|
||||||
- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。
|
- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。
|
||||||
- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。
|
- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。
|
||||||
- `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
|
- `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
|
||||||
|
- `background/steps/paypal-approve.js`:Plus 模式第 8 步实现,负责驱动 PayPal 登录、关闭可见通行密钥提示、点击“同意并继续”,并等待授权流程离开 PayPal 页面。
|
||||||
- `background/steps/platform-verify.js`:步骤 10 实现,负责 CPA / SUB2API 回调验证,以及 Codex2API 的协议式 callback code/state 交换。
|
- `background/steps/platform-verify.js`:步骤 10 实现,负责 CPA / SUB2API 回调验证,以及 Codex2API 的协议式 callback code/state 交换。
|
||||||
|
- `background/steps/plus-return-confirm.js`:Plus 模式第 9 步实现,负责等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒再完成。
|
||||||
- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
|
- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
|
||||||
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与提交后落地页分支判断。
|
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责注册入口点击、邮箱提交与提交后落地页分支判断。
|
||||||
|
|
||||||
@@ -80,7 +84,9 @@
|
|||||||
- `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。
|
- `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。
|
||||||
- `content/mail-163.js`:163 / 163 VIP / 126 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。
|
- `content/mail-163.js`:163 / 163 VIP / 126 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。
|
||||||
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱自动登录态确认、收件轮询、按步骤会话隔离“已试验证码”、在每次重发验证码之间执行一轮最多 15 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理;若页面出现“子邮箱已达上限邮箱”提示,会立即上报后台进入切号链路;当后台显式开启 `mail2925MatchTargetEmail` 时,会对邮件里显式出现的目标邮箱做弱匹配,避免 `receive` 模式误捞别人的验证码。
|
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱自动登录态确认、收件轮询、按步骤会话隔离“已试验证码”、在每次重发验证码之间执行一轮最多 15 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理;若页面出现“子邮箱已达上限邮箱”提示,会立即上报后台进入切号链路;当后台显式开启 `mail2925MatchTargetEmail` 时,会对邮件里显式出现的目标邮箱做弱匹配,避免 `receive` 模式误捞别人的验证码。
|
||||||
|
- `content/paypal-flow.js`:PayPal 页面脚本,负责识别登录表单、填写 PayPal 账号密码、处理页面内可见通行密钥提示,并点击 PayPal 授权页的“同意并继续”按钮。
|
||||||
- `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。
|
- `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。
|
||||||
|
- `content/plus-checkout.js`:ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。
|
||||||
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
|
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
|
||||||
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 会在注册弹窗默认处于手机号输入模式时自动切回邮箱输入模式,并兼容本地化邮箱占位与 `aria-label`;登录链路还会显式识别 `phone-verification` 页面,避免把手机验证码页误判成邮箱验证码页或普通未知页。
|
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 会在注册弹窗默认处于手机号输入模式时自动切回邮箱输入模式,并兼容本地化邮箱占位与 `aria-label`;登录链路还会显式识别 `phone-verification` 页面,避免把手机验证码页误判成邮箱验证码页或普通未知页。
|
||||||
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;当前承接步骤 10。
|
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;当前承接步骤 10。
|
||||||
@@ -90,7 +96,8 @@
|
|||||||
## `data/`
|
## `data/`
|
||||||
|
|
||||||
- `data/names.js`:随机姓名、生日等测试数据源。
|
- `data/names.js`:随机姓名、生日等测试数据源。
|
||||||
- `data/step-definitions.js`:共享步骤元数据,前后台共同使用,用于动态渲染和步骤注册。
|
- `data/address-sources.js`:Plus 模式本地地址 seed 表,负责按国家选择用于触发 checkout 内置 Google 地址推荐的查询词和结构化地址 fallback;第一版不实时抓取外部地址网站。
|
||||||
|
- `data/step-definitions.js`:共享步骤元数据,前后台共同使用,用于动态渲染和步骤注册;当前同时提供普通 10 步定义与 Plus 模式 12 步定义。
|
||||||
|
|
||||||
## `docs/`
|
## `docs/`
|
||||||
|
|
||||||
@@ -125,13 +132,14 @@
|
|||||||
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
|
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
|
||||||
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行(包括 Codex2API 配置)的禁用与隐藏。
|
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行(包括 Codex2API 配置)的禁用与隐藏。
|
||||||
- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行与统一的收起态列表高度样式。
|
- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行与统一的收起态列表高度样式。
|
||||||
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“接码”开关与“验证码重发”拆成同一行展示;只有开启接码后,下面的 HeroSMS 平台、国家与 API Key 行才会显示;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免 receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱` 时额外显示 `自定义号池` 文本框;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密钥配置行;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
|
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“接码”开关与“验证码重发”拆成同一行展示;只有开启接码后,下面的 HeroSMS 平台、国家与 API Key 行才会显示;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免 receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱` 时额外显示 `自定义号池` 文本框;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密钥配置行;设置卡片新增 `Plus 模式` 开关与 PayPal 账号/密码输入行;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
|
||||||
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显示并同时服务于 provide / receive 两种模式;`自定义邮箱池` 模式会按邮箱池长度锁定自动轮数,并把当前输入的邮箱池配置参与自动启动前保存;`邮箱服务 = 自定义邮箱` 时,如果配置了 `customMailProviderPool`,也会按号池长度锁定自动轮数并在 Auto 中按轮次分配注册邮箱,同时在普通失败时继续复用当前邮箱,只有成功或出现手机号验证时才切换下一个邮箱;Step 8 的自定义邮箱确认弹窗当前额外提供“出现手机号验证”按钮,用于直接走与真实 add-phone 一致的 fatal 分支;HeroSMS 国家列表会在 sidepanel 初始化时拉取并恢复到本地保存的国家/API 设置,接码开关关闭时会直接隐藏相关配置;账号记录快照同步改为默认自动模式,只要本地 helper 可用就会自动落盘;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑;Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示,同时在首次初始化后按现有规则决定是否弹出新手引导提示。
|
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显示并同时服务于 provide / receive 两种模式;`自定义邮箱池` 模式会按邮箱池长度锁定自动轮数,并把当前输入的邮箱池配置参与自动启动前保存;`邮箱服务 = 自定义邮箱` 时,如果配置了 `customMailProviderPool`,也会按号池长度锁定自动轮数并在 Auto 中按轮次分配注册邮箱,同时在普通失败时继续复用当前邮箱,只有成功或出现手机号验证时才切换下一个邮箱;Plus 模式开启后,步骤列表会切换为 12 步,并显示 PayPal 凭据配置;Step 8 的自定义邮箱确认弹窗当前额外提供“出现手机号验证”按钮,用于直接走与真实 add-phone 一致的 fatal 分支;HeroSMS 国家列表会在 sidepanel 初始化时拉取并恢复到本地保存的国家/API 设置,接码开关关闭时会直接隐藏相关配置;账号记录快照同步改为默认自动模式,只要本地 helper 可用就会自动落盘;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑;Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示,同时在首次初始化后按现有规则决定是否弹出新手引导提示。
|
||||||
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。
|
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。
|
||||||
|
|
||||||
## `tests/`
|
## `tests/`
|
||||||
|
|
||||||
- `tests/activation-utils.test.js`:测试内容脚本激活策略与平台回调可恢复错误判断。
|
- `tests/activation-utils.test.js`:测试内容脚本激活策略与平台回调可恢复错误判断。
|
||||||
|
- `tests/address-sources.test.js`:测试 Plus 模式本地地址 seed 表的国家归一化、fallback 国家选择与基础地址字段。
|
||||||
- `tests/auth-page-recovery.test.js`:测试认证页共享恢复层的重试页识别与恢复行为。
|
- `tests/auth-page-recovery.test.js`:测试认证页共享恢复层的重试页识别与恢复行为。
|
||||||
- `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文,并补充 `gmailBaseEmail` / `mail2925BaseEmail` 在 fresh reset 后仍被保留的回归验证。
|
- `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文,并补充 `gmailBaseEmail` / `mail2925BaseEmail` 在 fresh reset 后仍被保留的回归验证。
|
||||||
- `tests/auto-run-step4-mail2925-thread-terminate.test.js`:测试步骤 4 命中 2925“结束当前尝试”错误时,不会再沿用当前邮箱回到步骤 1 重开,而是直接把错误抛给自动重试控制器。
|
- `tests/auto-run-step4-mail2925-thread-terminate.test.js`:测试步骤 4 命中 2925“结束当前尝试”错误时,不会再沿用当前邮箱回到步骤 1 重开,而是直接把错误抛给自动重试控制器。
|
||||||
|
|||||||
Reference in New Issue
Block a user