feat: lay groundwork for multi-flow registries

This commit is contained in:
QLHazyCoder
2026-05-13 04:46:56 +08:00
parent 04f89620be
commit a85ed0ce89
28 changed files with 2775 additions and 92 deletions
+162 -18
View File
@@ -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,
+10
View File
@@ -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': 'LuckMailAPI 购邮)',
'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 || '未知来源';
}
+48
View File
@@ -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,
};
});
+21 -3
View File
@@ -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);
}
+6 -1
View File
@@ -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':
+484
View File
@@ -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,
};
});
+123 -35
View File
@@ -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();
+4
View File
@@ -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';
+19 -3
View File
@@ -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);
}
+69
View File
@@ -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,
};
});
+5
View File
@@ -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"
+242
View File
@@ -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,
};
});
+433
View File
@@ -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,
};
});
+1
View File
@@ -1732,6 +1732,7 @@
<script src="../mail-provider-utils.js"></script>
<script src="../hotmail-utils.js"></script>
<script src="../luckmail-utils.js"></script>
<script src="../shared/flow-capabilities.js"></script>
<script src="../data/step-definitions.js"></script>
<script src="update-service.js"></script>
<script src="contribution-content-update-service.js"></script>
+428 -30
View File
@@ -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 });
+3
View File
@@ -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: {',
@@ -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 的邮件轮询规则构造器/
);
});
@@ -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');
});
@@ -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', () => {
@@ -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);
});
@@ -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 = [];
@@ -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 = {};
+34
View File
@@ -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', () => {
+72
View File
@@ -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, []);
});
@@ -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' };
+58 -2
View File
@@ -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'),
+56
View File
@@ -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');
});
+37
View File
@@ -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 () => {},