From a85ed0ce8994c59f19c1b71957cf6f0e105830bc Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Wed, 13 May 2026 04:46:56 +0800
Subject: [PATCH] feat: lay groundwork for multi-flow registries
---
background.js | 180 ++++++-
background/logging-status.js | 10 +
background/mail-rule-registry.js | 48 ++
background/message-router.js | 24 +-
background/navigation-utils.js | 7 +-
background/runtime-state.js | 484 ++++++++++++++++++
background/tab-runtime.js | 158 ++++--
background/verification-flow.js | 4 +
content/utils.js | 22 +-
flows/openai/mail-rules.js | 69 +++
manifest.json | 5 +
shared/flow-capabilities.js | 242 +++++++++
shared/source-registry.js | 433 ++++++++++++++++
sidepanel/sidepanel.html | 1 +
sidepanel/sidepanel.js | 458 +++++++++++++++--
tests/background-luckmail.test.js | 3 +
...ckground-mail-rule-registry-module.test.js | 93 ++++
.../background-message-router-module.test.js | 36 ++
...background-navigation-utils-module.test.js | 1 +
tests/background-runtime-state-module.test.js | 163 ++++++
.../background-signup-method-settings.test.js | 45 ++
tests/background-tab-runtime-module.test.js | 70 +++
tests/content-utils.test.js | 34 ++
tests/flow-capabilities-module.test.js | 72 +++
...epanel-phone-verification-settings.test.js | 52 ++
tests/sidepanel-plus-payment-method.test.js | 60 ++-
tests/source-registry-module.test.js | 56 ++
tests/verification-flow-polling.test.js | 37 ++
28 files changed, 2775 insertions(+), 92 deletions(-)
create mode 100644 background/mail-rule-registry.js
create mode 100644 background/runtime-state.js
create mode 100644 flows/openai/mail-rules.js
create mode 100644 shared/flow-capabilities.js
create mode 100644 shared/source-registry.js
create mode 100644 tests/background-mail-rule-registry-module.test.js
create mode 100644 tests/background-runtime-state-module.test.js
create mode 100644 tests/flow-capabilities-module.test.js
create mode 100644 tests/source-registry-module.test.js
diff --git a/background.js b/background.js
index b65b5d1..e6017a3 100644
--- a/background.js
+++ b/background.js
@@ -1,6 +1,8 @@
// background.js — Service Worker: orchestration, state, tab management, message routing
importScripts(
+ 'shared/source-registry.js',
+ 'shared/flow-capabilities.js',
'managed-alias-utils.js',
'mail2925-utils.js',
'paypal-utils.js',
@@ -17,8 +19,11 @@ importScripts(
'background/ip-proxy-core.js',
'background/panel-bridge.js',
'background/registration-email-state.js',
+ 'background/runtime-state.js',
'background/generated-email-helpers.js',
'background/signup-flow-helpers.js',
+ 'background/mail-rule-registry.js',
+ 'flows/openai/mail-rules.js',
'background/message-router.js',
'background/verification-flow.js',
'background/auto-run-controller.js',
@@ -80,6 +85,8 @@ const STEP_IDS = Array.from(new Set(ALL_STEP_DEFINITIONS
.map((definition) => Number(definition?.id))
.filter(Number.isFinite)))
.sort((left, right) => left - right);
+const DEFAULT_ACTIVE_FLOW_ID = 'openai';
+const DEFAULT_STEP_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending']));
const NORMAL_STEP_IDS = NORMAL_STEP_DEFINITIONS
.map((definition) => Number(definition?.id))
.filter(Number.isFinite)
@@ -205,6 +212,11 @@ const {
isRecoverableStep9AuthFailure,
} = self.MultiPageActivationUtils;
const registrationEmailStateHelpers = self.MultiPageRegistrationEmailState?.createRegistrationEmailStateHelpers?.() || null;
+const runtimeStateHelpers = self.MultiPageBackgroundRuntimeState?.createRuntimeStateHelpers?.({
+ DEFAULT_ACTIVE_FLOW_ID,
+ defaultStepStatuses: DEFAULT_STEP_STATUSES,
+ getStepDefinitionForState: (step, state) => getStepDefinitionForState(step, state),
+}) || null;
const DEFAULT_REGISTRATION_EMAIL_STATE = registrationEmailStateHelpers?.DEFAULT_REGISTRATION_EMAIL_STATE || {
current: '',
previous: '',
@@ -269,6 +281,20 @@ function getPreservedPhoneIdentity(state = {}) {
return null;
}
+function buildStateViewWithRuntimeState(state = {}) {
+ if (runtimeStateHelpers?.buildStateView) {
+ return runtimeStateHelpers.buildStateView(state);
+ }
+ return state;
+}
+
+function buildStatePatchWithRuntimeState(currentState = {}, updates = {}) {
+ if (runtimeStateHelpers?.buildSessionStatePatch) {
+ return runtimeStateHelpers.buildSessionStatePatch(currentState, updates);
+ }
+ return updates;
+}
+
function statePatchHasChanges(state = {}, patch = {}) {
return Object.keys(patch).some((key) => JSON.stringify(state?.[key] ?? null) !== JSON.stringify(patch[key] ?? null));
}
@@ -844,8 +870,13 @@ const SETTINGS_EXPORT_FILENAME_PREFIX = 'multipage-settings';
const STEP6_REGISTRATION_SUCCESS_WAIT_MS = 20000;
const DEFAULT_STATE = {
+ activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ activeRunId: '',
+ currentNodeId: '',
+ nodeStatuses: {},
currentStep: 0, // 当前流程执行到的步骤编号。
- stepStatuses: Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending'])),
+ stepStatuses: { ...DEFAULT_STEP_STATUSES },
+ runtimeState: runtimeStateHelpers?.buildDefaultRuntimeState?.() || null,
...CONTRIBUTION_RUNTIME_DEFAULTS,
oauthUrl: null, // 运行时抓取到的 OAuth 地址,不要手动预填。
resolvedSignupMethod: null, // 当前自动轮次冻结后的实际注册方式。
@@ -1320,7 +1351,49 @@ function normalizeSignupMethod(value = '') {
: 'email';
}
+function getFlowCapabilityRegistry() {
+ const rootScope = typeof self !== 'undefined' ? self : globalThis;
+ if (typeof flowCapabilityRegistry !== 'undefined' && flowCapabilityRegistry) {
+ return flowCapabilityRegistry;
+ }
+ return rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai',
+ }) || null;
+}
+
+function resolveCurrentFlowCapabilities(state = {}, options = {}) {
+ const registry = getFlowCapabilityRegistry();
+ if (!registry?.resolveSidepanelCapabilities) {
+ return null;
+ }
+ return registry.resolveSidepanelCapabilities({
+ activeFlowId: options?.activeFlowId ?? state?.activeFlowId,
+ panelMode: options?.panelMode ?? state?.panelMode,
+ signupMethod: options?.signupMethod ?? state?.signupMethod,
+ state,
+ });
+}
+
function canUsePhoneSignup(state = {}) {
+ const capabilityState = typeof resolveCurrentFlowCapabilities === 'function'
+ ? resolveCurrentFlowCapabilities(state)
+ : (() => {
+ const rootScope = typeof self !== 'undefined' ? self : globalThis;
+ const registry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai',
+ }) || null;
+ return registry?.resolveSidepanelCapabilities
+ ? registry.resolveSidepanelCapabilities({
+ activeFlowId: state?.activeFlowId,
+ panelMode: state?.panelMode,
+ signupMethod: state?.signupMethod,
+ state,
+ })
+ : null;
+ })();
+ if (capabilityState && typeof capabilityState.canUsePhoneSignup === 'boolean') {
+ return capabilityState.canUsePhoneSignup;
+ }
return Boolean(state?.phoneVerificationEnabled)
&& !Boolean(state?.plusModeEnabled)
&& !Boolean(state?.contributionMode);
@@ -1332,9 +1405,26 @@ function resolveSignupMethod(state = {}) {
return normalizeSignupMethod(frozenMethod);
}
const method = normalizeSignupMethod(state?.signupMethod);
- return method === SIGNUP_METHOD_PHONE && canUsePhoneSignup(state)
- ? SIGNUP_METHOD_PHONE
- : SIGNUP_METHOD_EMAIL;
+ const capabilityState = typeof resolveCurrentFlowCapabilities === 'function'
+ ? resolveCurrentFlowCapabilities(state, { signupMethod: method })
+ : (() => {
+ const rootScope = typeof self !== 'undefined' ? self : globalThis;
+ const registry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai',
+ }) || null;
+ return registry?.resolveSidepanelCapabilities
+ ? registry.resolveSidepanelCapabilities({
+ activeFlowId: state?.activeFlowId,
+ panelMode: state?.panelMode,
+ signupMethod: method,
+ state,
+ })
+ : null;
+ })();
+ if (capabilityState?.effectiveSignupMethod) {
+ return normalizeSignupMethod(capabilityState.effectiveSignupMethod);
+ }
+ return method === SIGNUP_METHOD_PHONE && canUsePhoneSignup(state) ? SIGNUP_METHOD_PHONE : SIGNUP_METHOD_EMAIL;
}
async function ensureResolvedSignupMethodForRun(options = {}) {
@@ -2762,7 +2852,9 @@ function buildPersistentSettingsPayload(input = {}, options = {}) {
};
if (Object.prototype.hasOwnProperty.call(payload, 'phoneVerificationEnabled')
|| Object.prototype.hasOwnProperty.call(payload, 'plusModeEnabled')
- || Object.prototype.hasOwnProperty.call(payload, 'signupMethod')) {
+ || Object.prototype.hasOwnProperty.call(payload, 'signupMethod')
+ || Object.prototype.hasOwnProperty.call(payload, 'panelMode')
+ || Object.prototype.hasOwnProperty.call(payload, 'activeFlowId')) {
payload.signupMethod = resolveSignupMethod(nextSignupConstraintState);
}
if (payload.ipProxyServiceProfiles) {
@@ -2838,7 +2930,13 @@ async function getState() {
getPersistedAliasState(),
accountRunHistoryHelpers?.getPersistedAccountRunHistory?.() || [],
]);
- return { ...DEFAULT_STATE, ...persistedSettings, ...persistedAliasState, ...state, accountRunHistory };
+ return buildStateViewWithRuntimeState({
+ ...DEFAULT_STATE,
+ ...persistedSettings,
+ ...persistedAliasState,
+ ...state,
+ accountRunHistory,
+ });
}
async function initializeSessionStorageAccess() {
@@ -2857,19 +2955,24 @@ async function initializeSessionStorageAccess() {
async function setState(updates) {
console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
if (Object.keys(updates || {}).length > 0) {
- await chrome.storage.session.set(updates);
+ const currentSessionState = await chrome.storage.session.get(null);
+ const sessionUpdates = buildStatePatchWithRuntimeState({
+ ...DEFAULT_STATE,
+ ...currentSessionState,
+ }, updates);
+ await chrome.storage.session.set(sessionUpdates);
const persistentAliasUpdates = {};
- if (Object.prototype.hasOwnProperty.call(updates, 'manualAliasUsage')) {
- persistentAliasUpdates.manualAliasUsage = normalizeBooleanMap(updates.manualAliasUsage);
+ if (Object.prototype.hasOwnProperty.call(sessionUpdates, 'manualAliasUsage')) {
+ persistentAliasUpdates.manualAliasUsage = normalizeBooleanMap(sessionUpdates.manualAliasUsage);
}
- if (Object.prototype.hasOwnProperty.call(updates, 'preservedAliases')) {
- persistentAliasUpdates.preservedAliases = normalizeBooleanMap(updates.preservedAliases);
+ if (Object.prototype.hasOwnProperty.call(sessionUpdates, 'preservedAliases')) {
+ persistentAliasUpdates.preservedAliases = normalizeBooleanMap(sessionUpdates.preservedAliases);
}
- if (Object.prototype.hasOwnProperty.call(updates, 'icloudAliasCache')) {
- persistentAliasUpdates.icloudAliasCache = normalizeIcloudAliasCacheList(updates.icloudAliasCache);
+ if (Object.prototype.hasOwnProperty.call(sessionUpdates, 'icloudAliasCache')) {
+ persistentAliasUpdates.icloudAliasCache = normalizeIcloudAliasCacheList(sessionUpdates.icloudAliasCache);
}
- if (Object.prototype.hasOwnProperty.call(updates, 'icloudAliasCacheAt')) {
- persistentAliasUpdates.icloudAliasCacheAt = Math.max(0, Number(updates.icloudAliasCacheAt) || 0);
+ if (Object.prototype.hasOwnProperty.call(sessionUpdates, 'icloudAliasCacheAt')) {
+ persistentAliasUpdates.icloudAliasCacheAt = Math.max(0, Number(sessionUpdates.icloudAliasCacheAt) || 0);
}
if (Object.keys(persistentAliasUpdates).length > 0) {
await chrome.storage.local.set(persistentAliasUpdates);
@@ -3455,7 +3558,7 @@ async function resetState() {
? prev.freeReusablePhoneActivation
: null;
await chrome.storage.session.clear();
- await chrome.storage.session.set({
+ const resetPayload = buildStatePatchWithRuntimeState({}, {
...DEFAULT_STATE,
...persistedSettings,
...persistedAliasState,
@@ -3485,6 +3588,7 @@ async function resetState() {
? Number(prev.automationWindowId)
: null,
});
+ await chrome.storage.session.set(resetPayload);
}
/**
@@ -7210,7 +7314,7 @@ function isSignupEntryHost(hostname = '') {
if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupEntryHost) {
return navigationUtils.isSignupEntryHost(hostname);
}
- return ['chatgpt.com', 'chat.openai.com'].includes(hostname);
+ return ['chatgpt.com', 'www.chatgpt.com', 'chat.openai.com'].includes(hostname);
}
function isLikelyLoggedInChatgptHomeUrl(rawUrl) {
@@ -7294,6 +7398,7 @@ function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
if (!candidate) return false;
const reference = parseUrlSafely(referenceUrl);
switch (source) {
+ case 'openai-auth':
case 'signup-page':
return isSignupPageHost(candidate.hostname) || isSignupEntryHost(candidate.hostname);
case 'duck-mail':
@@ -7304,6 +7409,8 @@ function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
return is163MailHost(candidate.hostname);
case 'gmail-mail':
return candidate.hostname === 'mail.google.com';
+ case 'icloud-mail':
+ return candidate.hostname === 'www.icloud.com' || candidate.hostname === 'www.icloud.com.cn';
case 'inbucket-mail':
return Boolean(reference) && candidate.origin === reference.origin && candidate.pathname.startsWith('/m/');
case 'mail-2925':
@@ -7314,11 +7421,24 @@ function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
return Boolean(reference)
&& candidate.origin === reference.origin
&& (candidate.pathname.startsWith('/admin/accounts') || candidate.pathname.startsWith('/login') || candidate.pathname === '/');
+ case 'codex2api-panel':
+ return Boolean(reference)
+ && candidate.origin === reference.origin
+ && (candidate.pathname.startsWith('/admin/accounts') || candidate.pathname === '/admin' || candidate.pathname === '/');
default:
return false;
}
}
+function sourcesMatch(leftSource, rightSource) {
+ if (sourceRegistry?.resolveCanonicalSource) {
+ const left = sourceRegistry.resolveCanonicalSource(leftSource);
+ const right = sourceRegistry.resolveCanonicalSource(rightSource);
+ return Boolean(left && right && left === right);
+ }
+ return String(leftSource || '').trim() === String(rightSource || '').trim();
+}
+
async function rememberSourceLastUrl(source, url) {
return tabRuntime.rememberSourceLastUrl(source, url);
}
@@ -7399,7 +7519,7 @@ async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
function isContentScriptReadyPong(source, pong) {
if (!pong?.ok) return false;
- if (pong.source && pong.source !== source) return false;
+ if (pong.source && !sourcesMatch(pong.source, source)) return false;
if (source === 'plus-checkout') {
return Boolean(pong.plusCheckoutReady);
}
@@ -7590,11 +7710,13 @@ function getSourceLabel(source) {
return loggingStatus.getSourceLabel(source);
}
const labels = {
+ 'openai-auth': '认证页',
'gmail-mail': 'Gmail 邮箱',
'sidepanel': '侧边栏',
'signup-page': '认证页',
'vps-panel': 'CPA 面板',
'sub2api-panel': 'SUB2API 后台',
+ 'codex2api-panel': 'Codex2API 后台',
'qq-mail': 'QQ 邮箱',
'mail-163': '163 邮箱',
'mail-2925': '2925 邮箱',
@@ -7606,6 +7728,8 @@ function getSourceLabel(source) {
'cloudmail': 'Cloud Mail',
'plus-checkout': 'Plus Checkout',
'paypal-flow': 'PayPal 授权页',
+ 'gopay-flow': 'GoPay 授权页',
+ 'unknown-source': '未知来源',
};
return labels[source] || source || '未知来源';
}
@@ -7659,10 +7783,16 @@ function getStepFetchNetworkRetryPolicy(step) {
};
}
+const sourceRegistry = self.MultiPageSourceRegistry?.createSourceRegistry?.() || null;
+const flowCapabilityRegistry = self.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: DEFAULT_ACTIVE_FLOW_ID,
+}) || null;
+
const navigationUtils = self.MultiPageBackgroundNavigationUtils?.createNavigationUtils({
DEFAULT_CODEX2API_URL,
DEFAULT_SUB2API_URL,
normalizeLocalCpaStep9Mode,
+ sourceRegistry,
});
const loggingStatus = self.MultiPageBackgroundLoggingStatus?.createLoggingStatus({
@@ -7672,6 +7802,7 @@ const loggingStatus = self.MultiPageBackgroundLoggingStatus?.createLoggingStatus
isRecoverableStep9AuthFailure,
LOG_PREFIX,
setState,
+ sourceRegistry,
STOP_ERROR_MESSAGE,
});
@@ -7684,6 +7815,7 @@ const tabRuntime = self.MultiPageBackgroundTabRuntime?.createTabRuntime({
isRetryableContentScriptTransportError,
LOG_PREFIX,
matchesSourceUrlFamily,
+ sourceRegistry,
setState,
sleepWithStop,
STOP_ERROR_MESSAGE,
@@ -11251,8 +11383,20 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
waitForTabStableComplete,
waitForTabUrlMatch,
});
+const openAiMailRules = self.MultiPageOpenAiMailRules?.createOpenAiMailRules({
+ getHotmailVerificationRequestTimestamp,
+ MAIL_2925_VERIFICATION_INTERVAL_MS,
+ MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
+});
+const mailRuleRegistry = self.MultiPageBackgroundMailRuleRegistry?.createMailRuleRegistry({
+ defaultFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ flowBuilders: {
+ openai: openAiMailRules,
+ },
+});
const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.createVerificationFlowHelpers({
addLog,
+ buildVerificationPollPayload: mailRuleRegistry?.buildVerificationPollPayload,
chrome,
closeConflictingTabsForSource,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
diff --git a/background/logging-status.js b/background/logging-status.js
index b86afa8..f92cd39 100644
--- a/background/logging-status.js
+++ b/background/logging-status.js
@@ -9,16 +9,22 @@
isRecoverableStep9AuthFailure,
LOG_PREFIX,
setState,
+ sourceRegistry = null,
STOP_ERROR_MESSAGE,
} = deps;
function getSourceLabel(source) {
+ if (sourceRegistry?.getSourceLabel) {
+ return sourceRegistry.getSourceLabel(source);
+ }
const labels = {
+ 'openai-auth': '认证页',
'gmail-mail': 'Gmail 邮箱',
'sidepanel': '侧边栏',
'signup-page': '认证页',
'vps-panel': 'CPA 面板',
'sub2api-panel': 'SUB2API 后台',
+ 'codex2api-panel': 'Codex2API 后台',
'qq-mail': 'QQ 邮箱',
'mail-163': '163 邮箱',
'mail-2925': '2925 邮箱',
@@ -28,6 +34,10 @@
'luckmail-api': 'LuckMail(API 购邮)',
'cloudflare-temp-email': 'Cloudflare Temp Email',
'cloudmail': 'Cloud Mail',
+ 'plus-checkout': 'Plus Checkout',
+ 'paypal-flow': 'PayPal 授权页',
+ 'gopay-flow': 'GoPay 授权页',
+ 'unknown-source': '未知来源',
};
return labels[source] || source || '未知来源';
}
diff --git a/background/mail-rule-registry.js b/background/mail-rule-registry.js
new file mode 100644
index 0000000..be9848f
--- /dev/null
+++ b/background/mail-rule-registry.js
@@ -0,0 +1,48 @@
+(function attachBackgroundMailRuleRegistry(root, factory) {
+ root.MultiPageBackgroundMailRuleRegistry = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundMailRuleRegistryModule() {
+ function createMailRuleRegistry(deps = {}) {
+ const {
+ defaultFlowId = 'openai',
+ flowBuilders = {},
+ } = deps;
+
+ function resolveFlowId(state = {}) {
+ const activeFlowId = String(state?.activeFlowId || '').trim();
+ return activeFlowId || String(defaultFlowId || '').trim() || 'openai';
+ }
+
+ function getFlowBuilder(flowId) {
+ const normalizedFlowId = String(flowId || '').trim();
+ return normalizedFlowId ? flowBuilders[normalizedFlowId] || null : null;
+ }
+
+ function getVerificationMailRule(step, state = {}) {
+ const flowId = resolveFlowId(state);
+ const flowBuilder = getFlowBuilder(flowId);
+ if (!flowBuilder || typeof flowBuilder.getRuleDefinition !== 'function') {
+ throw new Error(`未找到 flow=${flowId} 的邮件规则定义。`);
+ }
+ return flowBuilder.getRuleDefinition(step, state);
+ }
+
+ function buildVerificationPollPayload(step, state = {}, overrides = {}) {
+ const flowId = resolveFlowId(state);
+ const flowBuilder = getFlowBuilder(flowId);
+ if (!flowBuilder || typeof flowBuilder.buildVerificationPollPayload !== 'function') {
+ throw new Error(`未找到 flow=${flowId} 的邮件轮询规则构造器。`);
+ }
+ return flowBuilder.buildVerificationPollPayload(step, state, overrides);
+ }
+
+ return {
+ buildVerificationPollPayload,
+ getVerificationMailRule,
+ resolveFlowId,
+ };
+ }
+
+ return {
+ createMailRuleRegistry,
+ };
+});
diff --git a/background/message-router.js b/background/message-router.js
index f3127a0..25ed7b4 100644
--- a/background/message-router.js
+++ b/background/message-router.js
@@ -51,11 +51,27 @@
getStepIdsForState,
getLastStepIdForState,
normalizeSignupMethod = (value = '') => String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email',
- canUsePhoneSignup = (state = {}) => Boolean(state?.phoneVerificationEnabled)
- && !Boolean(state?.plusModeEnabled)
- && !Boolean(state?.contributionMode),
+ canUsePhoneSignup = (state = {}) => {
+ const rootScope = typeof self !== 'undefined' ? self : globalThis;
+ const capabilityRegistry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: 'openai',
+ }) || null;
+ if (capabilityRegistry?.canUsePhoneSignup) {
+ return capabilityRegistry.canUsePhoneSignup(state);
+ }
+ return Boolean(state?.phoneVerificationEnabled)
+ && !Boolean(state?.plusModeEnabled)
+ && !Boolean(state?.contributionMode);
+ },
resolveSignupMethod = (state = {}) => {
const method = normalizeSignupMethod(state?.signupMethod);
+ const rootScope = typeof self !== 'undefined' ? self : globalThis;
+ const capabilityRegistry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: 'openai',
+ }) || null;
+ if (capabilityRegistry?.resolveSignupMethod) {
+ return capabilityRegistry.resolveSignupMethod(state, method);
+ }
return method === 'phone' && canUsePhoneSignup(state) ? 'phone' : 'email';
},
getTabId,
@@ -999,6 +1015,8 @@
Object.prototype.hasOwnProperty.call(updates, 'phoneVerificationEnabled')
|| Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
|| Object.prototype.hasOwnProperty.call(updates, 'signupMethod')
+ || Object.prototype.hasOwnProperty.call(updates, 'panelMode')
+ || Object.prototype.hasOwnProperty.call(updates, 'activeFlowId')
) {
updates.signupMethod = resolveSignupMethod(nextSignupState);
}
diff --git a/background/navigation-utils.js b/background/navigation-utils.js
index 9077502..2353973 100644
--- a/background/navigation-utils.js
+++ b/background/navigation-utils.js
@@ -6,6 +6,7 @@
DEFAULT_CODEX2API_URL,
DEFAULT_SUB2API_URL,
normalizeLocalCpaStep9Mode,
+ sourceRegistry = null,
} = deps;
function parseUrlSafely(rawUrl) {
@@ -66,7 +67,7 @@
}
function isSignupEntryHost(hostname = '') {
- return ['chatgpt.com', 'chat.openai.com'].includes(hostname);
+ return ['chatgpt.com', 'www.chatgpt.com', 'chat.openai.com'].includes(hostname);
}
function isSignupPasswordPageUrl(rawUrl) {
@@ -117,12 +118,16 @@
}
function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
+ if (sourceRegistry?.matchesSourceUrlFamily) {
+ return sourceRegistry.matchesSourceUrlFamily(source, candidateUrl, referenceUrl);
+ }
const candidate = parseUrlSafely(candidateUrl);
if (!candidate) return false;
const reference = parseUrlSafely(referenceUrl);
switch (source) {
+ case 'openai-auth':
case 'signup-page':
return isSignupPageHost(candidate.hostname) || isSignupEntryHost(candidate.hostname);
case 'duck-mail':
diff --git a/background/runtime-state.js b/background/runtime-state.js
new file mode 100644
index 0000000..cfacddd
--- /dev/null
+++ b/background/runtime-state.js
@@ -0,0 +1,484 @@
+(function attachBackgroundRuntimeState(root, factory) {
+ root.MultiPageBackgroundRuntimeState = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundRuntimeStateModule() {
+ function createRuntimeStateHelpers(deps = {}) {
+ const {
+ DEFAULT_ACTIVE_FLOW_ID = 'openai',
+ defaultStepStatuses = {},
+ getStepDefinitionForState = null,
+ } = deps;
+
+ const RUNTIME_SHARED_FIELDS = Object.freeze([
+ 'automationWindowId',
+ 'tabRegistry',
+ 'sourceLastUrls',
+ 'flowStartTime',
+ ]);
+ const RUNTIME_PROXY_FIELDS = Object.freeze([
+ 'ipProxyApiPool',
+ 'ipProxyApiCurrentIndex',
+ 'ipProxyApiCurrent',
+ 'ipProxyAccountPool',
+ 'ipProxyAccountCurrentIndex',
+ 'ipProxyAccountCurrent',
+ 'ipProxyPool',
+ 'ipProxyCurrentIndex',
+ 'ipProxyCurrent',
+ ]);
+ const OPENAI_FLOW_FIELD_GROUPS = Object.freeze({
+ auth: Object.freeze([
+ 'oauthUrl',
+ 'localhostUrl',
+ ]),
+ platformBinding: Object.freeze([
+ 'cpaOAuthState',
+ 'cpaManagementOrigin',
+ 'sub2apiSessionId',
+ 'sub2apiOAuthState',
+ 'sub2apiGroupId',
+ 'sub2apiDraftName',
+ 'sub2apiProxyId',
+ 'sub2apiGroupIds',
+ 'codex2apiSessionId',
+ 'codex2apiOAuthState',
+ ]),
+ plus: Object.freeze([
+ 'plusCheckoutTabId',
+ 'plusCheckoutUrl',
+ 'plusCheckoutCountry',
+ 'plusCheckoutCurrency',
+ 'plusCheckoutSource',
+ 'plusBillingCountryText',
+ 'plusBillingAddress',
+ 'plusPaypalApprovedAt',
+ 'plusGoPayApprovedAt',
+ 'plusReturnUrl',
+ 'plusManualConfirmationPending',
+ 'plusManualConfirmationRequestId',
+ 'plusManualConfirmationStep',
+ 'plusManualConfirmationMethod',
+ 'plusManualConfirmationTitle',
+ 'plusManualConfirmationMessage',
+ 'gopayHelperReferenceId',
+ 'gopayHelperGoPayGuid',
+ 'gopayHelperRedirectUrl',
+ 'gopayHelperNextAction',
+ 'gopayHelperFlowId',
+ 'gopayHelperChallengeId',
+ 'gopayHelperStartPayload',
+ 'gopayHelperTaskId',
+ 'gopayHelperTaskStatus',
+ 'gopayHelperStatusText',
+ 'gopayHelperRemoteStage',
+ 'gopayHelperApiWaitingFor',
+ 'gopayHelperApiInputDeadlineAt',
+ 'gopayHelperApiInputWaitSeconds',
+ 'gopayHelperLastInputError',
+ 'gopayHelperOtpInvalidCount',
+ 'gopayHelperFailureStage',
+ 'gopayHelperFailureDetail',
+ 'gopayHelperTaskPayload',
+ 'gopayHelperOrderCreatedAt',
+ 'gopayHelperTaskProgressSignature',
+ 'gopayHelperTaskProgressAt',
+ 'gopayHelperTaskProgressTaskId',
+ 'gopayHelperPinPayload',
+ 'gopayHelperResolvedOtp',
+ 'gopayHelperOtpRequestId',
+ 'gopayHelperOtpReferenceId',
+ ]),
+ phoneVerification: Object.freeze([
+ 'currentPhoneActivation',
+ 'phoneNumber',
+ 'currentPhoneVerificationCode',
+ 'currentPhoneVerificationCountdownEndsAt',
+ 'currentPhoneVerificationCountdownWindowIndex',
+ 'currentPhoneVerificationCountdownWindowTotal',
+ 'reusablePhoneActivation',
+ 'freeReusablePhoneActivation',
+ 'phoneReusableActivationPool',
+ 'signupPhoneNumber',
+ 'signupPhoneActivation',
+ 'signupPhoneCompletedActivation',
+ 'signupPhoneVerificationRequestedAt',
+ 'signupPhoneVerificationPurpose',
+ ]),
+ luckmail: Object.freeze([
+ 'currentLuckmailPurchase',
+ 'currentLuckmailMailCursor',
+ ]),
+ identity: Object.freeze([
+ 'resolvedSignupMethod',
+ 'accountIdentifierType',
+ 'accountIdentifier',
+ 'registrationEmailState',
+ 'email',
+ 'password',
+ 'lastEmailTimestamp',
+ 'lastSignupCode',
+ 'lastLoginCode',
+ 'step8VerificationTargetEmail',
+ ]),
+ });
+
+ function isPlainObject(value) {
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
+ }
+
+ function cloneValue(value) {
+ if (Array.isArray(value)) {
+ return value.map((item) => cloneValue(item));
+ }
+ if (isPlainObject(value)) {
+ return Object.fromEntries(
+ Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
+ );
+ }
+ return value;
+ }
+
+ function normalizePlainObject(value) {
+ return isPlainObject(value) ? value : {};
+ }
+
+ function normalizeFlowId(value = '') {
+ const normalized = String(value || '').trim().toLowerCase();
+ return normalized || DEFAULT_ACTIVE_FLOW_ID;
+ }
+
+ function normalizeRunId(value = '') {
+ return String(value || '').trim();
+ }
+
+ function normalizeStepNumber(value) {
+ const step = Math.floor(Number(value) || 0);
+ return step > 0 ? step : 0;
+ }
+
+ function normalizeNodeStatus(value = '') {
+ const normalized = String(value || '').trim().toLowerCase();
+ if (!normalized) {
+ return 'pending';
+ }
+ return normalized;
+ }
+
+ function buildDefaultStepStatuses() {
+ return Object.fromEntries(
+ Object.entries(normalizePlainObject(defaultStepStatuses)).map(([key, value]) => [
+ String(key),
+ normalizeNodeStatus(value),
+ ])
+ );
+ }
+
+ function normalizeStepStatuses(value) {
+ const base = buildDefaultStepStatuses();
+ if (!isPlainObject(value)) {
+ return base;
+ }
+
+ const next = { ...base };
+ for (const [key, status] of Object.entries(value)) {
+ const step = normalizeStepNumber(key);
+ if (!step) continue;
+ next[String(step)] = normalizeNodeStatus(status);
+ }
+ return next;
+ }
+
+ function normalizeLegacyStepCompat(value = {}, state = {}) {
+ const candidate = normalizePlainObject(value);
+ const currentStep = normalizeStepNumber(
+ Object.prototype.hasOwnProperty.call(state, 'currentStep')
+ ? state.currentStep
+ : candidate.currentStep
+ );
+ const stepStatuses = normalizeStepStatuses(
+ Object.prototype.hasOwnProperty.call(state, 'stepStatuses')
+ ? state.stepStatuses
+ : candidate.stepStatuses
+ );
+
+ return {
+ currentStep,
+ stepStatuses,
+ };
+ }
+
+ function resolveStepKey(step, state = {}) {
+ const numericStep = normalizeStepNumber(step);
+ if (!numericStep || typeof getStepDefinitionForState !== 'function') {
+ return '';
+ }
+ return String(getStepDefinitionForState(numericStep, state)?.key || '').trim();
+ }
+
+ function normalizeNodeStatuses(value, state = {}, legacyStepCompat = null) {
+ if (isPlainObject(value) && Object.keys(value).length > 0) {
+ return Object.fromEntries(
+ Object.entries(value)
+ .map(([key, status]) => [String(key || '').trim(), normalizeNodeStatus(status)])
+ .filter(([key]) => Boolean(key))
+ );
+ }
+
+ const compat = legacyStepCompat || normalizeLegacyStepCompat({}, state);
+ const next = {};
+ for (const [stepKey, status] of Object.entries(compat.stepStatuses || {})) {
+ const step = normalizeStepNumber(stepKey);
+ if (!step) continue;
+ const nodeKey = resolveStepKey(step, state) || `step-${step}`;
+ next[nodeKey] = normalizeNodeStatus(status);
+ }
+ return next;
+ }
+
+ function pickDefinedFields(state = {}, fields = []) {
+ const next = {};
+ for (const field of fields) {
+ if (Object.prototype.hasOwnProperty.call(state, field)) {
+ next[field] = cloneValue(state[field]);
+ }
+ }
+ return next;
+ }
+
+ function buildSharedState(baseValue = {}, state = {}) {
+ return {
+ ...cloneValue(normalizePlainObject(baseValue)),
+ ...pickDefinedFields(state, RUNTIME_SHARED_FIELDS),
+ };
+ }
+
+ function buildServiceState(baseValue = {}, state = {}) {
+ const base = cloneValue(normalizePlainObject(baseValue));
+ return {
+ ...base,
+ proxy: {
+ ...cloneValue(normalizePlainObject(base.proxy)),
+ ...pickDefinedFields(state, RUNTIME_PROXY_FIELDS),
+ },
+ };
+ }
+
+ function flattenOpenAiFlowState(flowState = {}) {
+ const openaiState = normalizePlainObject(flowState.openai);
+ const next = {};
+ for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) {
+ const group = normalizePlainObject(openaiState[groupKey]);
+ for (const field of fields) {
+ if (Object.prototype.hasOwnProperty.call(group, field)) {
+ next[field] = cloneValue(group[field]);
+ }
+ }
+ }
+ return next;
+ }
+
+ function buildOpenAiFlowState(baseValue = {}, state = {}) {
+ const baseFlowState = cloneValue(normalizePlainObject(baseValue));
+ const baseOpenAi = cloneValue(normalizePlainObject(baseFlowState.openai));
+ const openaiState = {
+ ...baseOpenAi,
+ };
+
+ for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) {
+ openaiState[groupKey] = {
+ ...cloneValue(normalizePlainObject(baseOpenAi[groupKey])),
+ ...pickDefinedFields(state, fields),
+ };
+ }
+
+ return {
+ ...baseFlowState,
+ openai: openaiState,
+ };
+ }
+
+ function buildRuntimeStateDefault() {
+ return {
+ activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ activeRunId: '',
+ currentNodeId: '',
+ nodeStatuses: {},
+ sharedState: {},
+ serviceState: {
+ proxy: {},
+ },
+ flowState: {
+ openai: {
+ auth: {},
+ platformBinding: {},
+ plus: {},
+ phoneVerification: {},
+ luckmail: {},
+ identity: {},
+ },
+ },
+ legacyStepCompat: {
+ currentStep: 0,
+ stepStatuses: buildDefaultStepStatuses(),
+ },
+ };
+ }
+
+ function ensureRuntimeState(state = {}) {
+ const baseRuntimeState = {
+ ...buildRuntimeStateDefault(),
+ ...cloneValue(normalizePlainObject(state.runtimeState)),
+ };
+ const activeFlowId = normalizeFlowId(
+ Object.prototype.hasOwnProperty.call(state, 'activeFlowId')
+ ? state.activeFlowId
+ : baseRuntimeState.activeFlowId
+ );
+ const legacyStepCompat = normalizeLegacyStepCompat(baseRuntimeState.legacyStepCompat, state);
+ const currentNodeId = String(
+ Object.prototype.hasOwnProperty.call(state, 'currentNodeId')
+ ? state.currentNodeId
+ : (baseRuntimeState.currentNodeId || resolveStepKey(legacyStepCompat.currentStep, state))
+ ).trim();
+ const nodeStatuses = normalizeNodeStatuses(
+ Object.prototype.hasOwnProperty.call(state, 'nodeStatuses')
+ ? state.nodeStatuses
+ : baseRuntimeState.nodeStatuses,
+ state,
+ legacyStepCompat
+ );
+
+ return {
+ ...baseRuntimeState,
+ activeFlowId,
+ activeRunId: normalizeRunId(
+ Object.prototype.hasOwnProperty.call(state, 'activeRunId')
+ ? state.activeRunId
+ : baseRuntimeState.activeRunId
+ ),
+ currentNodeId,
+ nodeStatuses,
+ sharedState: buildSharedState(baseRuntimeState.sharedState, state),
+ serviceState: buildServiceState(baseRuntimeState.serviceState, state),
+ flowState: buildOpenAiFlowState(baseRuntimeState.flowState, state),
+ legacyStepCompat,
+ };
+ }
+
+ function buildFlattenedUpdates(updates = {}) {
+ const next = {
+ ...updates,
+ };
+ const runtimeState = normalizePlainObject(updates.runtimeState);
+ const legacyStepCompat = normalizePlainObject(updates.legacyStepCompat);
+ const sharedState = normalizePlainObject(updates.sharedState);
+ const serviceState = normalizePlainObject(updates.serviceState);
+ const flowState = normalizePlainObject(updates.flowState);
+
+ if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeFlowId')) {
+ next.activeFlowId = runtimeState.activeFlowId;
+ }
+ if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeRunId')) {
+ next.activeRunId = runtimeState.activeRunId;
+ }
+ if (Object.prototype.hasOwnProperty.call(runtimeState, 'currentNodeId')) {
+ next.currentNodeId = runtimeState.currentNodeId;
+ }
+ if (Object.prototype.hasOwnProperty.call(runtimeState, 'nodeStatuses')) {
+ next.nodeStatuses = cloneValue(runtimeState.nodeStatuses);
+ }
+ if (Object.prototype.hasOwnProperty.call(legacyStepCompat, 'currentStep')) {
+ next.currentStep = legacyStepCompat.currentStep;
+ }
+ if (Object.prototype.hasOwnProperty.call(legacyStepCompat, 'stepStatuses')) {
+ next.stepStatuses = cloneValue(legacyStepCompat.stepStatuses);
+ }
+ if (Object.prototype.hasOwnProperty.call(runtimeState, 'legacyStepCompat')) {
+ const compatCandidate = normalizePlainObject(runtimeState.legacyStepCompat);
+ if (Object.prototype.hasOwnProperty.call(compatCandidate, 'currentStep')) {
+ next.currentStep = compatCandidate.currentStep;
+ }
+ if (Object.prototype.hasOwnProperty.call(compatCandidate, 'stepStatuses')) {
+ next.stepStatuses = cloneValue(compatCandidate.stepStatuses);
+ }
+ }
+
+ Object.assign(next, pickDefinedFields(sharedState, RUNTIME_SHARED_FIELDS));
+ if (Object.prototype.hasOwnProperty.call(runtimeState, 'sharedState')) {
+ Object.assign(
+ next,
+ pickDefinedFields(normalizePlainObject(runtimeState.sharedState), RUNTIME_SHARED_FIELDS)
+ );
+ }
+
+ const serviceProxy = normalizePlainObject(serviceState.proxy);
+ Object.assign(next, pickDefinedFields(serviceProxy, RUNTIME_PROXY_FIELDS));
+ if (Object.prototype.hasOwnProperty.call(runtimeState, 'serviceState')) {
+ const runtimeServiceState = normalizePlainObject(runtimeState.serviceState);
+ Object.assign(
+ next,
+ pickDefinedFields(normalizePlainObject(runtimeServiceState.proxy), RUNTIME_PROXY_FIELDS)
+ );
+ }
+
+ Object.assign(next, flattenOpenAiFlowState(flowState));
+ if (Object.prototype.hasOwnProperty.call(runtimeState, 'flowState')) {
+ Object.assign(next, flattenOpenAiFlowState(normalizePlainObject(runtimeState.flowState)));
+ }
+
+ return next;
+ }
+
+ function buildStateView(state = {}) {
+ const runtimeState = ensureRuntimeState(state);
+ return {
+ ...state,
+ activeFlowId: runtimeState.activeFlowId,
+ activeRunId: runtimeState.activeRunId,
+ currentNodeId: runtimeState.currentNodeId,
+ nodeStatuses: cloneValue(runtimeState.nodeStatuses),
+ flowState: cloneValue(runtimeState.flowState),
+ sharedState: cloneValue(runtimeState.sharedState),
+ serviceState: cloneValue(runtimeState.serviceState),
+ legacyStepCompat: cloneValue(runtimeState.legacyStepCompat),
+ currentStep: runtimeState.legacyStepCompat.currentStep,
+ stepStatuses: cloneValue(runtimeState.legacyStepCompat.stepStatuses),
+ runtimeState,
+ };
+ }
+
+ function buildSessionStatePatch(currentState = {}, updates = {}) {
+ const flattenedUpdates = buildFlattenedUpdates(updates);
+ const nextState = {
+ ...currentState,
+ ...flattenedUpdates,
+ };
+ const runtimeState = ensureRuntimeState(nextState);
+
+ return {
+ ...flattenedUpdates,
+ activeFlowId: runtimeState.activeFlowId,
+ activeRunId: runtimeState.activeRunId,
+ currentNodeId: runtimeState.currentNodeId,
+ nodeStatuses: cloneValue(runtimeState.nodeStatuses),
+ currentStep: runtimeState.legacyStepCompat.currentStep,
+ stepStatuses: cloneValue(runtimeState.legacyStepCompat.stepStatuses),
+ runtimeState,
+ };
+ }
+
+ return {
+ DEFAULT_ACTIVE_FLOW_ID,
+ OPENAI_FLOW_FIELD_GROUPS,
+ RUNTIME_PROXY_FIELDS,
+ RUNTIME_SHARED_FIELDS,
+ buildDefaultRuntimeState: buildRuntimeStateDefault,
+ buildSessionStatePatch,
+ buildStateView,
+ ensureRuntimeState,
+ };
+ }
+
+ return {
+ createRuntimeStateHelpers,
+ };
+});
diff --git a/background/tab-runtime.js b/background/tab-runtime.js
index f08f1a3..b689715 100644
--- a/background/tab-runtime.js
+++ b/background/tab-runtime.js
@@ -11,6 +11,7 @@
isRetryableContentScriptTransportError,
LOG_PREFIX,
matchesSourceUrlFamily,
+ sourceRegistry = null,
setState,
sleepWithStop,
STOP_ERROR_MESSAGE,
@@ -19,6 +20,65 @@
const pendingCommands = new Map();
+ function resolveCanonicalSource(source) {
+ if (sourceRegistry?.resolveCanonicalSource) {
+ return sourceRegistry.resolveCanonicalSource(source);
+ }
+ return String(source || '').trim();
+ }
+
+ function getSourceKeys(source) {
+ if (sourceRegistry?.getSourceKeys) {
+ const registryKeys = sourceRegistry.getSourceKeys(source);
+ if (Array.isArray(registryKeys) && registryKeys.length) {
+ return registryKeys;
+ }
+ }
+ const normalized = String(source || '').trim();
+ return normalized ? [normalized] : [];
+ }
+
+ function getSourceCommandKey(source) {
+ const keys = getSourceKeys(source);
+ return keys[0] || String(source || '').trim();
+ }
+
+ function sourcesMatch(leftSource, rightSource) {
+ const left = resolveCanonicalSource(leftSource);
+ const right = resolveCanonicalSource(rightSource);
+ return Boolean(left && right && left === right);
+ }
+
+ function getSourceMapValue(record, source) {
+ const map = record && typeof record === 'object' ? record : {};
+ for (const key of getSourceKeys(source)) {
+ if (Object.prototype.hasOwnProperty.call(map, key)) {
+ return map[key];
+ }
+ }
+ return undefined;
+ }
+
+ function setSourceMapValue(record, source, value) {
+ const nextRecord = { ...(record || {}) };
+ const keys = getSourceKeys(source);
+ const canonicalKey = keys[0] || String(source || '').trim();
+ for (const key of keys.slice(1)) {
+ delete nextRecord[key];
+ }
+ if (canonicalKey) {
+ nextRecord[canonicalKey] = value;
+ }
+ return nextRecord;
+ }
+
+ function getCleanupOwnerSource(cleanupScope) {
+ if (sourceRegistry?.getCleanupOwnerSource) {
+ return sourceRegistry.getCleanupOwnerSource(cleanupScope);
+ }
+ return cleanupScope === 'oauth-localhost-callback' ? 'signup-page' : '';
+ }
+
function normalizeAutomationWindowId(value) {
if (value === null || value === undefined || value === '') {
return null;
@@ -170,7 +230,7 @@
}
async function registerTab(source, tabId) {
- const registry = await getTabRegistry();
+ let registry = await getTabRegistry();
let windowId = null;
if (chrome?.tabs?.get && Number.isInteger(tabId)) {
const tab = await chrome.tabs.get(tabId).catch(() => null);
@@ -180,42 +240,42 @@
}
windowId = normalizeAutomationWindowId(tab?.windowId);
}
- registry[source] = {
+ registry = setSourceMapValue(registry, source, {
tabId,
ready: true,
...(windowId !== null ? { windowId } : {}),
- };
+ });
await setState({ tabRegistry: registry });
console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
}
async function isTabAlive(source) {
- const registry = await getTabRegistry();
- const entry = registry[source];
+ let registry = await getTabRegistry();
+ const entry = getSourceMapValue(registry, source);
if (!entry) return false;
try {
const tab = await chrome.tabs.get(entry.tabId);
if (!(await isTabInAutomationWindow(tab))) {
- registry[source] = null;
+ registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
return false;
}
return true;
} catch {
- registry[source] = null;
+ registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
return false;
}
}
async function getTabId(source) {
- const registry = await getTabRegistry();
- const tabId = registry[source]?.tabId || null;
+ let registry = await getTabRegistry();
+ const tabId = getSourceMapValue(registry, source)?.tabId || null;
if (!Number.isInteger(tabId)) {
return null;
}
if (!(await isTabInAutomationWindow(tabId))) {
- registry[source] = null;
+ registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
return null;
}
@@ -225,8 +285,7 @@
async function rememberSourceLastUrl(source, url) {
if (!source || !url) return;
const state = await getState();
- const sourceLastUrls = { ...(state.sourceLastUrls || {}) };
- sourceLastUrls[source] = url;
+ const sourceLastUrls = setSourceMapValue(state.sourceLastUrls, source, url);
await setState({ sourceLastUrls });
}
@@ -234,7 +293,7 @@
const { excludeTabIds = [] } = options;
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
const state = await getState();
- const lastUrl = state.sourceLastUrls?.[source];
+ const lastUrl = getSourceMapValue(state.sourceLastUrls, source);
const referenceUrls = [currentUrl, lastUrl].filter(Boolean);
if (!referenceUrls.length) return;
@@ -249,9 +308,10 @@
await chrome.tabs.remove(matchedIds).catch(() => { });
- const registry = await getTabRegistry();
- if (registry[source]?.tabId && matchedIds.includes(registry[source].tabId)) {
- registry[source] = null;
+ let registry = await getTabRegistry();
+ const sourceEntry = getSourceMapValue(registry, source);
+ if (sourceEntry?.tabId && matchedIds.includes(sourceEntry.tabId)) {
+ registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
}
@@ -274,7 +334,7 @@
async function closeLocalhostCallbackTabs(callbackUrl, options = {}) {
if (!isLocalhostOAuthCallbackUrl(callbackUrl)) return 0;
- const { excludeTabIds = [] } = options;
+ const { excludeTabIds = [], ownerSource = getCleanupOwnerSource('oauth-localhost-callback') } = options;
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
const tabs = await queryTabsInAutomationWindow({});
const matchedIds = tabs
@@ -286,9 +346,10 @@
await chrome.tabs.remove(matchedIds).catch(() => { });
- const registry = await getTabRegistry();
- if (registry['signup-page']?.tabId && matchedIds.includes(registry['signup-page'].tabId)) {
- registry['signup-page'] = null;
+ let registry = await getTabRegistry();
+ const ownerEntry = getSourceMapValue(registry, ownerSource);
+ if (ownerEntry?.tabId && matchedIds.includes(ownerEntry.tabId)) {
+ registry = setSourceMapValue(registry, ownerSource, null);
await setState({ tabRegistry: registry });
}
@@ -468,7 +529,7 @@
while (Date.now() - start < timeoutMs) {
attempt += 1;
const pong = await pingContentScriptOnTab(tabId);
- if (pong?.ok && (!pong.source || pong.source === source)) {
+ if (pong?.ok && (!pong.source || sourcesMatch(pong.source, source))) {
console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
await registerTab(source, tabId);
return;
@@ -478,9 +539,13 @@
throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
}
- const registry = await getTabRegistry();
- if (registry[source]) {
- registry[source].ready = false;
+ let registry = await getTabRegistry();
+ const sourceEntry = getSourceMapValue(registry, source);
+ if (sourceEntry) {
+ registry = setSourceMapValue(registry, source, {
+ ...sourceEntry,
+ ready: false,
+ });
await setState({ tabRegistry: registry });
}
@@ -505,7 +570,7 @@
}
const pongAfterInject = await pingContentScriptOnTab(tabId);
- if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) {
+ if (pongAfterInject?.ok && (!pongAfterInject.source || sourcesMatch(pongAfterInject.source, source))) {
console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready after inject ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
await registerTab(source, tabId);
return;
@@ -609,14 +674,16 @@
function queueCommand(source, message, timeout = 15000) {
return new Promise((resolve, reject) => {
+ const commandKey = getSourceCommandKey(source);
const timer = setTimeout(() => {
- pendingCommands.delete(source);
+ pendingCommands.delete(commandKey);
reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
}, timeout);
- pendingCommands.set(source, {
+ pendingCommands.set(commandKey, {
message,
resolve,
reject,
+ source,
timer,
responseTimeoutMs: timeout,
});
@@ -625,11 +692,16 @@
}
function flushCommand(source, tabId) {
- const pending = pendingCommands.get(source);
+ const pending = pendingCommands.get(getSourceCommandKey(source));
if (pending) {
clearTimeout(pending.timer);
- pendingCommands.delete(source);
- sendTabMessageWithTimeout(tabId, source, pending.message, pending.responseTimeoutMs).then(pending.resolve).catch(pending.reject);
+ pendingCommands.delete(getSourceCommandKey(source));
+ sendTabMessageWithTimeout(
+ tabId,
+ pending.source || source,
+ pending.message,
+ pending.responseTimeoutMs
+ ).then(pending.resolve).catch(pending.reject);
console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
}
}
@@ -677,18 +749,29 @@
const sameUrl = currentTab.url === url;
const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
- const registry = await getTabRegistry();
+ let registry = await getTabRegistry();
+ const sourceEntry = getSourceMapValue(registry, source);
if (sameUrl) {
await chrome.tabs.update(tabId, { active: true });
if (shouldReloadOnReuse) {
- if (registry[source]) registry[source].ready = false;
+ if (sourceEntry) {
+ registry = setSourceMapValue(registry, source, {
+ ...sourceEntry,
+ ready: false,
+ });
+ }
await setState({ tabRegistry: registry });
await chrome.tabs.reload(tabId);
await waitForTabUpdateComplete(tabId);
}
if (options.inject) {
- if (registry[source]) registry[source].ready = false;
+ if (sourceEntry) {
+ registry = setSourceMapValue(registry, source, {
+ ...sourceEntry,
+ ready: false,
+ });
+ }
await setState({ tabRegistry: registry });
if (options.injectSource) {
await chrome.scripting.executeScript({
@@ -710,7 +793,12 @@
return tabId;
}
- if (registry[source]) registry[source].ready = false;
+ if (sourceEntry) {
+ registry = setSourceMapValue(registry, source, {
+ ...sourceEntry,
+ ready: false,
+ });
+ }
await setState({ tabRegistry: registry });
await chrome.tabs.update(tabId, { url, active: true });
@@ -765,7 +853,7 @@
throwIfStopped();
const { responseTimeoutMs = getContentScriptResponseTimeoutMs(message) } = options;
const registry = await getTabRegistry();
- const entry = registry[source];
+ const entry = getSourceMapValue(registry, source);
if (!entry || !entry.ready) {
throwIfStopped();
diff --git a/background/verification-flow.js b/background/verification-flow.js
index 25fcb3d..dcfb066 100644
--- a/background/verification-flow.js
+++ b/background/verification-flow.js
@@ -7,6 +7,7 @@
function createVerificationFlowHelpers(deps = {}) {
const {
addLog: rawAddLog = async () => {},
+ buildVerificationPollPayload: externalBuildVerificationPollPayload = null,
chrome,
closeConflictingTabsForSource,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
@@ -408,6 +409,9 @@
}
function getVerificationPollPayload(step, state, overrides = {}) {
+ if (typeof externalBuildVerificationPollPayload === 'function') {
+ return externalBuildVerificationPollPayload(step, state, overrides);
+ }
const is2925Provider = state?.mailProvider === '2925';
const mail2925MatchTargetEmail = is2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive';
diff --git a/content/utils.js b/content/utils.js
index 4a35f3e..8e749c3 100644
--- a/content/utils.js
+++ b/content/utils.js
@@ -7,8 +7,16 @@ function detectScriptSource({
url = '',
hostname = '',
} = {}) {
+ const sourceRegistry = globalThis?.MultiPageSourceRegistry?.createSourceRegistry?.();
+ if (sourceRegistry?.detectSourceFromLocation) {
+ return sourceRegistry.detectSourceFromLocation({
+ injectedSource,
+ url,
+ hostname,
+ });
+ }
if (injectedSource) return injectedSource;
- if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
+ if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'openai-auth';
if (hostname === 'mail.qq.com' || hostname === 'wx.mail.qq.com') return 'qq-mail';
if (
hostname === 'mail.163.com'
@@ -22,8 +30,7 @@ function detectScriptSource({
if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
if (url.includes('chatgpt.com')) return 'chatgpt';
if (url.includes("2925.com")) return "mail-2925";
- // VPS panel — detected dynamically since URL is configurable
- return 'vps-panel';
+ return 'unknown-source';
}
const SCRIPT_SOURCE = (() => {
@@ -294,6 +301,10 @@ function log(message, level = 'info', options = {}) {
* Report that this content script is loaded and ready.
*/
function reportReady() {
+ if (getRuntimeScriptSource() === 'unknown-source') {
+ console.warn(LOG_PREFIX, 'skip CONTENT_SCRIPT_READY for unknown source');
+ return;
+ }
console.log(LOG_PREFIX, '内容脚本已就绪');
const message = {
type: 'CONTENT_SCRIPT_READY',
@@ -439,6 +450,10 @@ async function humanPause(min = 250, max = 850) {
}
function shouldReportReadyForFrame(source, isChildFrame) {
+ const sourceRegistry = globalThis?.MultiPageSourceRegistry?.createSourceRegistry?.();
+ if (sourceRegistry?.shouldReportReadyForFrame) {
+ return sourceRegistry.shouldReportReadyForFrame(source, isChildFrame);
+ }
if (!isChildFrame) return true;
return ![
'qq-mail',
@@ -447,6 +462,7 @@ function shouldReportReadyForFrame(source, isChildFrame) {
'mail-2925',
'inbucket-mail',
'plus-checkout',
+ 'unknown-source',
].includes(source);
}
diff --git a/flows/openai/mail-rules.js b/flows/openai/mail-rules.js
new file mode 100644
index 0000000..940e80e
--- /dev/null
+++ b/flows/openai/mail-rules.js
@@ -0,0 +1,69 @@
+(function attachOpenAiMailRules(root, factory) {
+ root.MultiPageOpenAiMailRules = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createOpenAiMailRulesModule() {
+ const SIGNUP_CODE_RULE_ID = 'openai-signup-code';
+ const LOGIN_CODE_RULE_ID = 'openai-login-code';
+
+ function createOpenAiMailRules(deps = {}) {
+ const {
+ getHotmailVerificationRequestTimestamp = () => 0,
+ MAIL_2925_VERIFICATION_INTERVAL_MS = 15000,
+ MAIL_2925_VERIFICATION_MAX_ATTEMPTS = 15,
+ } = deps;
+
+ function isMail2925Provider(state = {}) {
+ return String(state?.mailProvider || '').trim().toLowerCase() === '2925';
+ }
+
+ function shouldMatchMail2925TargetEmail(state = {}) {
+ return isMail2925Provider(state)
+ && String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive';
+ }
+
+ function getRuleDefinition(step, state = {}) {
+ const normalizedStep = Number(step) === 4 ? 4 : 8;
+ const mail2925Provider = isMail2925Provider(state);
+ const signupStep = normalizedStep === 4;
+
+ return {
+ flowId: 'openai',
+ ruleId: signupStep ? SIGNUP_CODE_RULE_ID : LOGIN_CODE_RULE_ID,
+ step: normalizedStep,
+ artifactType: 'code',
+ filterAfterTimestamp: mail2925Provider
+ ? 0
+ : getHotmailVerificationRequestTimestamp(normalizedStep, state),
+ senderFilters: signupStep
+ ? ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward']
+ : ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
+ subjectFilters: signupStep
+ ? ['verify', 'verification', 'code', '验证码', 'confirm']
+ : ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
+ targetEmail: signupStep
+ ? state?.email
+ : (String(state?.step8VerificationTargetEmail || '').trim() || state?.email),
+ mail2925MatchTargetEmail: shouldMatchMail2925TargetEmail(state),
+ maxAttempts: mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
+ intervalMs: mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
+ };
+ }
+
+ function buildVerificationPollPayload(step, state = {}, overrides = {}) {
+ return {
+ ...getRuleDefinition(step, state),
+ ...(overrides || {}),
+ };
+ }
+
+ return {
+ buildVerificationPollPayload,
+ getRuleDefinition,
+ };
+ }
+
+ return {
+ LOGIN_CODE_RULE_ID,
+ SIGNUP_CODE_RULE_ID,
+ createOpenAiMailRules,
+ };
+});
diff --git a/manifest.json b/manifest.json
index f79b796..2d53725 100644
--- a/manifest.json
+++ b/manifest.json
@@ -49,6 +49,7 @@
],
"js": [
"content/activation-utils.js",
+ "shared/source-registry.js",
"content/utils.js",
"content/operation-delay.js",
"content/auth-page-recovery.js",
@@ -65,6 +66,7 @@
],
"js": [
"content/activation-utils.js",
+ "shared/source-registry.js",
"content/utils.js",
"content/qq-mail.js"
],
@@ -81,6 +83,7 @@
],
"js": [
"content/activation-utils.js",
+ "shared/source-registry.js",
"content/utils.js",
"content/mail-163.js"
],
@@ -94,6 +97,7 @@
],
"js": [
"content/activation-utils.js",
+ "shared/source-registry.js",
"content/utils.js",
"content/icloud-mail.js"
],
@@ -106,6 +110,7 @@
],
"js": [
"content/activation-utils.js",
+ "shared/source-registry.js",
"content/utils.js",
"content/operation-delay.js",
"content/duck-mail.js"
diff --git a/shared/flow-capabilities.js b/shared/flow-capabilities.js
new file mode 100644
index 0000000..8946645
--- /dev/null
+++ b/shared/flow-capabilities.js
@@ -0,0 +1,242 @@
+(function attachMultiPageFlowCapabilities(root, factory) {
+ root.MultiPageFlowCapabilities = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createFlowCapabilitiesModule() {
+ const DEFAULT_FLOW_ID = 'openai';
+ const DEFAULT_PANEL_MODE = 'cpa';
+ const SIGNUP_METHOD_EMAIL = 'email';
+ const SIGNUP_METHOD_PHONE = 'phone';
+ const VALID_PANEL_MODES = Object.freeze(['cpa', 'sub2api', 'codex2api']);
+
+ const DEFAULT_FLOW_CAPABILITIES = Object.freeze({
+ supportsEmailSignup: true,
+ supportsPhoneSignup: false,
+ supportsPhoneVerificationSettings: false,
+ supportsPlusMode: false,
+ supportsContributionMode: false,
+ supportsPlatformBinding: [],
+ supportsLuckmail: false,
+ supportsOauthTimeoutBudget: false,
+ canSwitchFlow: false,
+ stepDefinitionMode: 'default',
+ });
+
+ const FLOW_CAPABILITIES = Object.freeze({
+ openai: Object.freeze({
+ ...DEFAULT_FLOW_CAPABILITIES,
+ supportsPhoneSignup: true,
+ supportsPhoneVerificationSettings: true,
+ supportsPlusMode: true,
+ supportsContributionMode: true,
+ supportsPlatformBinding: ['cpa', 'sub2api', 'codex2api'],
+ supportsLuckmail: true,
+ supportsOauthTimeoutBudget: true,
+ stepDefinitionMode: 'openai-dynamic',
+ }),
+ });
+
+ const DEFAULT_PANEL_CAPABILITIES = Object.freeze({
+ supportsPhoneSignup: true,
+ requiresPhoneSignupWarning: false,
+ });
+
+ const PANEL_CAPABILITIES = Object.freeze({
+ cpa: Object.freeze({
+ supportsPhoneSignup: true,
+ requiresPhoneSignupWarning: true,
+ }),
+ sub2api: Object.freeze({
+ supportsPhoneSignup: true,
+ requiresPhoneSignupWarning: false,
+ }),
+ codex2api: Object.freeze({
+ supportsPhoneSignup: true,
+ requiresPhoneSignupWarning: false,
+ }),
+ });
+
+ function normalizeFlowId(value = '', fallback = DEFAULT_FLOW_ID) {
+ const normalized = String(value || '').trim().toLowerCase();
+ if (normalized) {
+ return normalized;
+ }
+ const fallbackValue = String(fallback || '').trim().toLowerCase();
+ return fallbackValue || DEFAULT_FLOW_ID;
+ }
+
+ function normalizePanelMode(value = '', fallback = DEFAULT_PANEL_MODE) {
+ const normalized = String(value || '').trim().toLowerCase();
+ if (VALID_PANEL_MODES.includes(normalized)) {
+ return normalized;
+ }
+ const fallbackValue = String(fallback || '').trim().toLowerCase();
+ return VALID_PANEL_MODES.includes(fallbackValue) ? fallbackValue : DEFAULT_PANEL_MODE;
+ }
+
+ function normalizeSignupMethod(value = '') {
+ return String(value || '').trim().toLowerCase() === SIGNUP_METHOD_PHONE
+ ? SIGNUP_METHOD_PHONE
+ : SIGNUP_METHOD_EMAIL;
+ }
+
+ function normalizePanelModeList(values = []) {
+ if (!Array.isArray(values)) {
+ return [];
+ }
+ const seen = new Set();
+ const normalized = [];
+ values.forEach((value) => {
+ const mode = normalizePanelMode(value, '');
+ if (!mode || seen.has(mode)) {
+ return;
+ }
+ seen.add(mode);
+ normalized.push(mode);
+ });
+ return normalized;
+ }
+
+ function createFlowCapabilityRegistry(deps = {}) {
+ const {
+ defaultFlowCapabilities = DEFAULT_FLOW_CAPABILITIES,
+ defaultFlowId = DEFAULT_FLOW_ID,
+ defaultPanelCapabilities = DEFAULT_PANEL_CAPABILITIES,
+ flowCapabilities = FLOW_CAPABILITIES,
+ panelCapabilities = PANEL_CAPABILITIES,
+ } = deps;
+
+ function getFlowCapabilities(flowId) {
+ const normalizedFlowId = normalizeFlowId(flowId, defaultFlowId);
+ const entry = flowCapabilities[normalizedFlowId] || null;
+ return {
+ ...defaultFlowCapabilities,
+ ...(entry || {}),
+ supportsPlatformBinding: normalizePanelModeList(entry?.supportsPlatformBinding || defaultFlowCapabilities.supportsPlatformBinding),
+ };
+ }
+
+ function getPanelCapabilities(panelMode) {
+ const normalizedPanelMode = normalizePanelMode(panelMode);
+ return {
+ ...defaultPanelCapabilities,
+ ...(panelCapabilities[normalizedPanelMode] || {}),
+ };
+ }
+
+ function resolveSidepanelCapabilities(options = {}) {
+ const state = options?.state || {};
+ const activeFlowId = normalizeFlowId(
+ options?.activeFlowId ?? state?.activeFlowId,
+ defaultFlowId
+ );
+ const flowState = getFlowCapabilities(activeFlowId);
+ const requestedPanelMode = normalizePanelMode(
+ options?.panelMode ?? state?.panelMode,
+ DEFAULT_PANEL_MODE
+ );
+ const supportedPanelModes = normalizePanelModeList(flowState.supportsPlatformBinding);
+ const panelModeSupported = supportedPanelModes.length === 0
+ ? true
+ : supportedPanelModes.includes(requestedPanelMode);
+ const effectivePanelMode = panelModeSupported
+ ? requestedPanelMode
+ : supportedPanelModes[0];
+ const panelState = getPanelCapabilities(effectivePanelMode);
+ const runtimeLocks = {
+ autoRunLocked: Boolean(options?.autoRunLocked ?? state?.autoRunLocked),
+ contributionMode: flowState.supportsContributionMode && Boolean(state?.contributionMode),
+ phoneVerificationEnabled: flowState.supportsPhoneVerificationSettings && Boolean(state?.phoneVerificationEnabled),
+ plusModeEnabled: flowState.supportsPlusMode && Boolean(state?.plusModeEnabled),
+ settingsMenuLocked: Boolean(options?.settingsMenuLocked ?? state?.settingsMenuLocked),
+ };
+ const effectiveSignupMethods = [];
+ if (flowState.supportsEmailSignup !== false) {
+ effectiveSignupMethods.push(SIGNUP_METHOD_EMAIL);
+ }
+ const canSelectPhoneSignup = Boolean(flowState.supportsPhoneSignup)
+ && Boolean(panelState.supportsPhoneSignup)
+ && runtimeLocks.phoneVerificationEnabled
+ && !runtimeLocks.plusModeEnabled
+ && !runtimeLocks.contributionMode;
+ if (canSelectPhoneSignup) {
+ effectiveSignupMethods.push(SIGNUP_METHOD_PHONE);
+ }
+ if (!effectiveSignupMethods.length) {
+ effectiveSignupMethods.push(SIGNUP_METHOD_EMAIL);
+ }
+ const requestedSignupMethod = normalizeSignupMethod(
+ options?.signupMethod ?? state?.signupMethod
+ );
+ const effectiveSignupMethod = requestedSignupMethod === SIGNUP_METHOD_PHONE && canSelectPhoneSignup
+ ? SIGNUP_METHOD_PHONE
+ : (effectiveSignupMethods.includes(SIGNUP_METHOD_EMAIL)
+ ? SIGNUP_METHOD_EMAIL
+ : effectiveSignupMethods[0]);
+
+ return {
+ activeFlowId,
+ canShowContributionMode: Boolean(flowState.supportsContributionMode),
+ canShowLuckmail: Boolean(flowState.supportsLuckmail),
+ canShowPhoneSettings: Boolean(flowState.supportsPhoneVerificationSettings),
+ canShowPlusSettings: Boolean(flowState.supportsPlusMode),
+ canSwitchFlow: Boolean(flowState.canSwitchFlow),
+ canUsePhoneSignup: canSelectPhoneSignup,
+ canUseSelectedPanelMode: panelModeSupported,
+ effectivePanelMode,
+ effectiveSignupMethod,
+ effectiveSignupMethods,
+ flowCapabilities: flowState,
+ panelCapabilities: panelState,
+ panelMode: effectivePanelMode,
+ requestedPanelMode,
+ requestedSignupMethod,
+ runtimeLocks,
+ shouldWarnCpaPhoneSignup: effectiveSignupMethod === SIGNUP_METHOD_PHONE
+ && Boolean(panelState.requiresPhoneSignupWarning),
+ stepDefinitionOptions: {
+ activeFlowId,
+ panelMode: effectivePanelMode,
+ plusModeEnabled: runtimeLocks.plusModeEnabled,
+ signupMethod: effectiveSignupMethod,
+ },
+ supportedPanelModes,
+ };
+ }
+
+ function canUsePhoneSignup(state = {}) {
+ return resolveSidepanelCapabilities({ state }).canUsePhoneSignup;
+ }
+
+ function resolveSignupMethod(state = {}, signupMethod = undefined) {
+ return resolveSidepanelCapabilities({
+ signupMethod,
+ state,
+ }).effectiveSignupMethod;
+ }
+
+ return {
+ canUsePhoneSignup,
+ getFlowCapabilities,
+ getPanelCapabilities,
+ normalizeFlowId,
+ normalizePanelMode,
+ normalizeSignupMethod,
+ resolveSidepanelCapabilities,
+ resolveSignupMethod,
+ };
+ }
+
+ return {
+ createFlowCapabilityRegistry,
+ DEFAULT_FLOW_CAPABILITIES,
+ DEFAULT_FLOW_ID,
+ DEFAULT_PANEL_CAPABILITIES,
+ DEFAULT_PANEL_MODE,
+ FLOW_CAPABILITIES,
+ PANEL_CAPABILITIES,
+ SIGNUP_METHOD_EMAIL,
+ SIGNUP_METHOD_PHONE,
+ normalizeFlowId,
+ normalizePanelMode,
+ normalizeSignupMethod,
+ };
+});
diff --git a/shared/source-registry.js b/shared/source-registry.js
new file mode 100644
index 0000000..6d7ce66
--- /dev/null
+++ b/shared/source-registry.js
@@ -0,0 +1,433 @@
+(function attachMultiPageSourceRegistry(root, factory) {
+ root.MultiPageSourceRegistry = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createSourceRegistryModule() {
+ const SOURCE_ALIASES = Object.freeze({
+ 'signup-page': 'openai-auth',
+ });
+
+ const SOURCE_DEFINITIONS = Object.freeze({
+ 'openai-auth': {
+ flowId: 'openai',
+ kind: 'flow-page',
+ label: '认证页',
+ readyPolicy: 'allow-child-frame',
+ family: 'openai-auth-family',
+ driverId: 'content/signup-page',
+ cleanupScopes: ['oauth-localhost-callback'],
+ },
+ chatgpt: {
+ flowId: 'openai',
+ kind: 'flow-entry',
+ label: 'ChatGPT 首页',
+ readyPolicy: 'allow-child-frame',
+ family: 'chatgpt-entry-family',
+ driverId: null,
+ cleanupScopes: [],
+ },
+ 'qq-mail': {
+ flowId: null,
+ kind: 'mail-provider',
+ label: 'QQ 邮箱',
+ readyPolicy: 'top-frame-only',
+ family: 'qq-mail-family',
+ driverId: 'content/qq-mail',
+ cleanupScopes: [],
+ },
+ 'mail-163': {
+ flowId: null,
+ kind: 'mail-provider',
+ label: '163 邮箱',
+ readyPolicy: 'top-frame-only',
+ family: 'mail-163-family',
+ driverId: 'content/mail-163',
+ cleanupScopes: [],
+ },
+ 'gmail-mail': {
+ flowId: null,
+ kind: 'mail-provider',
+ label: 'Gmail 邮箱',
+ readyPolicy: 'top-frame-only',
+ family: 'gmail-mail-family',
+ driverId: 'content/gmail-mail',
+ cleanupScopes: [],
+ },
+ 'icloud-mail': {
+ flowId: null,
+ kind: 'mail-provider',
+ label: 'iCloud 邮箱',
+ readyPolicy: 'allow-child-frame',
+ family: 'icloud-mail-family',
+ driverId: 'content/icloud-mail',
+ cleanupScopes: [],
+ },
+ 'inbucket-mail': {
+ flowId: null,
+ kind: 'mail-provider',
+ label: 'Inbucket 邮箱',
+ readyPolicy: 'top-frame-only',
+ family: 'inbucket-mail-family',
+ driverId: 'content/inbucket-mail',
+ cleanupScopes: [],
+ },
+ 'mail-2925': {
+ flowId: null,
+ kind: 'mail-provider',
+ label: '2925 邮箱',
+ readyPolicy: 'top-frame-only',
+ family: 'mail-2925-family',
+ driverId: 'content/mail-2925',
+ cleanupScopes: [],
+ },
+ 'duck-mail': {
+ flowId: null,
+ kind: 'mail-provider',
+ label: 'Duck 邮箱',
+ readyPolicy: 'allow-child-frame',
+ family: 'duck-mail-family',
+ driverId: 'content/duck-mail',
+ cleanupScopes: [],
+ },
+ 'vps-panel': {
+ flowId: 'openai',
+ kind: 'panel-page',
+ label: 'CPA 面板',
+ readyPolicy: 'allow-child-frame',
+ family: 'vps-panel-family',
+ driverId: 'content/vps-panel',
+ cleanupScopes: [],
+ },
+ 'sub2api-panel': {
+ flowId: 'openai',
+ kind: 'panel-page',
+ label: 'SUB2API 后台',
+ readyPolicy: 'allow-child-frame',
+ family: 'sub2api-panel-family',
+ driverId: 'content/sub2api-panel',
+ cleanupScopes: [],
+ },
+ 'codex2api-panel': {
+ flowId: 'openai',
+ kind: 'panel-page',
+ label: 'Codex2API 后台',
+ readyPolicy: 'allow-child-frame',
+ family: 'codex2api-panel-family',
+ driverId: 'content/sub2api-panel',
+ cleanupScopes: [],
+ },
+ 'plus-checkout': {
+ flowId: 'openai',
+ kind: 'flow-page',
+ label: 'Plus Checkout',
+ readyPolicy: 'top-frame-only',
+ family: 'plus-checkout-family',
+ driverId: 'content/plus-checkout',
+ cleanupScopes: [],
+ },
+ 'paypal-flow': {
+ flowId: 'openai',
+ kind: 'flow-page',
+ label: 'PayPal 授权页',
+ readyPolicy: 'allow-child-frame',
+ family: 'paypal-flow-family',
+ driverId: 'content/paypal-flow',
+ cleanupScopes: [],
+ },
+ 'gopay-flow': {
+ flowId: 'openai',
+ kind: 'flow-page',
+ label: 'GoPay 授权页',
+ readyPolicy: 'allow-child-frame',
+ family: 'gopay-flow-family',
+ driverId: 'content/gopay-flow',
+ cleanupScopes: [],
+ },
+ 'unknown-source': {
+ flowId: null,
+ kind: 'unknown',
+ label: '未知来源',
+ readyPolicy: 'disabled',
+ family: 'unknown-family',
+ driverId: null,
+ cleanupScopes: [],
+ },
+ });
+
+ const DRIVER_DEFINITIONS = Object.freeze({
+ 'content/signup-page': {
+ sourceId: 'openai-auth',
+ commands: [
+ 'OPEN_SIGNUP',
+ 'SUBMIT_SIGNUP_IDENTIFIER',
+ 'SUBMIT_PASSWORD',
+ 'SUBMIT_PROFILE',
+ 'SUBMIT_LOGIN_CODE',
+ 'SUBMIT_PHONE_CODE',
+ 'DETECT_AUTH_STATE',
+ ],
+ },
+ 'content/qq-mail': {
+ sourceId: 'qq-mail',
+ commands: ['POLL_EMAIL'],
+ },
+ 'content/mail-163': {
+ sourceId: 'mail-163',
+ commands: ['POLL_EMAIL'],
+ },
+ 'content/gmail-mail': {
+ sourceId: 'gmail-mail',
+ commands: ['POLL_EMAIL'],
+ },
+ 'content/icloud-mail': {
+ sourceId: 'icloud-mail',
+ commands: ['POLL_EMAIL'],
+ },
+ 'content/mail-2925': {
+ sourceId: 'mail-2925',
+ commands: ['POLL_EMAIL'],
+ },
+ 'content/duck-mail': {
+ sourceId: 'duck-mail',
+ commands: ['FETCH_ALIAS_EMAIL'],
+ },
+ 'content/sub2api-panel': {
+ sourceId: 'sub2api-panel',
+ commands: ['OPEN_PANEL', 'FETCH_OAUTH_URL', 'VERIFY_PLATFORM'],
+ },
+ 'content/vps-panel': {
+ sourceId: 'vps-panel',
+ commands: ['OPEN_PANEL', 'FETCH_OAUTH_URL'],
+ },
+ 'content/plus-checkout': {
+ sourceId: 'plus-checkout',
+ commands: ['CREATE_CHECKOUT', 'FILL_CHECKOUT'],
+ },
+ 'content/paypal-flow': {
+ sourceId: 'paypal-flow',
+ commands: ['APPROVE_PAYPAL'],
+ },
+ 'content/gopay-flow': {
+ sourceId: 'gopay-flow',
+ commands: ['APPROVE_GOPAY'],
+ },
+ });
+
+ const CLEANUP_SCOPE_OWNERS = Object.freeze({
+ 'oauth-localhost-callback': 'openai-auth',
+ });
+
+ const AUTH_PAGE_HOSTS = new Set(['auth0.openai.com', 'auth.openai.com', 'accounts.openai.com']);
+ const ENTRY_PAGE_HOSTS = new Set(['chatgpt.com', 'www.chatgpt.com', 'chat.openai.com']);
+ const CHILD_FRAME_BLOCKED_SOURCES = new Set([
+ 'qq-mail',
+ 'mail-163',
+ 'gmail-mail',
+ 'mail-2925',
+ 'inbucket-mail',
+ 'plus-checkout',
+ ]);
+
+ function createSourceRegistry() {
+ function parseUrlSafely(rawUrl) {
+ if (!rawUrl) return null;
+ try {
+ return new URL(rawUrl);
+ } catch {
+ return null;
+ }
+ }
+
+ function normalizeSourceId(source) {
+ return String(source || '').trim();
+ }
+
+ function resolveCanonicalSource(source) {
+ const normalized = normalizeSourceId(source);
+ if (!normalized) return '';
+ return SOURCE_ALIASES[normalized] || normalized;
+ }
+
+ function getAliasKeysForCanonicalSource(source) {
+ const canonical = resolveCanonicalSource(source);
+ return Object.keys(SOURCE_ALIASES).filter((alias) => SOURCE_ALIASES[alias] === canonical);
+ }
+
+ function getSourceKeys(source) {
+ const normalized = normalizeSourceId(source);
+ const canonical = resolveCanonicalSource(normalized);
+ return Array.from(new Set([
+ canonical,
+ ...getAliasKeysForCanonicalSource(canonical),
+ normalized,
+ ].filter(Boolean)));
+ }
+
+ function getSourceMeta(source) {
+ const canonical = resolveCanonicalSource(source);
+ const definition = SOURCE_DEFINITIONS[canonical];
+ if (!definition) {
+ return null;
+ }
+ return {
+ id: canonical,
+ aliases: getAliasKeysForCanonicalSource(canonical),
+ ...definition,
+ };
+ }
+
+ function getSourceLabel(source) {
+ return getSourceMeta(source)?.label || normalizeSourceId(source) || '未知来源';
+ }
+
+ function getDriverIdForSource(source) {
+ return getSourceMeta(source)?.driverId || null;
+ }
+
+ function getDriverMeta(sourceOrDriverId) {
+ const directDriverId = normalizeSourceId(sourceOrDriverId);
+ const driverId = Object.prototype.hasOwnProperty.call(DRIVER_DEFINITIONS, directDriverId)
+ ? directDriverId
+ : getDriverIdForSource(sourceOrDriverId);
+ if (!driverId || !Object.prototype.hasOwnProperty.call(DRIVER_DEFINITIONS, driverId)) {
+ return null;
+ }
+ return {
+ id: driverId,
+ ...DRIVER_DEFINITIONS[driverId],
+ };
+ }
+
+ function isSignupPageHost(hostname = '') {
+ return AUTH_PAGE_HOSTS.has(String(hostname || '').toLowerCase());
+ }
+
+ function isSignupEntryHost(hostname = '') {
+ return ENTRY_PAGE_HOSTS.has(String(hostname || '').toLowerCase());
+ }
+
+ function is163MailHost(hostname = '') {
+ const normalized = String(hostname || '').toLowerCase();
+ return normalized === 'mail.163.com'
+ || normalized.endsWith('.mail.163.com')
+ || normalized === 'mail.126.com'
+ || normalized.endsWith('.mail.126.com')
+ || normalized === 'webmail.vip.163.com';
+ }
+
+ function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
+ const candidate = parseUrlSafely(candidateUrl);
+ if (!candidate) return false;
+
+ const canonical = resolveCanonicalSource(source);
+ const reference = parseUrlSafely(referenceUrl);
+
+ switch (canonical) {
+ case 'openai-auth':
+ return isSignupPageHost(candidate.hostname) || isSignupEntryHost(candidate.hostname);
+ case 'chatgpt':
+ return isSignupEntryHost(candidate.hostname);
+ case 'duck-mail':
+ return candidate.hostname === 'duckduckgo.com' && candidate.pathname.startsWith('/email/');
+ case 'qq-mail':
+ return candidate.hostname === 'mail.qq.com' || candidate.hostname === 'wx.mail.qq.com';
+ case 'mail-163':
+ return is163MailHost(candidate.hostname);
+ case 'gmail-mail':
+ return candidate.hostname === 'mail.google.com';
+ case 'icloud-mail':
+ return candidate.hostname === 'www.icloud.com'
+ || candidate.hostname === 'www.icloud.com.cn';
+ case 'inbucket-mail':
+ return Boolean(reference)
+ && candidate.origin === reference.origin
+ && candidate.pathname.startsWith('/m/');
+ case 'mail-2925':
+ return candidate.hostname === '2925.com' || candidate.hostname === 'www.2925.com';
+ case 'vps-panel':
+ return Boolean(reference)
+ && candidate.origin === reference.origin
+ && candidate.pathname === reference.pathname;
+ case 'sub2api-panel':
+ return Boolean(reference)
+ && candidate.origin === reference.origin
+ && (
+ candidate.pathname.startsWith('/admin/accounts')
+ || candidate.pathname.startsWith('/login')
+ || candidate.pathname === '/'
+ );
+ case 'codex2api-panel':
+ return Boolean(reference)
+ && candidate.origin === reference.origin
+ && (
+ candidate.pathname.startsWith('/admin/accounts')
+ || candidate.pathname === '/admin'
+ || candidate.pathname === '/'
+ );
+ case 'plus-checkout':
+ return candidate.hostname === 'chatgpt.com'
+ && candidate.pathname.startsWith('/checkout/');
+ case 'paypal-flow':
+ return candidate.hostname.endsWith('paypal.com');
+ case 'gopay-flow':
+ return /gopay|gojek/i.test(candidate.hostname);
+ default:
+ return false;
+ }
+ }
+
+ function detectSourceFromLocation({
+ injectedSource,
+ url = '',
+ hostname = '',
+ } = {}) {
+ if (injectedSource) return resolveCanonicalSource(injectedSource);
+
+ const normalizedHostname = String(hostname || '').toLowerCase();
+ const normalizedUrl = String(url || '');
+
+ if (isSignupPageHost(normalizedHostname)) return 'openai-auth';
+ if (normalizedHostname === 'mail.qq.com' || normalizedHostname === 'wx.mail.qq.com') return 'qq-mail';
+ if (is163MailHost(normalizedHostname)) return 'mail-163';
+ if (normalizedHostname === 'mail.google.com') return 'gmail-mail';
+ if (normalizedHostname === 'www.icloud.com' || normalizedHostname === 'www.icloud.com.cn') return 'icloud-mail';
+ if (normalizedUrl.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
+ if (normalizedUrl.includes('2925.com')) return 'mail-2925';
+ if (isSignupEntryHost(normalizedHostname)) return 'chatgpt';
+ return 'unknown-source';
+ }
+
+ function shouldReportReadyForFrame(source, isChildFrame) {
+ const canonical = resolveCanonicalSource(source);
+ const readyPolicy = getSourceMeta(canonical)?.readyPolicy || 'allow-child-frame';
+ if (readyPolicy === 'disabled') return false;
+ if (!isChildFrame) return true;
+ if (readyPolicy === 'top-frame-only') return false;
+ if (CHILD_FRAME_BLOCKED_SOURCES.has(canonical)) return false;
+ return true;
+ }
+
+ function getCleanupOwnerSource(cleanupScope) {
+ return resolveCanonicalSource(CLEANUP_SCOPE_OWNERS[String(cleanupScope || '').trim()] || '');
+ }
+
+ return {
+ detectSourceFromLocation,
+ getCleanupOwnerSource,
+ getDriverIdForSource,
+ getDriverMeta,
+ getSourceKeys,
+ getSourceLabel,
+ getSourceMeta,
+ is163MailHost,
+ isSignupEntryHost,
+ isSignupPageHost,
+ matchesSourceUrlFamily,
+ parseUrlSafely,
+ resolveCanonicalSource,
+ shouldReportReadyForFrame,
+ };
+ }
+
+ return {
+ createSourceRegistry,
+ };
+});
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index f26087c..9b70f74 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -1732,6 +1732,7 @@
+
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 7e7c5b3..99453c1 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -517,6 +517,7 @@ const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
+const DEFAULT_ACTIVE_FLOW_ID = 'openai';
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = DEFAULT_PLUS_PAYMENT_METHOD;
let currentSignupMethod = DEFAULT_SIGNUP_METHOD;
@@ -1931,6 +1932,39 @@ function shouldWarnCpaPhoneSignup(signupMethod = null, panelMode = null) {
)
);
+ const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function'
+ ? resolveCurrentSidepanelCapabilities({
+ panelMode: resolvedPanelMode,
+ signupMethod: resolvedSignupMethod,
+ state: {
+ ...(typeof latestState !== 'undefined' ? latestState : {}),
+ panelMode: resolvedPanelMode,
+ signupMethod: resolvedSignupMethod,
+ },
+ })
+ : (() => {
+ const rootScope = typeof window !== 'undefined' ? window : globalThis;
+ const registry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai',
+ }) || null;
+ return registry?.resolveSidepanelCapabilities
+ ? registry.resolveSidepanelCapabilities({
+ activeFlowId: typeof latestState !== 'undefined' ? latestState?.activeFlowId : '',
+ panelMode: resolvedPanelMode,
+ signupMethod: resolvedSignupMethod,
+ state: {
+ ...(typeof latestState !== 'undefined' ? latestState : {}),
+ panelMode: resolvedPanelMode,
+ signupMethod: resolvedSignupMethod,
+ },
+ })
+ : null;
+ })();
+
+ if (capabilityState && typeof capabilityState.shouldWarnCpaPhoneSignup === 'boolean') {
+ return capabilityState.shouldWarnCpaPhoneSignup && !isCpaPhoneSignupPromptDismissed();
+ }
+
return resolvedSignupMethod === SIGNUP_METHOD_PHONE
&& resolvedPanelMode === 'cpa'
&& !isCpaPhoneSignupPromptDismissed();
@@ -3470,6 +3504,57 @@ function collectSettingsPayload() {
? normalizeSignupMethod(latestState?.signupMethod)
: (String(latestState?.signupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'))
);
+ const normalizePanelModeSafe = typeof normalizePanelMode === 'function'
+ ? normalizePanelMode
+ : ((value = '') => {
+ const normalized = String(value || '').trim().toLowerCase();
+ return normalized === 'sub2api' || normalized === 'codex2api' ? normalized : 'cpa';
+ });
+ const rawPanelMode = normalizePanelModeSafe(selectPanelMode?.value || latestState?.panelMode || 'cpa');
+ const rawPlusModeEnabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
+ ? Boolean(inputPlusModeEnabled.checked)
+ : Boolean(latestState?.plusModeEnabled);
+ const rawPhoneVerificationEnabled = Boolean(inputPhoneVerificationEnabled?.checked);
+ const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function'
+ ? resolveCurrentSidepanelCapabilities({
+ panelMode: rawPanelMode,
+ signupMethod: selectedSignupMethod,
+ state: {
+ ...(latestState || {}),
+ panelMode: rawPanelMode,
+ plusModeEnabled: rawPlusModeEnabled,
+ phoneVerificationEnabled: rawPhoneVerificationEnabled,
+ signupMethod: selectedSignupMethod,
+ },
+ })
+ : (() => {
+ const rootScope = typeof window !== 'undefined' ? window : globalThis;
+ const registry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai',
+ }) || null;
+ return registry?.resolveSidepanelCapabilities
+ ? registry.resolveSidepanelCapabilities({
+ activeFlowId: latestState?.activeFlowId,
+ panelMode: rawPanelMode,
+ signupMethod: selectedSignupMethod,
+ state: {
+ ...(latestState || {}),
+ panelMode: rawPanelMode,
+ plusModeEnabled: rawPlusModeEnabled,
+ phoneVerificationEnabled: rawPhoneVerificationEnabled,
+ signupMethod: selectedSignupMethod,
+ },
+ })
+ : null;
+ })();
+ const effectivePanelMode = capabilityState?.effectivePanelMode || capabilityState?.panelMode || rawPanelMode;
+ const effectivePlusModeEnabled = capabilityState
+ ? Boolean(capabilityState.runtimeLocks?.plusModeEnabled)
+ : rawPlusModeEnabled;
+ const effectivePhoneVerificationEnabled = capabilityState
+ ? Boolean(capabilityState.runtimeLocks?.phoneVerificationEnabled)
+ : rawPhoneVerificationEnabled;
+ const effectiveSignupMethod = capabilityState?.effectiveSignupMethod || selectedSignupMethod;
const plusPaymentMethod = getSelectedPlusPaymentMethod();
const normalizeGpcHelperPhoneModeSafe = typeof normalizeGpcHelperPhoneModeValue === 'function'
? normalizeGpcHelperPhoneModeValue
@@ -3527,7 +3612,7 @@ function collectSettingsPayload() {
});
return {
...(contributionModeEnabled ? {} : {
- panelMode: selectPanelMode.value,
+ panelMode: effectivePanelMode,
}),
vpsUrl: inputVpsUrl.value.trim(),
vpsPassword: inputVpsPassword.value,
@@ -3565,9 +3650,7 @@ function collectSettingsPayload() {
ipProxyRegion: currentIpProxyServiceProfile.region,
codex2apiUrl: inputCodex2ApiUrl.value.trim(),
codex2apiAdminKey: inputCodex2ApiAdminKey.value.trim(),
- plusModeEnabled: typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
- ? Boolean(inputPlusModeEnabled.checked)
- : Boolean(latestState?.plusModeEnabled),
+ plusModeEnabled: effectivePlusModeEnabled,
plusPaymentMethod,
paypalEmail: String(currentPayPalAccount?.email || latestState?.paypalEmail || '').trim(),
paypalPassword: String(currentPayPalAccount?.password || latestState?.paypalPassword || ''),
@@ -3682,8 +3765,8 @@ function collectSettingsPayload() {
oauthFlowTimeoutEnabled: typeof inputOAuthFlowTimeoutEnabled !== 'undefined' && inputOAuthFlowTimeoutEnabled
? Boolean(inputOAuthFlowTimeoutEnabled.checked)
: true,
- phoneVerificationEnabled: Boolean(inputPhoneVerificationEnabled?.checked),
- signupMethod: selectedSignupMethod,
+ phoneVerificationEnabled: effectivePhoneVerificationEnabled,
+ signupMethod: effectiveSignupMethod,
phoneSmsProvider: phoneSmsProviderValue,
phoneSmsProviderOrder: phoneSmsProviderOrderValue,
verificationResendCount: normalizeVerificationResendCount(
@@ -7215,11 +7298,66 @@ function normalizePanelMode(value = '') {
return 'cpa';
}
+let flowCapabilityRegistry = null;
+
+function getFlowCapabilityRegistry() {
+ if (flowCapabilityRegistry) {
+ return flowCapabilityRegistry;
+ }
+ const rootScope = typeof window !== 'undefined' ? window : globalThis;
+ flowCapabilityRegistry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ }) || null;
+ return flowCapabilityRegistry;
+}
+
+function resolveCurrentSidepanelCapabilities(options = {}) {
+ const registry = getFlowCapabilityRegistry();
+ if (!registry?.resolveSidepanelCapabilities) {
+ return null;
+ }
+ const state = {
+ ...(latestState || {}),
+ ...(options?.state || {}),
+ };
+ return registry.resolveSidepanelCapabilities({
+ activeFlowId: options?.activeFlowId ?? state?.activeFlowId,
+ panelMode: options?.panelMode ?? state?.panelMode,
+ signupMethod: options?.signupMethod ?? state?.signupMethod,
+ state,
+ });
+}
+
+function resolveStepDefinitionCapabilityState(state = latestState, options = {}) {
+ const nextState = {
+ ...(state || {}),
+ ...(options?.state || {}),
+ };
+ const capabilityState = resolveCurrentSidepanelCapabilities({
+ activeFlowId: options?.activeFlowId ?? nextState?.activeFlowId,
+ panelMode: options?.panelMode ?? nextState?.panelMode,
+ signupMethod: options?.signupMethod ?? nextState?.signupMethod,
+ state: nextState,
+ });
+ return {
+ capabilityState,
+ plusModeEnabled: capabilityState
+ ? Boolean(capabilityState.runtimeLocks?.plusModeEnabled)
+ : Boolean(nextState?.plusModeEnabled),
+ signupMethod: capabilityState?.effectiveSignupMethod
+ || normalizeSignupMethod((options?.signupMethod ?? nextState?.signupMethod) || DEFAULT_SIGNUP_METHOD),
+ };
+}
+
function getSelectedPanelMode() {
const selectedValue = typeof selectPanelMode !== 'undefined' && selectPanelMode
? selectPanelMode.value
: (typeof latestState !== 'undefined' ? latestState?.panelMode : '');
- return normalizePanelMode(selectedValue || 'cpa');
+ const resolvedPanelMode = normalizePanelMode(selectedValue || 'cpa');
+ const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function'
+ ? resolveCurrentSidepanelCapabilities({ panelMode: resolvedPanelMode })
+ : null;
+ return capabilityState?.effectivePanelMode || capabilityState?.panelMode || resolvedPanelMode;
}
function getSelectedSignupMethod() {
@@ -7244,6 +7382,37 @@ function canSelectPhoneSignupMethod() {
? Boolean(inputPlusModeEnabled.checked)
: Boolean(latestState?.plusModeEnabled);
const contributionModeEnabled = Boolean(latestState?.contributionMode);
+ const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function'
+ ? resolveCurrentSidepanelCapabilities({
+ panelMode: typeof getSelectedPanelMode === 'function' ? getSelectedPanelMode() : latestState?.panelMode,
+ state: {
+ ...(typeof latestState !== 'undefined' ? latestState : {}),
+ phoneVerificationEnabled: phoneEnabled,
+ plusModeEnabled,
+ contributionMode: contributionModeEnabled,
+ },
+ })
+ : (() => {
+ const rootScope = typeof window !== 'undefined' ? window : globalThis;
+ const registry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai',
+ }) || null;
+ return registry?.resolveSidepanelCapabilities
+ ? registry.resolveSidepanelCapabilities({
+ activeFlowId: typeof latestState !== 'undefined' ? latestState?.activeFlowId : '',
+ panelMode: typeof getSelectedPanelMode === 'function' ? getSelectedPanelMode() : (latestState?.panelMode || 'cpa'),
+ state: {
+ ...(typeof latestState !== 'undefined' ? latestState : {}),
+ phoneVerificationEnabled: phoneEnabled,
+ plusModeEnabled,
+ contributionMode: contributionModeEnabled,
+ },
+ })
+ : null;
+ })();
+ if (capabilityState && typeof capabilityState.canSelectPhoneSignup === 'boolean') {
+ return capabilityState.canSelectPhoneSignup;
+ }
return phoneEnabled && !plusModeEnabled && !contributionModeEnabled;
}
@@ -7295,22 +7464,68 @@ function updateSignupMethodUI(options = {}) {
}
}
});
- syncStepDefinitionsForMode(
- typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
- ? Boolean(inputPlusModeEnabled.checked)
- : Boolean(latestState?.plusModeEnabled),
- {
- plusPaymentMethod: getSelectedPlusPaymentMethod(latestState),
+ const stepDefinitionState = typeof resolveStepDefinitionCapabilityState === 'function'
+ ? resolveStepDefinitionCapabilityState({
+ ...(latestState || {}),
+ plusModeEnabled: typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
+ ? Boolean(inputPlusModeEnabled.checked)
+ : Boolean(latestState?.plusModeEnabled),
signupMethod: selectedMethod,
- }
- );
+ }, {
+ signupMethod: selectedMethod,
+ })
+ : {
+ plusModeEnabled: typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
+ ? Boolean(inputPlusModeEnabled.checked)
+ : Boolean(latestState?.plusModeEnabled),
+ signupMethod: selectedMethod,
+ };
+ syncStepDefinitionsForMode(stepDefinitionState.plusModeEnabled, {
+ plusPaymentMethod: getSelectedPlusPaymentMethod(latestState),
+ signupMethod: selectedMethod,
+ });
if (typeof syncSignupPhoneInputFromState === 'function') {
syncSignupPhoneInputFromState(latestState);
}
}
function updatePhoneVerificationSettingsUI() {
- const enabled = Boolean(inputPhoneVerificationEnabled?.checked);
+ const rawEnabled = Boolean(inputPhoneVerificationEnabled?.checked);
+ const rawPlusModeEnabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
+ ? Boolean(inputPlusModeEnabled.checked)
+ : Boolean(latestState?.plusModeEnabled);
+ const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function'
+ ? resolveCurrentSidepanelCapabilities({
+ panelMode: typeof getSelectedPanelMode === 'function' ? getSelectedPanelMode() : latestState?.panelMode,
+ signupMethod: typeof getSelectedSignupMethod === 'function' ? getSelectedSignupMethod() : latestState?.signupMethod,
+ state: {
+ ...(latestState || {}),
+ phoneVerificationEnabled: rawEnabled,
+ plusModeEnabled: rawPlusModeEnabled,
+ },
+ })
+ : (() => {
+ const rootScope = typeof window !== 'undefined' ? window : globalThis;
+ const registry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai',
+ }) || null;
+ return registry?.resolveSidepanelCapabilities
+ ? registry.resolveSidepanelCapabilities({
+ activeFlowId: latestState?.activeFlowId,
+ panelMode: typeof getSelectedPanelMode === 'function' ? getSelectedPanelMode() : (latestState?.panelMode || 'cpa'),
+ signupMethod: typeof getSelectedSignupMethod === 'function' ? getSelectedSignupMethod() : latestState?.signupMethod,
+ state: {
+ ...(latestState || {}),
+ phoneVerificationEnabled: rawEnabled,
+ plusModeEnabled: rawPlusModeEnabled,
+ },
+ })
+ : null;
+ })();
+ const canShowPhoneSettings = capabilityState
+ ? Boolean(capabilityState.canShowPhoneSettings)
+ : true;
+ const enabled = canShowPhoneSettings && rawEnabled;
const showSettings = enabled && phoneVerificationSectionExpanded;
const normalizeProvider = typeof normalizePhoneSmsProviderValue === 'function'
? normalizePhoneSmsProviderValue
@@ -7333,10 +7548,10 @@ function updatePhoneVerificationSettingsUI() {
const fiveSimProvider = provider === fiveSimProviderValue;
const nexSmsProvider = provider === nexSmsProviderValue;
if (rowPhoneVerificationEnabled) {
- rowPhoneVerificationEnabled.style.display = '';
+ rowPhoneVerificationEnabled.style.display = canShowPhoneSettings ? '' : 'none';
}
if (rowHeroSmsPlatform) {
- rowHeroSmsPlatform.style.display = '';
+ rowHeroSmsPlatform.style.display = canShowPhoneSettings ? '' : 'none';
}
updateSignupMethodUI();
if (btnTogglePhoneVerificationSection) {
@@ -7447,9 +7662,37 @@ function updatePlusModeUI() {
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
const gpcValue = typeof PLUS_PAYMENT_METHOD_GPC_HELPER !== 'undefined' ? PLUS_PAYMENT_METHOD_GPC_HELPER : 'gpc-helper';
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : paypalValue;
- const enabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
+ const rawEnabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
? Boolean(inputPlusModeEnabled.checked)
: false;
+ const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function'
+ ? resolveCurrentSidepanelCapabilities({
+ panelMode: typeof getSelectedPanelMode === 'function' ? getSelectedPanelMode() : latestState?.panelMode,
+ state: {
+ ...(latestState || {}),
+ plusModeEnabled: rawEnabled,
+ },
+ })
+ : (() => {
+ const rootScope = typeof window !== 'undefined' ? window : globalThis;
+ const registry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai',
+ }) || null;
+ return registry?.resolveSidepanelCapabilities
+ ? registry.resolveSidepanelCapabilities({
+ activeFlowId: latestState?.activeFlowId,
+ panelMode: typeof getSelectedPanelMode === 'function' ? getSelectedPanelMode() : (latestState?.panelMode || 'cpa'),
+ state: {
+ ...(latestState || {}),
+ plusModeEnabled: rawEnabled,
+ },
+ })
+ : null;
+ })();
+ const supportsPlusMode = capabilityState
+ ? Boolean(capabilityState.canShowPlusSettings)
+ : true;
+ const enabled = supportsPlusMode && rawEnabled;
const method = enabled ? getSelectedPlusPaymentMethod() : defaultMethod;
const gpcPhoneMode = normalizeGpcHelperPhoneModeValue(
typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode
@@ -7476,6 +7719,9 @@ function updatePlusModeUI() {
const canShowGpcModeSelector = gpcRowsVisible;
const localSmsControlsVisible = gpcRowsVisible && !isGpcAutoMode;
const effectiveLocalSmsEnabled = !isGpcAutoMode && localSmsEnabled;
+ if (typeof rowPlusMode !== 'undefined' && rowPlusMode) {
+ rowPlusMode.style.display = supportsPlusMode ? '' : 'none';
+ }
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) {
selectPlusPaymentMethod.value = method;
if (selectPlusPaymentMethod.style) {
@@ -7657,7 +7903,39 @@ function syncSignupPhoneInputFromState(state = latestState) {
const selectedMethod = typeof normalizeSignupMethod === 'function'
? normalizeSignupMethod(rawSignupMethod)
: (String(rawSignupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email');
- rowSignupPhone.style.display = phoneVerificationEnabled
+ const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function'
+ ? resolveCurrentSidepanelCapabilities({
+ panelMode: state?.panelMode || latestState?.panelMode,
+ signupMethod: selectedMethod,
+ state: {
+ ...(latestState || {}),
+ ...(state || {}),
+ phoneVerificationEnabled,
+ },
+ })
+ : (() => {
+ const rootScope = typeof window !== 'undefined' ? window : globalThis;
+ const registry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
+ defaultFlowId: typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai',
+ }) || null;
+ return registry?.resolveSidepanelCapabilities
+ ? registry.resolveSidepanelCapabilities({
+ activeFlowId: state?.activeFlowId || latestState?.activeFlowId,
+ panelMode: state?.panelMode || latestState?.panelMode,
+ signupMethod: selectedMethod,
+ state: {
+ ...(latestState || {}),
+ ...(state || {}),
+ phoneVerificationEnabled,
+ },
+ })
+ : null;
+ })();
+ const canShowPhoneSettings = capabilityState
+ ? Boolean(capabilityState.canShowPhoneSettings)
+ : true;
+ rowSignupPhone.style.display = canShowPhoneSettings
+ && phoneVerificationEnabled
&& (selectedMethod === 'phone' || Boolean(signupPhone) || Boolean(getSignupPhoneInputValue()) || signupPhoneInputDirty)
? ''
: 'none';
@@ -8285,9 +8563,17 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
function applySettingsState(state) {
if (typeof syncStepDefinitionsForMode === 'function') {
- syncStepDefinitionsForMode(Boolean(state?.plusModeEnabled), {
+ const stepDefinitionState = typeof resolveStepDefinitionCapabilityState === 'function'
+ ? resolveStepDefinitionCapabilityState(state, {
+ signupMethod: state?.signupMethod,
+ })
+ : {
+ plusModeEnabled: Boolean(state?.plusModeEnabled),
+ signupMethod: normalizeSignupMethod(state?.signupMethod || DEFAULT_SIGNUP_METHOD),
+ };
+ syncStepDefinitionsForMode(stepDefinitionState.plusModeEnabled, {
plusPaymentMethod: state?.plusPaymentMethod,
- signupMethod: state?.signupMethod,
+ signupMethod: stepDefinitionState.signupMethod,
});
}
const fallbackIpProxyService = '711proxy';
@@ -9776,6 +10062,30 @@ function updateMailProviderUI() {
const icloudHostPreferenceValue = typeof selectIcloudHostPreference !== 'undefined'
? selectIcloudHostPreference?.value
: latestState?.icloudHostPreference;
+ const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function'
+ ? resolveCurrentSidepanelCapabilities({
+ panelMode: typeof getSelectedPanelMode === 'function' ? getSelectedPanelMode() : latestState?.panelMode,
+ state: latestState || {},
+ })
+ : null;
+ const canShowLuckmail = capabilityState
+ ? Boolean(capabilityState.canShowLuckmail)
+ : true;
+ const mailProviderOptions = Array.from(selectMailProvider?.options || []);
+ mailProviderOptions.forEach((option) => {
+ if (!option) {
+ return;
+ }
+ if (String(option.value || '').trim().toLowerCase() === 'luckmail-api') {
+ option.hidden = !canShowLuckmail;
+ }
+ });
+ if (!canShowLuckmail && String(selectMailProvider?.value || '').trim().toLowerCase() === 'luckmail-api') {
+ const fallbackOption = mailProviderOptions.find((option) => option && !option.hidden);
+ if (fallbackOption) {
+ selectMailProvider.value = String(fallbackOption.value || '').trim();
+ }
+ }
const use2925 = selectMailProvider.value === '2925';
const useGmail = selectMailProvider.value === GMAIL_PROVIDER;
const useMail2925 = selectMailProvider.value === '2925';
@@ -9806,7 +10116,7 @@ function updateMailProviderUI() {
const useGeneratedAlias = usesGeneratedAliasMailProvider(selectMailProvider.value, mail2925Mode, selectedGenerator);
const useInbucket = selectMailProvider.value === 'inbucket';
const useHotmail = selectMailProvider.value === 'hotmail-api';
- const useLuckmail = isLuckmailProvider();
+ const useLuckmail = canShowLuckmail && isLuckmailProvider();
const useCustomEmail = isCustomMailProvider();
const useCustomMailProviderPool = useCustomEmail && usesCustomMailProviderPool(selectMailProvider.value);
const useIcloudProvider = isIcloudMailProvider();
@@ -10228,7 +10538,39 @@ async function handleDeleteSub2ApiGroup(groupName) {
}
function updatePanelModeUI() {
- const panelMode = getSelectedPanelMode();
+ const rawPanelMode = normalizePanelMode(selectPanelMode?.value || latestState?.panelMode || 'cpa');
+ const capabilityState = typeof resolveCurrentSidepanelCapabilities === 'function'
+ ? resolveCurrentSidepanelCapabilities({
+ panelMode: rawPanelMode,
+ state: {
+ ...(latestState || {}),
+ panelMode: rawPanelMode,
+ },
+ })
+ : null;
+ const supportedPanelModes = Array.isArray(capabilityState?.supportedPanelModes)
+ ? capabilityState.supportedPanelModes
+ : [];
+ if (selectPanelMode?.options && supportedPanelModes.length) {
+ Array.from(selectPanelMode.options).forEach((option) => {
+ if (!option) {
+ return;
+ }
+ const optionMode = normalizePanelMode(option.value || '');
+ const enabled = supportedPanelModes.includes(optionMode);
+ option.disabled = !enabled;
+ option.hidden = !enabled;
+ });
+ } else if (selectPanelMode?.options) {
+ Array.from(selectPanelMode.options).forEach((option) => {
+ if (!option) {
+ return;
+ }
+ option.disabled = false;
+ option.hidden = false;
+ });
+ }
+ const panelMode = capabilityState?.effectivePanelMode || capabilityState?.panelMode || getSelectedPanelMode();
if (selectPanelMode) {
selectPanelMode.value = panelMode;
}
@@ -11799,7 +12141,22 @@ inputPassword.addEventListener('blur', () => {
inputPlusModeEnabled?.addEventListener('change', () => {
updatePlusModeUI();
updateSignupMethodUI({ notify: true });
- syncStepDefinitionsForMode(Boolean(inputPlusModeEnabled.checked), getSelectedPlusPaymentMethod(), { render: true });
+ const stepDefinitionState = typeof resolveStepDefinitionCapabilityState === 'function'
+ ? resolveStepDefinitionCapabilityState({
+ ...(latestState || {}),
+ plusModeEnabled: Boolean(inputPlusModeEnabled.checked),
+ signupMethod: getSelectedSignupMethod(),
+ }, {
+ signupMethod: getSelectedSignupMethod(),
+ })
+ : {
+ plusModeEnabled: Boolean(inputPlusModeEnabled.checked),
+ signupMethod: getSelectedSignupMethod(),
+ };
+ syncStepDefinitionsForMode(stepDefinitionState.plusModeEnabled, getSelectedPlusPaymentMethod(), {
+ render: true,
+ signupMethod: stepDefinitionState.signupMethod,
+ });
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
@@ -11811,7 +12168,22 @@ inputOperationDelayEnabled?.addEventListener('change', () => {
selectPlusPaymentMethod?.addEventListener('change', () => {
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(selectPlusPaymentMethod.value);
updatePlusModeUI();
- syncStepDefinitionsForMode(Boolean(inputPlusModeEnabled?.checked), selectPlusPaymentMethod.value, { render: true });
+ const stepDefinitionState = typeof resolveStepDefinitionCapabilityState === 'function'
+ ? resolveStepDefinitionCapabilityState({
+ ...(latestState || {}),
+ plusModeEnabled: Boolean(inputPlusModeEnabled?.checked),
+ signupMethod: getSelectedSignupMethod(),
+ }, {
+ signupMethod: getSelectedSignupMethod(),
+ })
+ : {
+ plusModeEnabled: Boolean(inputPlusModeEnabled?.checked),
+ signupMethod: getSelectedSignupMethod(),
+ };
+ syncStepDefinitionsForMode(stepDefinitionState.plusModeEnabled, selectPlusPaymentMethod.value, {
+ render: true,
+ signupMethod: stepDefinitionState.signupMethod,
+ });
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
@@ -11873,9 +12245,22 @@ btnGpcHelperBalance?.addEventListener('click', async () => {
selectPlusPaymentMethod?.addEventListener('change', () => {
updatePlusModeUI();
- syncStepDefinitionsForMode(Boolean(inputPlusModeEnabled?.checked), {
+ const stepDefinitionState = typeof resolveStepDefinitionCapabilityState === 'function'
+ ? resolveStepDefinitionCapabilityState({
+ ...(latestState || {}),
+ plusModeEnabled: Boolean(inputPlusModeEnabled?.checked),
+ signupMethod: getSelectedSignupMethod(),
+ }, {
+ signupMethod: getSelectedSignupMethod(),
+ })
+ : {
+ plusModeEnabled: Boolean(inputPlusModeEnabled?.checked),
+ signupMethod: getSelectedSignupMethod(),
+ };
+ syncStepDefinitionsForMode(stepDefinitionState.plusModeEnabled, {
render: true,
plusPaymentMethod: selectPlusPaymentMethod.value,
+ signupMethod: stepDefinitionState.signupMethod,
});
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
@@ -12011,7 +12396,9 @@ checkboxAutoDeleteIcloud?.addEventListener('change', () => {
selectPanelMode.addEventListener('change', async () => {
const previousPanelMode = normalizePanelMode(latestState?.panelMode || 'cpa');
- const nextPanelMode = normalizePanelMode(selectPanelMode.value);
+ const rawNextPanelMode = normalizePanelMode(selectPanelMode.value);
+ selectPanelMode.value = rawNextPanelMode;
+ const nextPanelMode = getSelectedPanelMode();
selectPanelMode.value = nextPanelMode;
const confirmed = await confirmCpaPhoneSignupIfNeeded({
signupMethod: getSelectedSignupMethod(),
@@ -13845,10 +14232,21 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|| message.payload.gopayHelperOtpChannel !== undefined
|| message.payload.gopayHelperLocalSmsHelperEnabled !== undefined
) {
+ const stepDefinitionState = typeof resolveStepDefinitionCapabilityState === 'function'
+ ? resolveStepDefinitionCapabilityState(latestState, {
+ signupMethod: latestState?.signupMethod,
+ })
+ : {
+ plusModeEnabled: Boolean(latestState?.plusModeEnabled),
+ signupMethod: normalizeSignupMethod(latestState?.signupMethod || DEFAULT_SIGNUP_METHOD),
+ };
syncStepDefinitionsForMode(
- Boolean(latestState?.plusModeEnabled),
+ stepDefinitionState.plusModeEnabled,
latestState?.plusPaymentMethod,
- { render: true }
+ {
+ render: true,
+ signupMethod: stepDefinitionState.signupMethod,
+ }
);
updatePlusModeUI();
updateSignupMethodUI({ notify: true });
diff --git a/tests/background-luckmail.test.js b/tests/background-luckmail.test.js
index 669cd28..e9deb50 100644
--- a/tests/background-luckmail.test.js
+++ b/tests/background-luckmail.test.js
@@ -731,6 +731,9 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
'async function getPersistedAliasState() {',
' return {};',
'}',
+ 'function buildStatePatchWithRuntimeState(_currentState, updates) {',
+ ' return updates;',
+ '}',
'const chrome = {',
' storage: {',
' session: {',
diff --git a/tests/background-mail-rule-registry-module.test.js b/tests/background-mail-rule-registry-module.test.js
new file mode 100644
index 0000000..d6cc6c0
--- /dev/null
+++ b/tests/background-mail-rule-registry-module.test.js
@@ -0,0 +1,93 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+test('background imports mail rule registry and OpenAI mail rules modules', () => {
+ const source = fs.readFileSync('background.js', 'utf8');
+ assert.match(source, /background\/mail-rule-registry\.js/);
+ assert.match(source, /flows\/openai\/mail-rules\.js/);
+});
+
+test('mail rule registry exposes canonical OpenAI verification poll payloads', () => {
+ const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
+ const openAiSource = fs.readFileSync('flows/openai/mail-rules.js', 'utf8');
+ const registryApi = new Function('self', `${registrySource}; return self.MultiPageBackgroundMailRuleRegistry;`)({});
+ const openAiApi = new Function('self', `${openAiSource}; return self.MultiPageOpenAiMailRules;`)({});
+
+ const openAiMailRules = openAiApi.createOpenAiMailRules({
+ getHotmailVerificationRequestTimestamp: (step) => (step === 4 ? 123 : 456),
+ MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
+ MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
+ });
+ const registry = registryApi.createMailRuleRegistry({
+ defaultFlowId: 'openai',
+ flowBuilders: {
+ openai: openAiMailRules,
+ },
+ });
+
+ assert.deepEqual(
+ registry.buildVerificationPollPayload(
+ 4,
+ {
+ activeFlowId: 'openai',
+ email: 'user@example.com',
+ mailProvider: '2925',
+ mail2925Mode: 'receive',
+ },
+ { excludeCodes: ['111111'] }
+ ),
+ {
+ flowId: 'openai',
+ ruleId: 'openai-signup-code',
+ step: 4,
+ artifactType: 'code',
+ filterAfterTimestamp: 0,
+ senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
+ subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
+ targetEmail: 'user@example.com',
+ mail2925MatchTargetEmail: true,
+ maxAttempts: 15,
+ intervalMs: 15000,
+ excludeCodes: ['111111'],
+ }
+ );
+
+ assert.deepEqual(
+ registry.buildVerificationPollPayload(8, {
+ activeFlowId: 'openai',
+ email: 'user@example.com',
+ step8VerificationTargetEmail: 'login@example.com',
+ }),
+ {
+ flowId: 'openai',
+ ruleId: 'openai-login-code',
+ step: 8,
+ artifactType: 'code',
+ filterAfterTimestamp: 456,
+ senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
+ subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
+ targetEmail: 'login@example.com',
+ mail2925MatchTargetEmail: false,
+ maxAttempts: 5,
+ intervalMs: 3000,
+ }
+ );
+});
+
+test('mail rule registry rejects unknown active flow ids instead of silently using OpenAI rules', () => {
+ const registrySource = fs.readFileSync('background/mail-rule-registry.js', 'utf8');
+ const registryApi = new Function('self', `${registrySource}; return self.MultiPageBackgroundMailRuleRegistry;`)({});
+ const registry = registryApi.createMailRuleRegistry({
+ defaultFlowId: 'openai',
+ flowBuilders: {},
+ });
+
+ assert.throws(
+ () => registry.buildVerificationPollPayload(4, {
+ activeFlowId: 'site-a',
+ email: 'user@example.com',
+ }),
+ /未找到 flow=site-a 的邮件轮询规则构造器/
+ );
+});
diff --git a/tests/background-message-router-module.test.js b/tests/background-message-router-module.test.js
index fef97fd..f1a2da0 100644
--- a/tests/background-message-router-module.test.js
+++ b/tests/background-message-router-module.test.js
@@ -162,3 +162,39 @@ test('SAVE_SETTING broadcasts operation delay setting without background success
assert.deepStrictEqual(broadcasts.at(-1), { operationDelayEnabled: false });
assert.equal(logs.length, 0);
});
+
+test('SAVE_SETTING re-resolves signup method when panel mode changes', async () => {
+ const source = fs.readFileSync('background/message-router.js', 'utf8');
+ const globalScope = { console };
+ const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
+ let state = {
+ signupMethod: 'phone',
+ phoneVerificationEnabled: true,
+ plusModeEnabled: false,
+ panelMode: 'sub2api',
+ };
+
+ const router = api.createMessageRouter({
+ addLog: async () => {},
+ buildLuckmailSessionSettingsPayload: () => ({}),
+ buildPersistentSettingsPayload: (input = {}) => Object.prototype.hasOwnProperty.call(input, 'panelMode')
+ ? { panelMode: input.panelMode }
+ : {},
+ broadcastDataUpdate: () => {},
+ getState: async () => ({ ...state }),
+ resolveSignupMethod: (nextState = {}) => nextState.panelMode === 'cpa' ? 'email' : 'phone',
+ setPersistentSettings: async () => {},
+ setState: async (updates) => {
+ state = { ...state, ...updates };
+ },
+ });
+
+ const response = await router.handleMessage({
+ type: 'SAVE_SETTING',
+ payload: { panelMode: 'cpa' },
+ });
+
+ assert.equal(response.ok, true);
+ assert.equal(state.panelMode, 'cpa');
+ assert.equal(state.signupMethod, 'email');
+});
diff --git a/tests/background-navigation-utils-module.test.js b/tests/background-navigation-utils-module.test.js
index eee918b..cce18ed 100644
--- a/tests/background-navigation-utils-module.test.js
+++ b/tests/background-navigation-utils-module.test.js
@@ -29,6 +29,7 @@ test('navigation utils recognize signup password pages for email and phone signu
assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/create-account/password'), true);
assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/log-in/password'), true);
assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/log-in'), false);
+ assert.equal(utils.isSignupEntryHost('www.chatgpt.com'), true);
});
test('navigation utils treat 126 mail hosts as part of the shared NetEase mail family', () => {
diff --git a/tests/background-runtime-state-module.test.js b/tests/background-runtime-state-module.test.js
new file mode 100644
index 0000000..df67866
--- /dev/null
+++ b/tests/background-runtime-state-module.test.js
@@ -0,0 +1,163 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+function loadRuntimeStateApi() {
+ const source = fs.readFileSync('background/runtime-state.js', 'utf8');
+ const globalScope = {};
+ return new Function('self', `${source}; return self.MultiPageBackgroundRuntimeState;`)(globalScope);
+}
+
+test('background imports runtime-state module and wires state view helpers', () => {
+ const source = fs.readFileSync('background.js', 'utf8');
+ assert.match(source, /background\/runtime-state\.js/);
+ assert.match(source, /createRuntimeStateHelpers/);
+ assert.match(source, /buildStateViewWithRuntimeState/);
+ assert.match(source, /buildStatePatchWithRuntimeState/);
+ assert.match(source, /runtimeState:/);
+});
+
+test('runtime-state module exposes a factory', () => {
+ const api = loadRuntimeStateApi();
+ assert.equal(typeof api?.createRuntimeStateHelpers, 'function');
+});
+
+test('runtime-state view derives canonical flow metadata from legacy step state', () => {
+ const api = loadRuntimeStateApi();
+ const helpers = api.createRuntimeStateHelpers({
+ DEFAULT_ACTIVE_FLOW_ID: 'openai',
+ defaultStepStatuses: {
+ 1: 'pending',
+ 2: 'pending',
+ 10: 'pending',
+ },
+ getStepDefinitionForState(step) {
+ return {
+ 1: { id: 1, key: 'open-chatgpt' },
+ 2: { id: 2, key: 'submit-signup-email' },
+ 10: { id: 10, key: 'oauth-login' },
+ }[Number(step)] || null;
+ },
+ });
+
+ const view = helpers.buildStateView({
+ currentStep: 2,
+ stepStatuses: {
+ 1: 'completed',
+ 2: 'running',
+ },
+ oauthUrl: 'https://auth.example.com/start',
+ plusCheckoutTabId: 88,
+ currentPhoneActivation: {
+ activationId: 'active-1',
+ phoneNumber: '+447700900123',
+ },
+ tabRegistry: {
+ 'signup-page': { tabId: 12 },
+ },
+ sourceLastUrls: {
+ 'signup-page': 'https://auth.example.com/start',
+ },
+ flowStartTime: 12345,
+ });
+
+ assert.equal(view.activeFlowId, 'openai');
+ assert.equal(view.currentNodeId, 'submit-signup-email');
+ assert.deepStrictEqual(view.legacyStepCompat, {
+ currentStep: 2,
+ stepStatuses: {
+ 1: 'completed',
+ 2: 'running',
+ 10: 'pending',
+ },
+ });
+ assert.deepStrictEqual(view.nodeStatuses, {
+ 'open-chatgpt': 'completed',
+ 'submit-signup-email': 'running',
+ 'oauth-login': 'pending',
+ });
+ assert.equal(view.runtimeState.flowState.openai.auth.oauthUrl, 'https://auth.example.com/start');
+ assert.equal(view.runtimeState.flowState.openai.plus.plusCheckoutTabId, 88);
+ assert.deepStrictEqual(view.runtimeState.flowState.openai.phoneVerification.currentPhoneActivation, {
+ activationId: 'active-1',
+ phoneNumber: '+447700900123',
+ });
+ assert.deepStrictEqual(view.sharedState, {
+ tabRegistry: {
+ 'signup-page': { tabId: 12 },
+ },
+ sourceLastUrls: {
+ 'signup-page': 'https://auth.example.com/start',
+ },
+ flowStartTime: 12345,
+ });
+});
+
+test('runtime-state patch accepts nested flow updates while keeping legacy compatibility fields in sync', () => {
+ const api = loadRuntimeStateApi();
+ const helpers = api.createRuntimeStateHelpers({
+ DEFAULT_ACTIVE_FLOW_ID: 'openai',
+ defaultStepStatuses: {
+ 1: 'pending',
+ 2: 'pending',
+ 10: 'pending',
+ },
+ getStepDefinitionForState(step) {
+ return {
+ 1: { id: 1, key: 'open-chatgpt' },
+ 2: { id: 2, key: 'submit-signup-email' },
+ 10: { id: 10, key: 'oauth-login' },
+ }[Number(step)] || null;
+ },
+ });
+
+ const patch = helpers.buildSessionStatePatch({
+ currentStep: 1,
+ stepStatuses: {
+ 1: 'running',
+ 2: 'pending',
+ 10: 'pending',
+ },
+ oauthUrl: 'https://old.example.com/start',
+ }, {
+ runtimeState: {
+ activeRunId: 'run-001',
+ flowState: {
+ openai: {
+ auth: {
+ oauthUrl: 'https://new.example.com/start',
+ },
+ plus: {
+ plusCheckoutTabId: 99,
+ },
+ },
+ },
+ legacyStepCompat: {
+ currentStep: 10,
+ stepStatuses: {
+ 1: 'completed',
+ 10: 'running',
+ },
+ },
+ },
+ });
+
+ assert.equal(patch.activeFlowId, 'openai');
+ assert.equal(patch.activeRunId, 'run-001');
+ assert.equal(patch.currentNodeId, 'oauth-login');
+ assert.equal(patch.oauthUrl, 'https://new.example.com/start');
+ assert.equal(patch.plusCheckoutTabId, 99);
+ assert.equal(patch.currentStep, 10);
+ assert.deepStrictEqual(patch.stepStatuses, {
+ 1: 'completed',
+ 2: 'pending',
+ 10: 'running',
+ });
+ assert.deepStrictEqual(patch.nodeStatuses, {
+ 'open-chatgpt': 'completed',
+ 'submit-signup-email': 'pending',
+ 'oauth-login': 'running',
+ });
+ assert.equal(patch.runtimeState.flowState.openai.auth.oauthUrl, 'https://new.example.com/start');
+ assert.equal(patch.runtimeState.flowState.openai.plus.plusCheckoutTabId, 99);
+});
diff --git a/tests/background-signup-method-settings.test.js b/tests/background-signup-method-settings.test.js
index 25d61c1..3af827a 100644
--- a/tests/background-signup-method-settings.test.js
+++ b/tests/background-signup-method-settings.test.js
@@ -95,6 +95,51 @@ return {
assert.equal(api.logs.some((entry) => /固定为邮箱注册/.test(entry.message)), true);
});
+test('signup method resolution respects the shared flow capability registry when available', () => {
+ const api = new Function(`
+const self = {
+ MultiPageFlowCapabilities: {
+ createFlowCapabilityRegistry() {
+ return {
+ resolveSidepanelCapabilities({ state = {}, signupMethod = 'email' } = {}) {
+ const phoneAllowed = String(state?.activeFlowId || '').trim().toLowerCase() === 'openai';
+ return {
+ canUsePhoneSignup: phoneAllowed,
+ effectiveSignupMethod: signupMethod === 'phone' && phoneAllowed ? 'phone' : 'email',
+ };
+ },
+ };
+ },
+ },
+};
+const SIGNUP_METHOD_EMAIL = 'email';
+const SIGNUP_METHOD_PHONE = 'phone';
+${extractFunction('normalizeSignupMethod')}
+${extractFunction('canUsePhoneSignup')}
+${extractFunction('resolveSignupMethod')}
+return {
+ canUsePhoneSignup,
+ resolveSignupMethod,
+};
+`)();
+
+ assert.equal(api.canUsePhoneSignup({
+ activeFlowId: 'site-a',
+ phoneVerificationEnabled: true,
+ signupMethod: 'phone',
+ }), false);
+ assert.equal(api.resolveSignupMethod({
+ activeFlowId: 'site-a',
+ phoneVerificationEnabled: true,
+ signupMethod: 'phone',
+ }), 'email');
+ assert.equal(api.resolveSignupMethod({
+ activeFlowId: 'openai',
+ phoneVerificationEnabled: true,
+ signupMethod: 'phone',
+ }), 'phone');
+});
+
test('background step definitions resolve titles from the frozen signup method', () => {
const api = new Function(`
const captured = [];
diff --git a/tests/background-tab-runtime-module.test.js b/tests/background-tab-runtime-module.test.js
index b1186bd..6a31394 100644
--- a/tests/background-tab-runtime-module.test.js
+++ b/tests/background-tab-runtime-module.test.js
@@ -16,6 +16,76 @@ test('tab runtime module exposes a factory', () => {
assert.equal(typeof api?.createTabRuntime, 'function');
});
+test('tab runtime accepts canonical openai-auth readiness for queued signup-page commands', async () => {
+ const runtimeSource = fs.readFileSync('background/tab-runtime.js', 'utf8');
+ const registrySource = fs.readFileSync('shared/source-registry.js', 'utf8');
+ const runtimeApi = new Function('self', `${runtimeSource}; return self.MultiPageBackgroundTabRuntime;`)({});
+ const registryApi = new Function('self', `${registrySource}; return self.MultiPageSourceRegistry;`)({});
+ const sourceRegistry = registryApi.createSourceRegistry();
+
+ const sentMessages = [];
+ let currentState = {
+ tabRegistry: {
+ 'signup-page': { tabId: 91, ready: true },
+ },
+ sourceLastUrls: {
+ 'signup-page': 'https://auth.openai.com/authorize',
+ },
+ };
+
+ const runtime = runtimeApi.createTabRuntime({
+ LOG_PREFIX: '[test]',
+ addLog: async () => {},
+ chrome: {
+ tabs: {
+ get: async (tabId) => ({
+ id: tabId,
+ windowId: 1,
+ url: 'https://auth.openai.com/authorize',
+ status: 'complete',
+ }),
+ query: async () => [],
+ sendMessage: async (tabId, message) => {
+ if (message.type === 'PING') {
+ return { ok: true, source: 'openai-auth' };
+ }
+ sentMessages.push({ tabId, message });
+ return { ok: true };
+ },
+ },
+ },
+ getSourceLabel: (source) => source || 'unknown',
+ getState: async () => currentState,
+ matchesSourceUrlFamily: (source, candidateUrl, referenceUrl) => (
+ sourceRegistry.matchesSourceUrlFamily(source, candidateUrl, referenceUrl)
+ ),
+ setState: async (updates) => {
+ currentState = { ...currentState, ...updates };
+ },
+ sourceRegistry,
+ throwIfStopped: () => {},
+ });
+
+ assert.equal(await runtime.getTabId('openai-auth'), 91);
+
+ currentState = {
+ tabRegistry: {},
+ sourceLastUrls: {},
+ };
+
+ const queued = runtime.queueCommand('signup-page', { type: 'STEP2_TEST' }, 1000);
+ runtime.flushCommand('openai-auth', 55);
+ await assert.doesNotReject(queued);
+ assert.deepEqual(sentMessages, [{ tabId: 55, message: { type: 'STEP2_TEST' } }]);
+
+ await runtime.ensureContentScriptReadyOnTab('signup-page', 77, {
+ timeoutMs: 100,
+ });
+
+ assert.deepEqual(currentState.tabRegistry['openai-auth'], { tabId: 77, ready: true, windowId: 1 });
+ assert.equal(Object.prototype.hasOwnProperty.call(currentState.tabRegistry, 'signup-page'), false);
+});
+
test('tab runtime caps per-attempt response timeout to the remaining resilient timeout budget', () => {
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
const globalScope = {};
diff --git a/tests/content-utils.test.js b/tests/content-utils.test.js
index 8703fdd..44353c8 100644
--- a/tests/content-utils.test.js
+++ b/tests/content-utils.test.js
@@ -81,6 +81,38 @@ return { detectScriptSource };
);
});
+test('detectScriptSource maps OpenAI auth hosts to canonical openai-auth source', () => {
+ const bundle = [extractFunction('detectScriptSource')].join('\n');
+ const api = new Function(`
+${bundle}
+return { detectScriptSource };
+`)();
+
+ assert.equal(
+ api.detectScriptSource({
+ url: 'https://auth.openai.com/create-account',
+ hostname: 'auth.openai.com',
+ }),
+ 'openai-auth'
+ );
+});
+
+test('detectScriptSource returns unknown-source for unrecognized pages', () => {
+ const bundle = [extractFunction('detectScriptSource')].join('\n');
+ const api = new Function(`
+${bundle}
+return { detectScriptSource };
+`)();
+
+ assert.equal(
+ api.detectScriptSource({
+ url: 'https://example.com/',
+ hostname: 'example.com',
+ }),
+ 'unknown-source'
+ );
+});
+
test('shouldReportReadyForFrame suppresses noisy plus checkout child frame ready logs', () => {
const bundle = [extractFunction('shouldReportReadyForFrame')].join('\n');
const api = new Function(`
@@ -91,6 +123,8 @@ return { shouldReportReadyForFrame };
assert.equal(api.shouldReportReadyForFrame('plus-checkout', true), false);
assert.equal(api.shouldReportReadyForFrame('plus-checkout', false), true);
assert.equal(api.shouldReportReadyForFrame('paypal-flow', true), true);
+ assert.equal(api.shouldReportReadyForFrame('unknown-source', false), true);
+ assert.equal(api.shouldReportReadyForFrame('unknown-source', true), false);
});
test('getRuntimeScriptSource follows injected source overrides after utils is already loaded', () => {
diff --git a/tests/flow-capabilities-module.test.js b/tests/flow-capabilities-module.test.js
new file mode 100644
index 0000000..e6d8125
--- /dev/null
+++ b/tests/flow-capabilities-module.test.js
@@ -0,0 +1,72 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+const source = fs.readFileSync('shared/flow-capabilities.js', 'utf8');
+
+function loadApi() {
+ const scope = {};
+ return new Function('self', `${source}; return self.MultiPageFlowCapabilities;`)(scope);
+}
+
+test('flow capability registry keeps OpenAI phone signup available only when runtime locks allow it', () => {
+ const api = loadApi();
+ const registry = api.createFlowCapabilityRegistry();
+
+ const enabledState = registry.resolveSidepanelCapabilities({
+ state: {
+ activeFlowId: 'openai',
+ panelMode: 'cpa',
+ phoneVerificationEnabled: true,
+ plusModeEnabled: false,
+ contributionMode: false,
+ signupMethod: 'phone',
+ },
+ });
+
+ assert.equal(enabledState.canUsePhoneSignup, true);
+ assert.equal(enabledState.effectiveSignupMethod, 'phone');
+ assert.equal(enabledState.shouldWarnCpaPhoneSignup, true);
+ assert.deepEqual(enabledState.effectiveSignupMethods, ['email', 'phone']);
+
+ const plusLockedState = registry.resolveSidepanelCapabilities({
+ state: {
+ activeFlowId: 'openai',
+ panelMode: 'sub2api',
+ phoneVerificationEnabled: true,
+ plusModeEnabled: true,
+ contributionMode: false,
+ signupMethod: 'phone',
+ },
+ });
+
+ assert.equal(plusLockedState.canUsePhoneSignup, false);
+ assert.equal(plusLockedState.effectiveSignupMethod, 'email');
+ assert.equal(plusLockedState.shouldWarnCpaPhoneSignup, false);
+ assert.deepEqual(plusLockedState.effectiveSignupMethods, ['email']);
+});
+
+test('flow capability registry defaults unknown flows to minimal non-phone capabilities', () => {
+ const api = loadApi();
+ const registry = api.createFlowCapabilityRegistry();
+
+ const capabilityState = registry.resolveSidepanelCapabilities({
+ state: {
+ activeFlowId: 'site-a',
+ panelMode: 'codex2api',
+ phoneVerificationEnabled: true,
+ plusModeEnabled: true,
+ contributionMode: true,
+ signupMethod: 'phone',
+ },
+ });
+
+ assert.equal(capabilityState.activeFlowId, 'site-a');
+ assert.equal(capabilityState.canShowPhoneSettings, false);
+ assert.equal(capabilityState.canShowPlusSettings, false);
+ assert.equal(capabilityState.canShowLuckmail, false);
+ assert.equal(capabilityState.canUsePhoneSignup, false);
+ assert.equal(capabilityState.effectiveSignupMethod, 'email');
+ assert.equal(capabilityState.panelMode, 'codex2api');
+ assert.deepEqual(capabilityState.supportedPanelModes, []);
+});
diff --git a/tests/sidepanel-phone-verification-settings.test.js b/tests/sidepanel-phone-verification-settings.test.js
index 6488369..774983e 100644
--- a/tests/sidepanel-phone-verification-settings.test.js
+++ b/tests/sidepanel-phone-verification-settings.test.js
@@ -294,6 +294,58 @@ return {
assert.equal(api.shouldWarnCpaPhoneSignup('phone', 'cpa'), false);
});
+test('sidepanel phone signup gating can follow the shared flow capability registry', () => {
+ const bundle = [
+ extractFunction('normalizeSignupMethod'),
+ extractFunction('normalizePanelMode'),
+ extractFunction('canSelectPhoneSignupMethod'),
+ extractFunction('shouldWarnCpaPhoneSignup'),
+ ].join('\n');
+
+ const api = new Function(`
+const window = {
+ MultiPageFlowCapabilities: {
+ createFlowCapabilityRegistry() {
+ return {
+ resolveSidepanelCapabilities({ state = {}, panelMode = 'cpa', signupMethod = 'email' } = {}) {
+ const phoneAllowed = String(state?.activeFlowId || '').trim().toLowerCase() === 'openai';
+ return {
+ canSelectPhoneSignup: phoneAllowed,
+ shouldWarnCpaPhoneSignup: phoneAllowed && signupMethod === 'phone' && panelMode === 'cpa',
+ };
+ },
+ };
+ },
+ },
+};
+let latestState = {
+ activeFlowId: 'site-a',
+ contributionMode: false,
+ panelMode: 'cpa',
+};
+const inputPhoneVerificationEnabled = { checked: true };
+const inputPlusModeEnabled = { checked: false };
+function getSelectedPanelMode() { return 'cpa'; }
+function getSelectedSignupMethod() { return 'phone'; }
+function isCpaPhoneSignupPromptDismissed() { return false; }
+${bundle}
+return {
+ canSelectPhoneSignupMethod,
+ shouldWarnCpaPhoneSignup,
+ setFlow(flowId) {
+ latestState.activeFlowId = flowId;
+ },
+};
+`)();
+
+ assert.equal(api.canSelectPhoneSignupMethod(), false);
+ assert.equal(api.shouldWarnCpaPhoneSignup('phone', 'cpa'), false);
+
+ api.setFlow('openai');
+ assert.equal(api.canSelectPhoneSignupMethod(), true);
+ assert.equal(api.shouldWarnCpaPhoneSignup('phone', 'cpa'), true);
+});
+
test('manual step 3 uses phone identity without requiring registration email', () => {
const api = new Function(`
let latestState = { signupMethod: 'phone', phoneVerificationEnabled: true, signupPhoneNumber: '+441111111111', accountIdentifierType: 'phone', accountIdentifier: '+441111111111' };
diff --git a/tests/sidepanel-plus-payment-method.test.js b/tests/sidepanel-plus-payment-method.test.js
index ded1b45..1a936be 100644
--- a/tests/sidepanel-plus-payment-method.test.js
+++ b/tests/sidepanel-plus-payment-method.test.js
@@ -124,8 +124,8 @@ test('sidepanel signup method UI syncs shared step definitions with the selected
test('sidepanel applies restored signup method when rebuilding shared step definitions on load', () => {
const source = extractFunction('applySettingsState');
- assert.match(source, /syncStepDefinitionsForMode\(Boolean\(state\?\.plusModeEnabled\),\s*\{/);
- assert.match(source, /signupMethod:\s*state\?\.signupMethod/);
+ assert.match(source, /resolveStepDefinitionCapabilityState\(state/);
+ assert.match(source, /signupMethod:\s*stepDefinitionState\.signupMethod/);
});
test('sidepanel Plus UI hides PayPal account selector while GoPay is selected', () => {
@@ -165,6 +165,62 @@ return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount };
assert.equal(api.rowPayPalAccount.style.display, '');
});
+test('sidepanel Plus UI can hide Plus controls when the shared flow capability registry disables them', () => {
+ const bundle = [
+ extractFunction('normalizePlusPaymentMethod'),
+ extractFunction('getSelectedPlusPaymentMethod'),
+ extractFunction('normalizeGpcHelperPhoneModeValue'),
+ extractFunction('getGpcHelperAutoModeEnabled'),
+ extractFunction('normalizeGpcAutoModePermissionValue'),
+ extractFunction('getGpcAutoModePermissionFromPayload'),
+ extractFunction('shouldPreserveSelectedGpcAutoMode'),
+ extractFunction('hasGpcAutoModePermissionField'),
+ extractFunction('isGpcAutoModePermissionDenied'),
+ extractFunction('normalizeGpcOtpChannelValue'),
+ extractFunction('updatePlusModeUI'),
+ ].join('\n');
+
+ const api = new Function(`
+const window = {
+ MultiPageFlowCapabilities: {
+ createFlowCapabilityRegistry() {
+ return {
+ resolveSidepanelCapabilities() {
+ return {
+ canShowPlusSettings: false,
+ runtimeLocks: { plusModeEnabled: false },
+ };
+ },
+ };
+ },
+ },
+};
+let latestState = { plusPaymentMethod: 'paypal' };
+const inputPlusModeEnabled = { checked: true };
+const rowPlusMode = { style: { display: '' } };
+const selectPlusPaymentMethod = { value: 'paypal', style: { display: '' } };
+const rowPlusPaymentMethod = { style: { display: '' } };
+const rowPayPalAccount = { style: { display: '' } };
+const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
+const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
+${bundle}
+return {
+ rowPlusMode,
+ rowPlusPaymentMethod,
+ rowPayPalAccount,
+ selectPlusPaymentMethod,
+ updatePlusModeUI,
+};
+`)();
+
+ api.updatePlusModeUI();
+
+ assert.equal(api.rowPlusMode.style.display, 'none');
+ assert.equal(api.rowPlusPaymentMethod.style.display, 'none');
+ assert.equal(api.rowPayPalAccount.style.display, 'none');
+ assert.equal(api.selectPlusPaymentMethod.style.display, 'none');
+});
+
test('sidepanel step definitions keep GPC helper mode distinct', () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
diff --git a/tests/source-registry-module.test.js b/tests/source-registry-module.test.js
new file mode 100644
index 0000000..ae6d47c
--- /dev/null
+++ b/tests/source-registry-module.test.js
@@ -0,0 +1,56 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+test('background imports shared source registry module', () => {
+ const source = fs.readFileSync('background.js', 'utf8');
+ assert.match(source, /shared\/source-registry\.js/);
+});
+
+test('manifest loads shared source registry before content utils in static bundles', () => {
+ const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
+ for (const entry of manifest.content_scripts || []) {
+ const scripts = Array.isArray(entry.js) ? entry.js : [];
+ if (!scripts.includes('content/utils.js')) continue;
+ assert.ok(scripts.includes('shared/source-registry.js'));
+ assert.ok(
+ scripts.indexOf('shared/source-registry.js') < scripts.indexOf('content/utils.js'),
+ 'shared/source-registry.js must load before content/utils.js'
+ );
+ }
+});
+
+test('shared source registry exposes canonical source, alias, detection, and ready policies', () => {
+ const source = fs.readFileSync('shared/source-registry.js', 'utf8');
+ const api = new Function('self', `${source}; return self.MultiPageSourceRegistry;`)({});
+ const registry = api.createSourceRegistry();
+
+ assert.equal(registry.resolveCanonicalSource('signup-page'), 'openai-auth');
+ assert.deepEqual(registry.getSourceKeys('signup-page'), ['openai-auth', 'signup-page']);
+ assert.equal(registry.getSourceLabel('openai-auth'), '认证页');
+ assert.equal(
+ registry.matchesSourceUrlFamily(
+ 'openai-auth',
+ 'https://chatgpt.com/',
+ 'https://auth.openai.com/authorize?client_id=test'
+ ),
+ true
+ );
+ assert.equal(
+ registry.detectSourceFromLocation({
+ url: 'https://auth.openai.com/create-account',
+ hostname: 'auth.openai.com',
+ }),
+ 'openai-auth'
+ );
+ assert.equal(
+ registry.detectSourceFromLocation({
+ url: 'https://example.com/',
+ hostname: 'example.com',
+ }),
+ 'unknown-source'
+ );
+ assert.equal(registry.shouldReportReadyForFrame('mail-163', true), false);
+ assert.equal(registry.shouldReportReadyForFrame('unknown-source', false), false);
+ assert.equal(registry.getCleanupOwnerSource('oauth-localhost-callback'), 'openai-auth');
+});
diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js
index 14eedb8..9e5dc1e 100644
--- a/tests/verification-flow-polling.test.js
+++ b/tests/verification-flow-polling.test.js
@@ -9,6 +9,7 @@ const api = new Function('self', `${source}; return self.MultiPageBackgroundVeri
function createVerificationFlowTestHelpers(overrides = {}) {
return api.createVerificationFlowHelpers({
addLog: async () => {},
+ buildVerificationPollPayload: null,
chrome: {
tabs: {
update: async () => {},
@@ -43,6 +44,42 @@ function createVerificationFlowTestHelpers(overrides = {}) {
});
}
+test('verification flow prefers injected verification poll payload builder when provided', () => {
+ const helpers = createVerificationFlowTestHelpers({
+ buildVerificationPollPayload: (step, state, overrides = {}) => ({
+ flowId: state.activeFlowId || 'openai',
+ ruleId: 'custom-rule',
+ step,
+ senderFilters: ['custom-sender'],
+ subjectFilters: ['custom-subject'],
+ targetEmail: state.email,
+ maxAttempts: 9,
+ intervalMs: 4321,
+ ...overrides,
+ }),
+ });
+
+ assert.deepStrictEqual(
+ helpers.getVerificationPollPayload(4, {
+ activeFlowId: 'openai',
+ email: 'user@example.com',
+ }, {
+ excludeCodes: ['111111'],
+ }),
+ {
+ flowId: 'openai',
+ ruleId: 'custom-rule',
+ step: 4,
+ senderFilters: ['custom-sender'],
+ subjectFilters: ['custom-subject'],
+ targetEmail: 'user@example.com',
+ maxAttempts: 9,
+ intervalMs: 4321,
+ excludeCodes: ['111111'],
+ }
+ );
+});
+
test('verification flow keeps 2925 polling cadence in the default payload', () => {
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},