feat: lay groundwork for multi-flow registries
This commit is contained in:
+162
-18
@@ -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,
|
||||
|
||||
@@ -9,16 +9,22 @@
|
||||
isRecoverableStep9AuthFailure,
|
||||
LOG_PREFIX,
|
||||
setState,
|
||||
sourceRegistry = null,
|
||||
STOP_ERROR_MESSAGE,
|
||||
} = deps;
|
||||
|
||||
function getSourceLabel(source) {
|
||||
if (sourceRegistry?.getSourceLabel) {
|
||||
return sourceRegistry.getSourceLabel(source);
|
||||
}
|
||||
const labels = {
|
||||
'openai-auth': '认证页',
|
||||
'gmail-mail': 'Gmail 邮箱',
|
||||
'sidepanel': '侧边栏',
|
||||
'signup-page': '认证页',
|
||||
'vps-panel': 'CPA 面板',
|
||||
'sub2api-panel': 'SUB2API 后台',
|
||||
'codex2api-panel': 'Codex2API 后台',
|
||||
'qq-mail': 'QQ 邮箱',
|
||||
'mail-163': '163 邮箱',
|
||||
'mail-2925': '2925 邮箱',
|
||||
@@ -28,6 +34,10 @@
|
||||
'luckmail-api': 'LuckMail(API 购邮)',
|
||||
'cloudflare-temp-email': 'Cloudflare Temp Email',
|
||||
'cloudmail': 'Cloud Mail',
|
||||
'plus-checkout': 'Plus Checkout',
|
||||
'paypal-flow': 'PayPal 授权页',
|
||||
'gopay-flow': 'GoPay 授权页',
|
||||
'unknown-source': '未知来源',
|
||||
};
|
||||
return labels[source] || source || '未知来源';
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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,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':
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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
@@ -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 });
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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' };
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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 () => {},
|
||||
|
||||
Reference in New Issue
Block a user