feat: lay groundwork for multi-flow registries

This commit is contained in:
QLHazyCoder
2026-05-13 04:46:56 +08:00
parent 04f89620be
commit a85ed0ce89
28 changed files with 2775 additions and 92 deletions
+10
View File
@@ -9,16 +9,22 @@
isRecoverableStep9AuthFailure,
LOG_PREFIX,
setState,
sourceRegistry = null,
STOP_ERROR_MESSAGE,
} = deps;
function getSourceLabel(source) {
if (sourceRegistry?.getSourceLabel) {
return sourceRegistry.getSourceLabel(source);
}
const labels = {
'openai-auth': '认证页',
'gmail-mail': 'Gmail 邮箱',
'sidepanel': '侧边栏',
'signup-page': '认证页',
'vps-panel': 'CPA 面板',
'sub2api-panel': 'SUB2API 后台',
'codex2api-panel': 'Codex2API 后台',
'qq-mail': 'QQ 邮箱',
'mail-163': '163 邮箱',
'mail-2925': '2925 邮箱',
@@ -28,6 +34,10 @@
'luckmail-api': 'LuckMailAPI 购邮)',
'cloudflare-temp-email': 'Cloudflare Temp Email',
'cloudmail': 'Cloud Mail',
'plus-checkout': 'Plus Checkout',
'paypal-flow': 'PayPal 授权页',
'gopay-flow': 'GoPay 授权页',
'unknown-source': '未知来源',
};
return labels[source] || source || '未知来源';
}
+48
View File
@@ -0,0 +1,48 @@
(function attachBackgroundMailRuleRegistry(root, factory) {
root.MultiPageBackgroundMailRuleRegistry = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundMailRuleRegistryModule() {
function createMailRuleRegistry(deps = {}) {
const {
defaultFlowId = 'openai',
flowBuilders = {},
} = deps;
function resolveFlowId(state = {}) {
const activeFlowId = String(state?.activeFlowId || '').trim();
return activeFlowId || String(defaultFlowId || '').trim() || 'openai';
}
function getFlowBuilder(flowId) {
const normalizedFlowId = String(flowId || '').trim();
return normalizedFlowId ? flowBuilders[normalizedFlowId] || null : null;
}
function getVerificationMailRule(step, state = {}) {
const flowId = resolveFlowId(state);
const flowBuilder = getFlowBuilder(flowId);
if (!flowBuilder || typeof flowBuilder.getRuleDefinition !== 'function') {
throw new Error(`未找到 flow=${flowId} 的邮件规则定义。`);
}
return flowBuilder.getRuleDefinition(step, state);
}
function buildVerificationPollPayload(step, state = {}, overrides = {}) {
const flowId = resolveFlowId(state);
const flowBuilder = getFlowBuilder(flowId);
if (!flowBuilder || typeof flowBuilder.buildVerificationPollPayload !== 'function') {
throw new Error(`未找到 flow=${flowId} 的邮件轮询规则构造器。`);
}
return flowBuilder.buildVerificationPollPayload(step, state, overrides);
}
return {
buildVerificationPollPayload,
getVerificationMailRule,
resolveFlowId,
};
}
return {
createMailRuleRegistry,
};
});
+21 -3
View File
@@ -51,11 +51,27 @@
getStepIdsForState,
getLastStepIdForState,
normalizeSignupMethod = (value = '') => String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email',
canUsePhoneSignup = (state = {}) => Boolean(state?.phoneVerificationEnabled)
&& !Boolean(state?.plusModeEnabled)
&& !Boolean(state?.contributionMode),
canUsePhoneSignup = (state = {}) => {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
const capabilityRegistry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
defaultFlowId: 'openai',
}) || null;
if (capabilityRegistry?.canUsePhoneSignup) {
return capabilityRegistry.canUsePhoneSignup(state);
}
return Boolean(state?.phoneVerificationEnabled)
&& !Boolean(state?.plusModeEnabled)
&& !Boolean(state?.contributionMode);
},
resolveSignupMethod = (state = {}) => {
const method = normalizeSignupMethod(state?.signupMethod);
const rootScope = typeof self !== 'undefined' ? self : globalThis;
const capabilityRegistry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
defaultFlowId: 'openai',
}) || null;
if (capabilityRegistry?.resolveSignupMethod) {
return capabilityRegistry.resolveSignupMethod(state, method);
}
return method === 'phone' && canUsePhoneSignup(state) ? 'phone' : 'email';
},
getTabId,
@@ -999,6 +1015,8 @@
Object.prototype.hasOwnProperty.call(updates, 'phoneVerificationEnabled')
|| Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
|| Object.prototype.hasOwnProperty.call(updates, 'signupMethod')
|| Object.prototype.hasOwnProperty.call(updates, 'panelMode')
|| Object.prototype.hasOwnProperty.call(updates, 'activeFlowId')
) {
updates.signupMethod = resolveSignupMethod(nextSignupState);
}
+6 -1
View File
@@ -6,6 +6,7 @@
DEFAULT_CODEX2API_URL,
DEFAULT_SUB2API_URL,
normalizeLocalCpaStep9Mode,
sourceRegistry = null,
} = deps;
function parseUrlSafely(rawUrl) {
@@ -66,7 +67,7 @@
}
function isSignupEntryHost(hostname = '') {
return ['chatgpt.com', 'chat.openai.com'].includes(hostname);
return ['chatgpt.com', 'www.chatgpt.com', 'chat.openai.com'].includes(hostname);
}
function isSignupPasswordPageUrl(rawUrl) {
@@ -117,12 +118,16 @@
}
function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) {
if (sourceRegistry?.matchesSourceUrlFamily) {
return sourceRegistry.matchesSourceUrlFamily(source, candidateUrl, referenceUrl);
}
const candidate = parseUrlSafely(candidateUrl);
if (!candidate) return false;
const reference = parseUrlSafely(referenceUrl);
switch (source) {
case 'openai-auth':
case 'signup-page':
return isSignupPageHost(candidate.hostname) || isSignupEntryHost(candidate.hostname);
case 'duck-mail':
+484
View File
@@ -0,0 +1,484 @@
(function attachBackgroundRuntimeState(root, factory) {
root.MultiPageBackgroundRuntimeState = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundRuntimeStateModule() {
function createRuntimeStateHelpers(deps = {}) {
const {
DEFAULT_ACTIVE_FLOW_ID = 'openai',
defaultStepStatuses = {},
getStepDefinitionForState = null,
} = deps;
const RUNTIME_SHARED_FIELDS = Object.freeze([
'automationWindowId',
'tabRegistry',
'sourceLastUrls',
'flowStartTime',
]);
const RUNTIME_PROXY_FIELDS = Object.freeze([
'ipProxyApiPool',
'ipProxyApiCurrentIndex',
'ipProxyApiCurrent',
'ipProxyAccountPool',
'ipProxyAccountCurrentIndex',
'ipProxyAccountCurrent',
'ipProxyPool',
'ipProxyCurrentIndex',
'ipProxyCurrent',
]);
const OPENAI_FLOW_FIELD_GROUPS = Object.freeze({
auth: Object.freeze([
'oauthUrl',
'localhostUrl',
]),
platformBinding: Object.freeze([
'cpaOAuthState',
'cpaManagementOrigin',
'sub2apiSessionId',
'sub2apiOAuthState',
'sub2apiGroupId',
'sub2apiDraftName',
'sub2apiProxyId',
'sub2apiGroupIds',
'codex2apiSessionId',
'codex2apiOAuthState',
]),
plus: Object.freeze([
'plusCheckoutTabId',
'plusCheckoutUrl',
'plusCheckoutCountry',
'plusCheckoutCurrency',
'plusCheckoutSource',
'plusBillingCountryText',
'plusBillingAddress',
'plusPaypalApprovedAt',
'plusGoPayApprovedAt',
'plusReturnUrl',
'plusManualConfirmationPending',
'plusManualConfirmationRequestId',
'plusManualConfirmationStep',
'plusManualConfirmationMethod',
'plusManualConfirmationTitle',
'plusManualConfirmationMessage',
'gopayHelperReferenceId',
'gopayHelperGoPayGuid',
'gopayHelperRedirectUrl',
'gopayHelperNextAction',
'gopayHelperFlowId',
'gopayHelperChallengeId',
'gopayHelperStartPayload',
'gopayHelperTaskId',
'gopayHelperTaskStatus',
'gopayHelperStatusText',
'gopayHelperRemoteStage',
'gopayHelperApiWaitingFor',
'gopayHelperApiInputDeadlineAt',
'gopayHelperApiInputWaitSeconds',
'gopayHelperLastInputError',
'gopayHelperOtpInvalidCount',
'gopayHelperFailureStage',
'gopayHelperFailureDetail',
'gopayHelperTaskPayload',
'gopayHelperOrderCreatedAt',
'gopayHelperTaskProgressSignature',
'gopayHelperTaskProgressAt',
'gopayHelperTaskProgressTaskId',
'gopayHelperPinPayload',
'gopayHelperResolvedOtp',
'gopayHelperOtpRequestId',
'gopayHelperOtpReferenceId',
]),
phoneVerification: Object.freeze([
'currentPhoneActivation',
'phoneNumber',
'currentPhoneVerificationCode',
'currentPhoneVerificationCountdownEndsAt',
'currentPhoneVerificationCountdownWindowIndex',
'currentPhoneVerificationCountdownWindowTotal',
'reusablePhoneActivation',
'freeReusablePhoneActivation',
'phoneReusableActivationPool',
'signupPhoneNumber',
'signupPhoneActivation',
'signupPhoneCompletedActivation',
'signupPhoneVerificationRequestedAt',
'signupPhoneVerificationPurpose',
]),
luckmail: Object.freeze([
'currentLuckmailPurchase',
'currentLuckmailMailCursor',
]),
identity: Object.freeze([
'resolvedSignupMethod',
'accountIdentifierType',
'accountIdentifier',
'registrationEmailState',
'email',
'password',
'lastEmailTimestamp',
'lastSignupCode',
'lastLoginCode',
'step8VerificationTargetEmail',
]),
});
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function cloneValue(value) {
if (Array.isArray(value)) {
return value.map((item) => cloneValue(item));
}
if (isPlainObject(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, entryValue]) => [key, cloneValue(entryValue)])
);
}
return value;
}
function normalizePlainObject(value) {
return isPlainObject(value) ? value : {};
}
function normalizeFlowId(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized || DEFAULT_ACTIVE_FLOW_ID;
}
function normalizeRunId(value = '') {
return String(value || '').trim();
}
function normalizeStepNumber(value) {
const step = Math.floor(Number(value) || 0);
return step > 0 ? step : 0;
}
function normalizeNodeStatus(value = '') {
const normalized = String(value || '').trim().toLowerCase();
if (!normalized) {
return 'pending';
}
return normalized;
}
function buildDefaultStepStatuses() {
return Object.fromEntries(
Object.entries(normalizePlainObject(defaultStepStatuses)).map(([key, value]) => [
String(key),
normalizeNodeStatus(value),
])
);
}
function normalizeStepStatuses(value) {
const base = buildDefaultStepStatuses();
if (!isPlainObject(value)) {
return base;
}
const next = { ...base };
for (const [key, status] of Object.entries(value)) {
const step = normalizeStepNumber(key);
if (!step) continue;
next[String(step)] = normalizeNodeStatus(status);
}
return next;
}
function normalizeLegacyStepCompat(value = {}, state = {}) {
const candidate = normalizePlainObject(value);
const currentStep = normalizeStepNumber(
Object.prototype.hasOwnProperty.call(state, 'currentStep')
? state.currentStep
: candidate.currentStep
);
const stepStatuses = normalizeStepStatuses(
Object.prototype.hasOwnProperty.call(state, 'stepStatuses')
? state.stepStatuses
: candidate.stepStatuses
);
return {
currentStep,
stepStatuses,
};
}
function resolveStepKey(step, state = {}) {
const numericStep = normalizeStepNumber(step);
if (!numericStep || typeof getStepDefinitionForState !== 'function') {
return '';
}
return String(getStepDefinitionForState(numericStep, state)?.key || '').trim();
}
function normalizeNodeStatuses(value, state = {}, legacyStepCompat = null) {
if (isPlainObject(value) && Object.keys(value).length > 0) {
return Object.fromEntries(
Object.entries(value)
.map(([key, status]) => [String(key || '').trim(), normalizeNodeStatus(status)])
.filter(([key]) => Boolean(key))
);
}
const compat = legacyStepCompat || normalizeLegacyStepCompat({}, state);
const next = {};
for (const [stepKey, status] of Object.entries(compat.stepStatuses || {})) {
const step = normalizeStepNumber(stepKey);
if (!step) continue;
const nodeKey = resolveStepKey(step, state) || `step-${step}`;
next[nodeKey] = normalizeNodeStatus(status);
}
return next;
}
function pickDefinedFields(state = {}, fields = []) {
const next = {};
for (const field of fields) {
if (Object.prototype.hasOwnProperty.call(state, field)) {
next[field] = cloneValue(state[field]);
}
}
return next;
}
function buildSharedState(baseValue = {}, state = {}) {
return {
...cloneValue(normalizePlainObject(baseValue)),
...pickDefinedFields(state, RUNTIME_SHARED_FIELDS),
};
}
function buildServiceState(baseValue = {}, state = {}) {
const base = cloneValue(normalizePlainObject(baseValue));
return {
...base,
proxy: {
...cloneValue(normalizePlainObject(base.proxy)),
...pickDefinedFields(state, RUNTIME_PROXY_FIELDS),
},
};
}
function flattenOpenAiFlowState(flowState = {}) {
const openaiState = normalizePlainObject(flowState.openai);
const next = {};
for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) {
const group = normalizePlainObject(openaiState[groupKey]);
for (const field of fields) {
if (Object.prototype.hasOwnProperty.call(group, field)) {
next[field] = cloneValue(group[field]);
}
}
}
return next;
}
function buildOpenAiFlowState(baseValue = {}, state = {}) {
const baseFlowState = cloneValue(normalizePlainObject(baseValue));
const baseOpenAi = cloneValue(normalizePlainObject(baseFlowState.openai));
const openaiState = {
...baseOpenAi,
};
for (const [groupKey, fields] of Object.entries(OPENAI_FLOW_FIELD_GROUPS)) {
openaiState[groupKey] = {
...cloneValue(normalizePlainObject(baseOpenAi[groupKey])),
...pickDefinedFields(state, fields),
};
}
return {
...baseFlowState,
openai: openaiState,
};
}
function buildRuntimeStateDefault() {
return {
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
activeRunId: '',
currentNodeId: '',
nodeStatuses: {},
sharedState: {},
serviceState: {
proxy: {},
},
flowState: {
openai: {
auth: {},
platformBinding: {},
plus: {},
phoneVerification: {},
luckmail: {},
identity: {},
},
},
legacyStepCompat: {
currentStep: 0,
stepStatuses: buildDefaultStepStatuses(),
},
};
}
function ensureRuntimeState(state = {}) {
const baseRuntimeState = {
...buildRuntimeStateDefault(),
...cloneValue(normalizePlainObject(state.runtimeState)),
};
const activeFlowId = normalizeFlowId(
Object.prototype.hasOwnProperty.call(state, 'activeFlowId')
? state.activeFlowId
: baseRuntimeState.activeFlowId
);
const legacyStepCompat = normalizeLegacyStepCompat(baseRuntimeState.legacyStepCompat, state);
const currentNodeId = String(
Object.prototype.hasOwnProperty.call(state, 'currentNodeId')
? state.currentNodeId
: (baseRuntimeState.currentNodeId || resolveStepKey(legacyStepCompat.currentStep, state))
).trim();
const nodeStatuses = normalizeNodeStatuses(
Object.prototype.hasOwnProperty.call(state, 'nodeStatuses')
? state.nodeStatuses
: baseRuntimeState.nodeStatuses,
state,
legacyStepCompat
);
return {
...baseRuntimeState,
activeFlowId,
activeRunId: normalizeRunId(
Object.prototype.hasOwnProperty.call(state, 'activeRunId')
? state.activeRunId
: baseRuntimeState.activeRunId
),
currentNodeId,
nodeStatuses,
sharedState: buildSharedState(baseRuntimeState.sharedState, state),
serviceState: buildServiceState(baseRuntimeState.serviceState, state),
flowState: buildOpenAiFlowState(baseRuntimeState.flowState, state),
legacyStepCompat,
};
}
function buildFlattenedUpdates(updates = {}) {
const next = {
...updates,
};
const runtimeState = normalizePlainObject(updates.runtimeState);
const legacyStepCompat = normalizePlainObject(updates.legacyStepCompat);
const sharedState = normalizePlainObject(updates.sharedState);
const serviceState = normalizePlainObject(updates.serviceState);
const flowState = normalizePlainObject(updates.flowState);
if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeFlowId')) {
next.activeFlowId = runtimeState.activeFlowId;
}
if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeRunId')) {
next.activeRunId = runtimeState.activeRunId;
}
if (Object.prototype.hasOwnProperty.call(runtimeState, 'currentNodeId')) {
next.currentNodeId = runtimeState.currentNodeId;
}
if (Object.prototype.hasOwnProperty.call(runtimeState, 'nodeStatuses')) {
next.nodeStatuses = cloneValue(runtimeState.nodeStatuses);
}
if (Object.prototype.hasOwnProperty.call(legacyStepCompat, 'currentStep')) {
next.currentStep = legacyStepCompat.currentStep;
}
if (Object.prototype.hasOwnProperty.call(legacyStepCompat, 'stepStatuses')) {
next.stepStatuses = cloneValue(legacyStepCompat.stepStatuses);
}
if (Object.prototype.hasOwnProperty.call(runtimeState, 'legacyStepCompat')) {
const compatCandidate = normalizePlainObject(runtimeState.legacyStepCompat);
if (Object.prototype.hasOwnProperty.call(compatCandidate, 'currentStep')) {
next.currentStep = compatCandidate.currentStep;
}
if (Object.prototype.hasOwnProperty.call(compatCandidate, 'stepStatuses')) {
next.stepStatuses = cloneValue(compatCandidate.stepStatuses);
}
}
Object.assign(next, pickDefinedFields(sharedState, RUNTIME_SHARED_FIELDS));
if (Object.prototype.hasOwnProperty.call(runtimeState, 'sharedState')) {
Object.assign(
next,
pickDefinedFields(normalizePlainObject(runtimeState.sharedState), RUNTIME_SHARED_FIELDS)
);
}
const serviceProxy = normalizePlainObject(serviceState.proxy);
Object.assign(next, pickDefinedFields(serviceProxy, RUNTIME_PROXY_FIELDS));
if (Object.prototype.hasOwnProperty.call(runtimeState, 'serviceState')) {
const runtimeServiceState = normalizePlainObject(runtimeState.serviceState);
Object.assign(
next,
pickDefinedFields(normalizePlainObject(runtimeServiceState.proxy), RUNTIME_PROXY_FIELDS)
);
}
Object.assign(next, flattenOpenAiFlowState(flowState));
if (Object.prototype.hasOwnProperty.call(runtimeState, 'flowState')) {
Object.assign(next, flattenOpenAiFlowState(normalizePlainObject(runtimeState.flowState)));
}
return next;
}
function buildStateView(state = {}) {
const runtimeState = ensureRuntimeState(state);
return {
...state,
activeFlowId: runtimeState.activeFlowId,
activeRunId: runtimeState.activeRunId,
currentNodeId: runtimeState.currentNodeId,
nodeStatuses: cloneValue(runtimeState.nodeStatuses),
flowState: cloneValue(runtimeState.flowState),
sharedState: cloneValue(runtimeState.sharedState),
serviceState: cloneValue(runtimeState.serviceState),
legacyStepCompat: cloneValue(runtimeState.legacyStepCompat),
currentStep: runtimeState.legacyStepCompat.currentStep,
stepStatuses: cloneValue(runtimeState.legacyStepCompat.stepStatuses),
runtimeState,
};
}
function buildSessionStatePatch(currentState = {}, updates = {}) {
const flattenedUpdates = buildFlattenedUpdates(updates);
const nextState = {
...currentState,
...flattenedUpdates,
};
const runtimeState = ensureRuntimeState(nextState);
return {
...flattenedUpdates,
activeFlowId: runtimeState.activeFlowId,
activeRunId: runtimeState.activeRunId,
currentNodeId: runtimeState.currentNodeId,
nodeStatuses: cloneValue(runtimeState.nodeStatuses),
currentStep: runtimeState.legacyStepCompat.currentStep,
stepStatuses: cloneValue(runtimeState.legacyStepCompat.stepStatuses),
runtimeState,
};
}
return {
DEFAULT_ACTIVE_FLOW_ID,
OPENAI_FLOW_FIELD_GROUPS,
RUNTIME_PROXY_FIELDS,
RUNTIME_SHARED_FIELDS,
buildDefaultRuntimeState: buildRuntimeStateDefault,
buildSessionStatePatch,
buildStateView,
ensureRuntimeState,
};
}
return {
createRuntimeStateHelpers,
};
});
+123 -35
View File
@@ -11,6 +11,7 @@
isRetryableContentScriptTransportError,
LOG_PREFIX,
matchesSourceUrlFamily,
sourceRegistry = null,
setState,
sleepWithStop,
STOP_ERROR_MESSAGE,
@@ -19,6 +20,65 @@
const pendingCommands = new Map();
function resolveCanonicalSource(source) {
if (sourceRegistry?.resolveCanonicalSource) {
return sourceRegistry.resolveCanonicalSource(source);
}
return String(source || '').trim();
}
function getSourceKeys(source) {
if (sourceRegistry?.getSourceKeys) {
const registryKeys = sourceRegistry.getSourceKeys(source);
if (Array.isArray(registryKeys) && registryKeys.length) {
return registryKeys;
}
}
const normalized = String(source || '').trim();
return normalized ? [normalized] : [];
}
function getSourceCommandKey(source) {
const keys = getSourceKeys(source);
return keys[0] || String(source || '').trim();
}
function sourcesMatch(leftSource, rightSource) {
const left = resolveCanonicalSource(leftSource);
const right = resolveCanonicalSource(rightSource);
return Boolean(left && right && left === right);
}
function getSourceMapValue(record, source) {
const map = record && typeof record === 'object' ? record : {};
for (const key of getSourceKeys(source)) {
if (Object.prototype.hasOwnProperty.call(map, key)) {
return map[key];
}
}
return undefined;
}
function setSourceMapValue(record, source, value) {
const nextRecord = { ...(record || {}) };
const keys = getSourceKeys(source);
const canonicalKey = keys[0] || String(source || '').trim();
for (const key of keys.slice(1)) {
delete nextRecord[key];
}
if (canonicalKey) {
nextRecord[canonicalKey] = value;
}
return nextRecord;
}
function getCleanupOwnerSource(cleanupScope) {
if (sourceRegistry?.getCleanupOwnerSource) {
return sourceRegistry.getCleanupOwnerSource(cleanupScope);
}
return cleanupScope === 'oauth-localhost-callback' ? 'signup-page' : '';
}
function normalizeAutomationWindowId(value) {
if (value === null || value === undefined || value === '') {
return null;
@@ -170,7 +230,7 @@
}
async function registerTab(source, tabId) {
const registry = await getTabRegistry();
let registry = await getTabRegistry();
let windowId = null;
if (chrome?.tabs?.get && Number.isInteger(tabId)) {
const tab = await chrome.tabs.get(tabId).catch(() => null);
@@ -180,42 +240,42 @@
}
windowId = normalizeAutomationWindowId(tab?.windowId);
}
registry[source] = {
registry = setSourceMapValue(registry, source, {
tabId,
ready: true,
...(windowId !== null ? { windowId } : {}),
};
});
await setState({ tabRegistry: registry });
console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
}
async function isTabAlive(source) {
const registry = await getTabRegistry();
const entry = registry[source];
let registry = await getTabRegistry();
const entry = getSourceMapValue(registry, source);
if (!entry) return false;
try {
const tab = await chrome.tabs.get(entry.tabId);
if (!(await isTabInAutomationWindow(tab))) {
registry[source] = null;
registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
return false;
}
return true;
} catch {
registry[source] = null;
registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
return false;
}
}
async function getTabId(source) {
const registry = await getTabRegistry();
const tabId = registry[source]?.tabId || null;
let registry = await getTabRegistry();
const tabId = getSourceMapValue(registry, source)?.tabId || null;
if (!Number.isInteger(tabId)) {
return null;
}
if (!(await isTabInAutomationWindow(tabId))) {
registry[source] = null;
registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
return null;
}
@@ -225,8 +285,7 @@
async function rememberSourceLastUrl(source, url) {
if (!source || !url) return;
const state = await getState();
const sourceLastUrls = { ...(state.sourceLastUrls || {}) };
sourceLastUrls[source] = url;
const sourceLastUrls = setSourceMapValue(state.sourceLastUrls, source, url);
await setState({ sourceLastUrls });
}
@@ -234,7 +293,7 @@
const { excludeTabIds = [] } = options;
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
const state = await getState();
const lastUrl = state.sourceLastUrls?.[source];
const lastUrl = getSourceMapValue(state.sourceLastUrls, source);
const referenceUrls = [currentUrl, lastUrl].filter(Boolean);
if (!referenceUrls.length) return;
@@ -249,9 +308,10 @@
await chrome.tabs.remove(matchedIds).catch(() => { });
const registry = await getTabRegistry();
if (registry[source]?.tabId && matchedIds.includes(registry[source].tabId)) {
registry[source] = null;
let registry = await getTabRegistry();
const sourceEntry = getSourceMapValue(registry, source);
if (sourceEntry?.tabId && matchedIds.includes(sourceEntry.tabId)) {
registry = setSourceMapValue(registry, source, null);
await setState({ tabRegistry: registry });
}
@@ -274,7 +334,7 @@
async function closeLocalhostCallbackTabs(callbackUrl, options = {}) {
if (!isLocalhostOAuthCallbackUrl(callbackUrl)) return 0;
const { excludeTabIds = [] } = options;
const { excludeTabIds = [], ownerSource = getCleanupOwnerSource('oauth-localhost-callback') } = options;
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
const tabs = await queryTabsInAutomationWindow({});
const matchedIds = tabs
@@ -286,9 +346,10 @@
await chrome.tabs.remove(matchedIds).catch(() => { });
const registry = await getTabRegistry();
if (registry['signup-page']?.tabId && matchedIds.includes(registry['signup-page'].tabId)) {
registry['signup-page'] = null;
let registry = await getTabRegistry();
const ownerEntry = getSourceMapValue(registry, ownerSource);
if (ownerEntry?.tabId && matchedIds.includes(ownerEntry.tabId)) {
registry = setSourceMapValue(registry, ownerSource, null);
await setState({ tabRegistry: registry });
}
@@ -468,7 +529,7 @@
while (Date.now() - start < timeoutMs) {
attempt += 1;
const pong = await pingContentScriptOnTab(tabId);
if (pong?.ok && (!pong.source || pong.source === source)) {
if (pong?.ok && (!pong.source || sourcesMatch(pong.source, source))) {
console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
await registerTab(source, tabId);
return;
@@ -478,9 +539,13 @@
throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
}
const registry = await getTabRegistry();
if (registry[source]) {
registry[source].ready = false;
let registry = await getTabRegistry();
const sourceEntry = getSourceMapValue(registry, source);
if (sourceEntry) {
registry = setSourceMapValue(registry, source, {
...sourceEntry,
ready: false,
});
await setState({ tabRegistry: registry });
}
@@ -505,7 +570,7 @@
}
const pongAfterInject = await pingContentScriptOnTab(tabId);
if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) {
if (pongAfterInject?.ok && (!pongAfterInject.source || sourcesMatch(pongAfterInject.source, source))) {
console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready after inject ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
await registerTab(source, tabId);
return;
@@ -609,14 +674,16 @@
function queueCommand(source, message, timeout = 15000) {
return new Promise((resolve, reject) => {
const commandKey = getSourceCommandKey(source);
const timer = setTimeout(() => {
pendingCommands.delete(source);
pendingCommands.delete(commandKey);
reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
}, timeout);
pendingCommands.set(source, {
pendingCommands.set(commandKey, {
message,
resolve,
reject,
source,
timer,
responseTimeoutMs: timeout,
});
@@ -625,11 +692,16 @@
}
function flushCommand(source, tabId) {
const pending = pendingCommands.get(source);
const pending = pendingCommands.get(getSourceCommandKey(source));
if (pending) {
clearTimeout(pending.timer);
pendingCommands.delete(source);
sendTabMessageWithTimeout(tabId, source, pending.message, pending.responseTimeoutMs).then(pending.resolve).catch(pending.reject);
pendingCommands.delete(getSourceCommandKey(source));
sendTabMessageWithTimeout(
tabId,
pending.source || source,
pending.message,
pending.responseTimeoutMs
).then(pending.resolve).catch(pending.reject);
console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
}
}
@@ -677,18 +749,29 @@
const sameUrl = currentTab.url === url;
const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
const registry = await getTabRegistry();
let registry = await getTabRegistry();
const sourceEntry = getSourceMapValue(registry, source);
if (sameUrl) {
await chrome.tabs.update(tabId, { active: true });
if (shouldReloadOnReuse) {
if (registry[source]) registry[source].ready = false;
if (sourceEntry) {
registry = setSourceMapValue(registry, source, {
...sourceEntry,
ready: false,
});
}
await setState({ tabRegistry: registry });
await chrome.tabs.reload(tabId);
await waitForTabUpdateComplete(tabId);
}
if (options.inject) {
if (registry[source]) registry[source].ready = false;
if (sourceEntry) {
registry = setSourceMapValue(registry, source, {
...sourceEntry,
ready: false,
});
}
await setState({ tabRegistry: registry });
if (options.injectSource) {
await chrome.scripting.executeScript({
@@ -710,7 +793,12 @@
return tabId;
}
if (registry[source]) registry[source].ready = false;
if (sourceEntry) {
registry = setSourceMapValue(registry, source, {
...sourceEntry,
ready: false,
});
}
await setState({ tabRegistry: registry });
await chrome.tabs.update(tabId, { url, active: true });
@@ -765,7 +853,7 @@
throwIfStopped();
const { responseTimeoutMs = getContentScriptResponseTimeoutMs(message) } = options;
const registry = await getTabRegistry();
const entry = registry[source];
const entry = getSourceMapValue(registry, source);
if (!entry || !entry.ready) {
throwIfStopped();
+4
View File
@@ -7,6 +7,7 @@
function createVerificationFlowHelpers(deps = {}) {
const {
addLog: rawAddLog = async () => {},
buildVerificationPollPayload: externalBuildVerificationPollPayload = null,
chrome,
closeConflictingTabsForSource,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
@@ -408,6 +409,9 @@
}
function getVerificationPollPayload(step, state, overrides = {}) {
if (typeof externalBuildVerificationPollPayload === 'function') {
return externalBuildVerificationPollPayload(step, state, overrides);
}
const is2925Provider = state?.mailProvider === '2925';
const mail2925MatchTargetEmail = is2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive';