fix: preserve phone identity on oauth-login completion
- Merge latest origin/dev into PR #245 for the current review baseline.\n- Sync Step 7 completion identity before oauth-login step-key handling returns.\n- Cover the real oauth-login stepKey route in the message-router regression test.
This commit is contained in:
+292
-22
@@ -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',
|
||||
@@ -15,10 +17,14 @@ importScripts(
|
||||
'background/paypal-account-store.js',
|
||||
'background/ip-proxy-provider-711proxy.js',
|
||||
'background/ip-proxy-core.js',
|
||||
'background/sub2api-api.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',
|
||||
@@ -56,21 +62,30 @@ importScripts(
|
||||
'content/activation-utils.js'
|
||||
);
|
||||
|
||||
const NORMAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: false }) || [];
|
||||
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
|
||||
const NORMAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
plusModeEnabled: false,
|
||||
}) || [];
|
||||
const PLUS_PAYPAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal',
|
||||
}) || NORMAL_STEP_DEFINITIONS;
|
||||
const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'gopay',
|
||||
}) || PLUS_PAYPAL_STEP_DEFINITIONS;
|
||||
const PLUS_GPC_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
}) || PLUS_GOPAY_STEP_DEFINITIONS;
|
||||
const PLUS_STEP_DEFINITIONS = PLUS_PAYPAL_STEP_DEFINITIONS;
|
||||
const ALL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getAllSteps?.() || [
|
||||
const ALL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getAllSteps?.({
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
}) || [
|
||||
...NORMAL_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_STEP_DEFINITIONS,
|
||||
...PLUS_GOPAY_STEP_DEFINITIONS,
|
||||
@@ -80,6 +95,7 @@ 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_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 +221,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 +290,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));
|
||||
}
|
||||
@@ -571,11 +606,21 @@ function getSignupMethodForStepDefinitions(state = {}) {
|
||||
function getStepDefinitionsForState(state = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.MultiPageStepDefinitions?.getSteps) {
|
||||
return rootScope.MultiPageStepDefinitions.getSteps({
|
||||
const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID === 'string' ? DEFAULT_ACTIVE_FLOW_ID : 'openai';
|
||||
const activeFlowId = String(state?.activeFlowId || '').trim().toLowerCase() || defaultFlowId;
|
||||
const definitions = rootScope.MultiPageStepDefinitions.getSteps({
|
||||
activeFlowId,
|
||||
plusModeEnabled: isPlusModeState(state),
|
||||
plusPaymentMethod: normalizePlusPaymentMethod(state?.plusPaymentMethod),
|
||||
signupMethod: getSignupMethodForStepDefinitions(state),
|
||||
});
|
||||
if (Array.isArray(definitions)) {
|
||||
return definitions;
|
||||
}
|
||||
}
|
||||
const activeFlowId = String(state?.activeFlowId || '').trim().toLowerCase();
|
||||
if (activeFlowId && activeFlowId !== DEFAULT_ACTIVE_FLOW_ID) {
|
||||
return [];
|
||||
}
|
||||
if (!isPlusModeState(state)) {
|
||||
return NORMAL_STEP_DEFINITIONS;
|
||||
@@ -607,10 +652,19 @@ function getStepIdsForState(state = {}) {
|
||||
|
||||
function getLastStepIdForState(state = {}) {
|
||||
const ids = getStepIdsForState(state);
|
||||
return ids[ids.length - 1] || 10;
|
||||
if (ids.length) {
|
||||
return ids[ids.length - 1];
|
||||
}
|
||||
return String(state?.activeFlowId || '').trim().toLowerCase() === DEFAULT_ACTIVE_FLOW_ID ? 10 : 0;
|
||||
}
|
||||
|
||||
function getAuthChainStartStepId(state = {}) {
|
||||
const authStepId = typeof getStepIdByKeyForState === 'function'
|
||||
? getStepIdByKeyForState('oauth-login', state)
|
||||
: null;
|
||||
if (Number.isInteger(authStepId) && authStepId > 0) {
|
||||
return authStepId;
|
||||
}
|
||||
return isPlusModeState(state) ? 10 : FINAL_OAUTH_CHAIN_START_STEP;
|
||||
}
|
||||
|
||||
@@ -844,8 +898,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, // 当前自动轮次冻结后的实际注册方式。
|
||||
@@ -869,6 +928,7 @@ const DEFAULT_STATE = {
|
||||
sub2apiSessionId: null, // SUB2API OpenAI Auth 会话 ID。
|
||||
sub2apiOAuthState: null, // SUB2API OpenAI Auth state。
|
||||
sub2apiGroupId: null, // SUB2API 目标分组 ID。
|
||||
sub2apiGroupIds: [], // SUB2API 多目标分组 ID。
|
||||
sub2apiDraftName: null, // SUB2API 本轮预生成的账号名称。
|
||||
sub2apiProxyId: null, // SUB2API 本轮使用的代理 ID。
|
||||
codex2apiSessionId: null, // Codex2API OAuth 会话 ID。
|
||||
@@ -1320,7 +1380,81 @@ 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 validateAutoRunStartState(state = {}, options = {}) {
|
||||
const registry = getFlowCapabilityRegistry();
|
||||
if (!registry?.validateAutoRunStart) {
|
||||
return { ok: true, errors: [] };
|
||||
}
|
||||
return registry.validateAutoRunStart({
|
||||
activeFlowId: options?.activeFlowId ?? state?.activeFlowId,
|
||||
panelMode: options?.panelMode ?? state?.panelMode,
|
||||
signupMethod: options?.signupMethod ?? state?.signupMethod,
|
||||
state,
|
||||
});
|
||||
}
|
||||
|
||||
function validateModeSwitchState(state = {}, options = {}) {
|
||||
const registry = getFlowCapabilityRegistry();
|
||||
if (!registry?.validateModeSwitch) {
|
||||
return {
|
||||
ok: true,
|
||||
changedKeys: Array.isArray(options?.changedKeys) ? options.changedKeys : [],
|
||||
errors: [],
|
||||
normalizedUpdates: {},
|
||||
};
|
||||
}
|
||||
return registry.validateModeSwitch({
|
||||
activeFlowId: options?.activeFlowId ?? state?.activeFlowId,
|
||||
changedKeys: options?.changedKeys,
|
||||
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 +1466,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 +2913,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 +2991,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 +3016,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);
|
||||
@@ -2926,6 +3090,30 @@ async function importSettingsBundle(configBundle) {
|
||||
fillDefaults: true,
|
||||
requireKnownKeys: true,
|
||||
});
|
||||
const importModeValidation = validateModeSwitchState({
|
||||
...state,
|
||||
...importedSettings,
|
||||
resolvedSignupMethod: null,
|
||||
}, {
|
||||
changedKeys: Object.keys(importedSettings),
|
||||
});
|
||||
if (importModeValidation?.normalizedUpdates && Object.keys(importModeValidation.normalizedUpdates).length > 0) {
|
||||
Object.assign(importedSettings, importModeValidation.normalizedUpdates);
|
||||
}
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(importedSettings, 'phoneVerificationEnabled')
|
||||
|| Object.prototype.hasOwnProperty.call(importedSettings, 'plusModeEnabled')
|
||||
|| Object.prototype.hasOwnProperty.call(importedSettings, 'signupMethod')
|
||||
|| Object.prototype.hasOwnProperty.call(importedSettings, 'panelMode')
|
||||
|| Object.prototype.hasOwnProperty.call(importedSettings, 'activeFlowId')
|
||||
|| Object.prototype.hasOwnProperty.call(importedSettings, 'contributionMode')
|
||||
) {
|
||||
importedSettings.signupMethod = resolveSignupMethod({
|
||||
...state,
|
||||
...importedSettings,
|
||||
resolvedSignupMethod: null,
|
||||
});
|
||||
}
|
||||
|
||||
await setPersistentSettings(importedSettings);
|
||||
|
||||
@@ -3463,7 +3651,7 @@ async function resetState() {
|
||||
? prev.freeReusablePhoneActivation
|
||||
: null;
|
||||
await chrome.storage.session.clear();
|
||||
await chrome.storage.session.set({
|
||||
const resetPayload = buildStatePatchWithRuntimeState({}, {
|
||||
...DEFAULT_STATE,
|
||||
...persistedSettings,
|
||||
...persistedAliasState,
|
||||
@@ -3493,6 +3681,7 @@ async function resetState() {
|
||||
? Number(prev.automationWindowId)
|
||||
: null,
|
||||
});
|
||||
await chrome.storage.session.set(resetPayload);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4055,6 +4244,8 @@ async function requestHotmailLocalCode(account, pollPayload = {}) {
|
||||
top: 5,
|
||||
senderFilters: pollPayload.senderFilters || [],
|
||||
subjectFilters: pollPayload.subjectFilters || [],
|
||||
requiredKeywords: pollPayload.requiredKeywords || [],
|
||||
codePatterns: pollPayload.codePatterns || [],
|
||||
excludeCodes: pollPayload.excludeCodes || [],
|
||||
filterAfterTimestamp: Number(pollPayload.filterAfterTimestamp || 0) || 0,
|
||||
}),
|
||||
@@ -4331,6 +4522,8 @@ async function pollHotmailVerificationCode(step, state, pollPayload = {}) {
|
||||
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
||||
senderFilters: pollPayload.senderFilters || [],
|
||||
subjectFilters: pollPayload.subjectFilters || [],
|
||||
requiredKeywords: pollPayload.requiredKeywords || [],
|
||||
codePatterns: pollPayload.codePatterns || [],
|
||||
excludeCodes: pollPayload.excludeCodes || [],
|
||||
});
|
||||
const match = matchResult.match;
|
||||
@@ -5513,6 +5706,8 @@ async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload
|
||||
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
||||
senderFilters: pollPayload.senderFilters || [],
|
||||
subjectFilters: pollPayload.subjectFilters || [],
|
||||
requiredKeywords: pollPayload.requiredKeywords || [],
|
||||
codePatterns: pollPayload.codePatterns || [],
|
||||
excludeCodes: pollPayload.excludeCodes || [],
|
||||
});
|
||||
const match = matchResult.match;
|
||||
@@ -7218,7 +7413,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) {
|
||||
@@ -7302,6 +7497,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':
|
||||
@@ -7312,6 +7508,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':
|
||||
@@ -7322,11 +7520,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);
|
||||
}
|
||||
@@ -7407,7 +7618,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);
|
||||
}
|
||||
@@ -7598,11 +7809,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 邮箱',
|
||||
@@ -7614,6 +7827,8 @@ function getSourceLabel(source) {
|
||||
'cloudmail': 'Cloud Mail',
|
||||
'plus-checkout': 'Plus Checkout',
|
||||
'paypal-flow': 'PayPal 授权页',
|
||||
'gopay-flow': 'GoPay 授权页',
|
||||
'unknown-source': '未知来源',
|
||||
};
|
||||
return labels[source] || source || '未知来源';
|
||||
}
|
||||
@@ -7667,10 +7882,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({
|
||||
@@ -7680,6 +7901,7 @@ const loggingStatus = self.MultiPageBackgroundLoggingStatus?.createLoggingStatus
|
||||
isRecoverableStep9AuthFailure,
|
||||
LOG_PREFIX,
|
||||
setState,
|
||||
sourceRegistry,
|
||||
STOP_ERROR_MESSAGE,
|
||||
});
|
||||
|
||||
@@ -7692,6 +7914,7 @@ const tabRuntime = self.MultiPageBackgroundTabRuntime?.createTabRuntime({
|
||||
isRetryableContentScriptTransportError,
|
||||
LOG_PREFIX,
|
||||
matchesSourceUrlFamily,
|
||||
sourceRegistry,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
STOP_ERROR_MESSAGE,
|
||||
@@ -8057,7 +8280,9 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
sub2apiSessionId: null,
|
||||
sub2apiOAuthState: null,
|
||||
sub2apiGroupId: null,
|
||||
sub2apiGroupIds: [],
|
||||
sub2apiDraftName: null,
|
||||
sub2apiProxyId: null,
|
||||
codex2apiSessionId: null,
|
||||
codex2apiOAuthState: null,
|
||||
flowStartTime: null,
|
||||
@@ -8556,6 +8781,30 @@ async function launchAutoRunTimerPlan(trigger = 'alarm', options = {}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (plan.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START) {
|
||||
const autoRunStartValidation = typeof validateAutoRunStartState === 'function'
|
||||
? validateAutoRunStartState(state, { state })
|
||||
: { ok: true, errors: [] };
|
||||
if (autoRunStartValidation?.ok === false) {
|
||||
const validationMessage = autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。';
|
||||
await clearAutoRunTimerAlarm();
|
||||
await broadcastAutoRunStatus('idle', {
|
||||
currentRun: 0,
|
||||
totalRuns: 1,
|
||||
attemptRun: 0,
|
||||
}, {
|
||||
autoRunRoundSummaries: [],
|
||||
autoRunTimerPlan: null,
|
||||
scheduledAutoRunPlan: null,
|
||||
});
|
||||
await addLog(`自动运行计划已取消:${validationMessage}`, 'error');
|
||||
if (trigger === 'manual') {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await clearAutoRunTimerAlarm();
|
||||
if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) {
|
||||
return false;
|
||||
@@ -8962,6 +9211,9 @@ async function handleStepData(step, payload) {
|
||||
if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null;
|
||||
if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null;
|
||||
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||||
if (payload.sub2apiGroupIds !== undefined) updates.sub2apiGroupIds = Array.isArray(payload.sub2apiGroupIds)
|
||||
? payload.sub2apiGroupIds
|
||||
: [];
|
||||
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||||
if (payload.cpaOAuthState !== undefined) updates.cpaOAuthState = payload.cpaOAuthState || null;
|
||||
@@ -11296,8 +11548,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,
|
||||
@@ -11638,6 +11902,7 @@ const step10Executor = self.MultiPageBackgroundStep10?.createStep10Executor({
|
||||
sendToContentScript,
|
||||
sendToContentScriptResilient,
|
||||
shouldBypassStep9ForLocalCpa,
|
||||
DEFAULT_SUB2API_GROUP_NAME,
|
||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||
});
|
||||
const stepExecutorsByKey = {
|
||||
@@ -11716,6 +11981,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
normalizeSignupMethod,
|
||||
canUsePhoneSignup,
|
||||
resolveSignupMethod,
|
||||
validateAutoRunStart: validateAutoRunStartState,
|
||||
getTabId,
|
||||
getStopRequested: () => stopRequested,
|
||||
handleCloudflareSecurityBlocked,
|
||||
@@ -11805,6 +12071,10 @@ const plusGoPayStepRegistry = buildStepRegistry(PLUS_GOPAY_STEP_DEFINITIONS);
|
||||
const plusGpcStepRegistry = buildStepRegistry(PLUS_GPC_STEP_DEFINITIONS);
|
||||
|
||||
function getStepRegistryForState(state = {}) {
|
||||
const activeFlowId = String(state?.activeFlowId || DEFAULT_ACTIVE_FLOW_ID).trim().toLowerCase() || DEFAULT_ACTIVE_FLOW_ID;
|
||||
if (activeFlowId !== DEFAULT_ACTIVE_FLOW_ID) {
|
||||
throw new Error(`当前尚未注册 flow=${activeFlowId} 的步骤执行器。`);
|
||||
}
|
||||
if (!isPlusModeState(state)) {
|
||||
return normalStepRegistry;
|
||||
}
|
||||
|
||||
@@ -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,13 +51,67 @@
|
||||
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';
|
||||
},
|
||||
validateAutoRunStart = (state = {}, options = {}) => {
|
||||
const validationState = options?.state || state;
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
const capabilityRegistry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
|
||||
defaultFlowId: 'openai',
|
||||
}) || null;
|
||||
if (!capabilityRegistry?.validateAutoRunStart) {
|
||||
return { ok: true, errors: [] };
|
||||
}
|
||||
return capabilityRegistry.validateAutoRunStart({
|
||||
activeFlowId: options?.activeFlowId ?? validationState?.activeFlowId,
|
||||
panelMode: options?.panelMode ?? validationState?.panelMode,
|
||||
signupMethod: options?.signupMethod ?? validationState?.signupMethod,
|
||||
state: validationState,
|
||||
});
|
||||
},
|
||||
validateModeSwitch = (state = {}, options = {}) => {
|
||||
const validationState = options?.state || state;
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
const capabilityRegistry = rootScope.MultiPageFlowCapabilities?.createFlowCapabilityRegistry?.({
|
||||
defaultFlowId: 'openai',
|
||||
}) || null;
|
||||
if (!capabilityRegistry?.validateModeSwitch) {
|
||||
return {
|
||||
ok: true,
|
||||
changedKeys: Array.isArray(options?.changedKeys) ? options.changedKeys : [],
|
||||
errors: [],
|
||||
normalizedUpdates: {},
|
||||
};
|
||||
}
|
||||
return capabilityRegistry.validateModeSwitch({
|
||||
activeFlowId: options?.activeFlowId ?? validationState?.activeFlowId,
|
||||
changedKeys: options?.changedKeys,
|
||||
panelMode: options?.panelMode ?? validationState?.panelMode,
|
||||
signupMethod: options?.signupMethod ?? validationState?.signupMethod,
|
||||
state: validationState,
|
||||
});
|
||||
},
|
||||
getTabId,
|
||||
getStopRequested,
|
||||
handleAutoRunLoopUnhandledError,
|
||||
@@ -421,6 +475,9 @@
|
||||
if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null;
|
||||
if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null;
|
||||
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||||
if (payload.sub2apiGroupIds !== undefined) updates.sub2apiGroupIds = Array.isArray(payload.sub2apiGroupIds)
|
||||
? payload.sub2apiGroupIds
|
||||
: [];
|
||||
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||||
if (payload.cpaOAuthState !== undefined) updates.cpaOAuthState = payload.cpaOAuthState || null;
|
||||
@@ -437,6 +494,7 @@
|
||||
const stepKey = getStepKeyForState(step, stateForStep);
|
||||
|
||||
if (stepKey === 'oauth-login') {
|
||||
await syncStepAccountIdentityFromPayload(payload);
|
||||
if (payload.skipLoginVerificationStep) {
|
||||
await setState({ loginVerificationRequestedAt: null });
|
||||
const latestState = await getState();
|
||||
@@ -938,6 +996,10 @@
|
||||
}
|
||||
}
|
||||
const state = await getState();
|
||||
const autoRunStartValidation = validateAutoRunStart(state, { state });
|
||||
if (autoRunStartValidation?.ok === false) {
|
||||
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
|
||||
}
|
||||
if (getPendingAutoRunTimerPlan(state)) {
|
||||
throw new Error('已有自动运行倒计时计划,请先取消或立即开始。');
|
||||
}
|
||||
@@ -965,6 +1027,11 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
const state = await getState();
|
||||
const autoRunStartValidation = validateAutoRunStart(state, { state });
|
||||
if (autoRunStartValidation?.ok === false) {
|
||||
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
|
||||
}
|
||||
const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1);
|
||||
return await scheduleAutoRun(totalRuns, {
|
||||
delayMinutes: message.payload?.delayMinutes,
|
||||
@@ -1036,6 +1103,16 @@
|
||||
const currentState = await getState();
|
||||
const updates = buildPersistentSettingsPayload(message.payload || {});
|
||||
const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {});
|
||||
const modeValidation = validateModeSwitch({
|
||||
...currentState,
|
||||
...updates,
|
||||
resolvedSignupMethod: null,
|
||||
}, {
|
||||
changedKeys: Object.keys(updates),
|
||||
});
|
||||
if (modeValidation?.normalizedUpdates && Object.keys(modeValidation.normalizedUpdates).length > 0) {
|
||||
Object.assign(updates, modeValidation.normalizedUpdates);
|
||||
}
|
||||
const nextSignupState = {
|
||||
...currentState,
|
||||
...updates,
|
||||
@@ -1045,6 +1122,9 @@
|
||||
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')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'contributionMode')
|
||||
) {
|
||||
updates.signupMethod = resolveSignupMethod(nextSignupState);
|
||||
}
|
||||
@@ -1145,7 +1225,12 @@
|
||||
);
|
||||
await addLog(`Plus 支付方式已切换为 ${selectedPlusPaymentMethod},已更新对应的 Plus 步骤。`, 'info');
|
||||
}
|
||||
return { ok: true, state: await getState(), proxyRouting };
|
||||
return {
|
||||
ok: true,
|
||||
modeValidation,
|
||||
proxyRouting,
|
||||
state: await getState(),
|
||||
};
|
||||
}
|
||||
|
||||
case 'REFRESH_GPC_CARD_BALANCE': {
|
||||
|
||||
@@ -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':
|
||||
|
||||
+24
-46
@@ -19,6 +19,25 @@
|
||||
SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||
} = deps;
|
||||
|
||||
let sub2ApiApi = null;
|
||||
|
||||
function getSub2ApiApi() {
|
||||
if (sub2ApiApi) {
|
||||
return sub2ApiApi;
|
||||
}
|
||||
const factory = deps.createSub2ApiApi
|
||||
|| self.MultiPageBackgroundSub2ApiApi?.createSub2ApiApi;
|
||||
if (typeof factory !== 'function') {
|
||||
throw new Error('SUB2API 直连接口模块未加载,无法生成 OAuth 链接。');
|
||||
}
|
||||
sub2ApiApi = factory({
|
||||
addLog,
|
||||
normalizeSub2ApiUrl,
|
||||
DEFAULT_SUB2API_GROUP_NAME,
|
||||
});
|
||||
return sub2ApiApi;
|
||||
}
|
||||
|
||||
function normalizeAdminKey(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
@@ -250,7 +269,6 @@
|
||||
async function requestSub2ApiOAuthUrl(state, options = {}) {
|
||||
const { logLabel = 'OAuth 刷新' } = options;
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||
const groupName = (state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME).trim() || DEFAULT_SUB2API_GROUP_NAME;
|
||||
|
||||
if (!sub2apiUrl) {
|
||||
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
|
||||
@@ -262,54 +280,14 @@
|
||||
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
await addLog(`${logLabel}:正在打开 SUB2API 后台...`);
|
||||
|
||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl);
|
||||
|
||||
const tab = typeof createAutomationTab === 'function'
|
||||
? await createAutomationTab({ url: sub2apiUrl, active: true })
|
||||
: await chrome.tabs.create({ url: sub2apiUrl, active: true });
|
||||
const tabId = tab.id;
|
||||
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
||||
|
||||
await addLog(`${logLabel}:SUB2API 页面已打开,正在等待页面进入目标地址...`);
|
||||
const matchedTab = await waitForTabUrlFamily('sub2api-panel', tabId, sub2apiUrl, {
|
||||
timeoutMs: 15000,
|
||||
retryDelayMs: 400,
|
||||
});
|
||||
if (!matchedTab) {
|
||||
await addLog(`${logLabel}:SUB2API 页面尚未稳定,继续尝试连接内容脚本...`, 'warn');
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('sub2api-panel', tabId, {
|
||||
inject: injectFiles,
|
||||
injectSource: 'sub2api-panel',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: `${logLabel}:SUB2API 页面仍在加载,正在重试连接内容脚本...`,
|
||||
});
|
||||
|
||||
const result = await sendToContentScript('sub2api-panel', {
|
||||
type: 'REQUEST_OAUTH_URL',
|
||||
source: 'background',
|
||||
payload: {
|
||||
const api = getSub2ApiApi();
|
||||
return api.generateOpenAiAuthUrl({
|
||||
...state,
|
||||
sub2apiUrl,
|
||||
sub2apiEmail: state.sub2apiEmail,
|
||||
sub2apiPassword: state.sub2apiPassword,
|
||||
sub2apiGroupName: groupName,
|
||||
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
|
||||
sub2apiAccountPriority: state.sub2apiAccountPriority,
|
||||
logStep: 7,
|
||||
},
|
||||
}, {
|
||||
responseTimeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||
logLabel,
|
||||
timeoutMs: SUB2API_STEP1_RESPONSE_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -19,9 +19,29 @@
|
||||
sendToContentScript,
|
||||
sendToContentScriptResilient,
|
||||
shouldBypassStep9ForLocalCpa,
|
||||
DEFAULT_SUB2API_GROUP_NAME = 'codex',
|
||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||
} = deps;
|
||||
|
||||
let sub2ApiApi = null;
|
||||
|
||||
function getSub2ApiApi() {
|
||||
if (sub2ApiApi) {
|
||||
return sub2ApiApi;
|
||||
}
|
||||
const factory = deps.createSub2ApiApi
|
||||
|| self.MultiPageBackgroundSub2ApiApi?.createSub2ApiApi;
|
||||
if (typeof factory !== 'function') {
|
||||
throw new Error('SUB2API 直连接口模块未加载,无法提交回调。');
|
||||
}
|
||||
sub2ApiApi = factory({
|
||||
addLog,
|
||||
normalizeSub2ApiUrl,
|
||||
DEFAULT_SUB2API_GROUP_NAME,
|
||||
});
|
||||
return sub2ApiApi;
|
||||
}
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
@@ -346,62 +366,22 @@
|
||||
if (!sub2apiUrl) {
|
||||
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
|
||||
}
|
||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||
|
||||
await addStepLog(visibleStep, '正在打开 SUB2API 后台...');
|
||||
|
||||
let tabId = await getTabId('sub2api-panel');
|
||||
const alive = tabId && await isTabAlive('sub2api-panel');
|
||||
|
||||
if (!alive) {
|
||||
tabId = await reuseOrCreateTab('sub2api-panel', sub2apiUrl, {
|
||||
inject: injectFiles,
|
||||
injectSource: 'sub2api-panel',
|
||||
reloadIfSameUrl: true,
|
||||
});
|
||||
} else {
|
||||
await closeConflictingTabsForSource('sub2api-panel', sub2apiUrl, { excludeTabIds: [tabId] });
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
await rememberSourceLastUrl('sub2api-panel', sub2apiUrl);
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('sub2api-panel', tabId, {
|
||||
inject: injectFiles,
|
||||
injectSource: 'sub2api-panel',
|
||||
});
|
||||
|
||||
await addStepLog(visibleStep, '正在向 SUB2API 提交回调并创建账号...');
|
||||
const requestMessage = {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: platformVerifyStep,
|
||||
source: 'background',
|
||||
payload: {
|
||||
localhostUrl: state.localhostUrl,
|
||||
visibleStep,
|
||||
sub2apiUrl,
|
||||
sub2apiEmail: state.sub2apiEmail,
|
||||
sub2apiPassword: state.sub2apiPassword,
|
||||
sub2apiGroupName: state.sub2apiGroupName,
|
||||
sub2apiDefaultProxyName: state.sub2apiDefaultProxyName,
|
||||
sub2apiAccountPriority: state.sub2apiAccountPriority,
|
||||
sub2apiProxyId: state.sub2apiProxyId,
|
||||
sub2apiSessionId: state.sub2apiSessionId,
|
||||
sub2apiOAuthState: state.sub2apiOAuthState,
|
||||
sub2apiGroupId: state.sub2apiGroupId,
|
||||
sub2apiGroupIds: state.sub2apiGroupIds,
|
||||
sub2apiDraftName: state.sub2apiDraftName,
|
||||
},
|
||||
};
|
||||
const api = getSub2ApiApi();
|
||||
const maxExchangeAttempts = 3;
|
||||
let lastError = null;
|
||||
for (let attempt = 1; attempt <= maxExchangeAttempts; attempt += 1) {
|
||||
try {
|
||||
const result = await sendToContentScript('sub2api-panel', requestMessage, {
|
||||
responseTimeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||
const result = await api.submitOpenAiCallback({
|
||||
...state,
|
||||
visibleStep,
|
||||
sub2apiUrl,
|
||||
}, {
|
||||
visibleStep,
|
||||
logLabel: `步骤 ${visibleStep}`,
|
||||
logOptions: { step: visibleStep, stepKey: 'platform-verify' },
|
||||
timeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
await completeStepFromBackground(platformVerifyStep, result);
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
(function attachBackgroundSub2ApiApi(root, factory) {
|
||||
root.MultiPageBackgroundSub2ApiApi = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundSub2ApiApiModule() {
|
||||
function createSub2ApiApi(deps = {}) {
|
||||
const {
|
||||
addLog = async () => {},
|
||||
normalizeSub2ApiUrl = (value) => value,
|
||||
DEFAULT_SUB2API_GROUP_NAME = 'codex',
|
||||
fetchImpl = (...args) => fetch(...args),
|
||||
} = deps;
|
||||
|
||||
const DEFAULT_REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
||||
const DEFAULT_PROXY_NAME = '';
|
||||
const DEFAULT_CONCURRENCY = 10;
|
||||
const DEFAULT_PRIORITY = 1;
|
||||
const DEFAULT_RATE_MULTIPLIER = 1;
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function extractStateFromAuthUrl(authUrl = '') {
|
||||
try {
|
||||
return new URL(authUrl).searchParams.get('state') || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRedirectUri(input = DEFAULT_REDIRECT_URI) {
|
||||
const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
|
||||
const parsed = new URL(withProtocol);
|
||||
if (!parsed.pathname || parsed.pathname === '/') {
|
||||
parsed.pathname = '/auth/callback';
|
||||
}
|
||||
if (parsed.pathname !== '/auth/callback') {
|
||||
throw new Error('SUB2API 回调地址必须是 /auth/callback,例如 http://localhost:1455/auth/callback');
|
||||
}
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function getSub2ApiOrigin(rawUrl = '') {
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(rawUrl);
|
||||
if (!sub2apiUrl) {
|
||||
throw new Error('SUB2API URL is not configured. Please fill it in the side panel first.');
|
||||
}
|
||||
try {
|
||||
return new URL(sub2apiUrl).origin;
|
||||
} catch {
|
||||
throw new Error('SUB2API URL 格式无效,请先在侧边栏检查。');
|
||||
}
|
||||
}
|
||||
|
||||
function getSub2ApiErrorMessage(payload, responseStatus = 500, path = '') {
|
||||
const candidates = [
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.error,
|
||||
payload?.reason,
|
||||
];
|
||||
const message = candidates.map(normalizeString).find(Boolean);
|
||||
return message || `SUB2API 请求失败(HTTP ${responseStatus}):${path}`;
|
||||
}
|
||||
|
||||
async function requestJson(origin, path, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const token = normalizeString(options.token);
|
||||
const response = await fetchImpl(`${origin}${path}`, {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
let payload = null;
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
payload = null;
|
||||
}
|
||||
|
||||
if (payload && typeof payload === 'object' && Object.prototype.hasOwnProperty.call(payload, 'code')) {
|
||||
if (Number(payload.code) === 0) {
|
||||
return payload.data;
|
||||
}
|
||||
throw new Error(getSub2ApiErrorMessage(payload, response.status, path));
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getSub2ApiErrorMessage(payload, response.status, path));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error(`SUB2API 请求超时:${path}`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function loginSub2Api(state = {}, options = {}) {
|
||||
const email = normalizeString(state.sub2apiEmail);
|
||||
const password = String(state.sub2apiPassword || '');
|
||||
const origin = getSub2ApiOrigin(state.sub2apiUrl);
|
||||
|
||||
if (!email) {
|
||||
throw new Error('尚未配置 SUB2API 登录邮箱,请先在侧边栏填写。');
|
||||
}
|
||||
if (!password) {
|
||||
throw new Error('尚未配置 SUB2API 登录密码,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const loginData = await requestJson(origin, '/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
timeoutMs: options.timeoutMs,
|
||||
body: { email, password },
|
||||
});
|
||||
|
||||
const token = normalizeString(loginData?.access_token || loginData?.accessToken);
|
||||
if (!token) {
|
||||
throw new Error('SUB2API 登录返回缺少 access_token。');
|
||||
}
|
||||
|
||||
return {
|
||||
origin,
|
||||
token,
|
||||
user: loginData?.user || null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSub2ApiGroupNames(value) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '').split(/[\r\n,,;;]+/);
|
||||
const seen = new Set();
|
||||
const names = [];
|
||||
for (const item of source) {
|
||||
const name = normalizeString(item);
|
||||
const key = name.toLowerCase();
|
||||
if (!name || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
names.push(name);
|
||||
}
|
||||
return names.length ? names : [DEFAULT_SUB2API_GROUP_NAME];
|
||||
}
|
||||
|
||||
async function getGroupsByNames(origin, token, groupNames, options = {}) {
|
||||
const targetNames = normalizeSub2ApiGroupNames(groupNames);
|
||||
const groups = await requestJson(origin, '/api/v1/admin/groups/all', {
|
||||
method: 'GET',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
});
|
||||
const matched = [];
|
||||
const missing = [];
|
||||
|
||||
for (const targetName of targetNames) {
|
||||
const normalized = targetName.toLowerCase();
|
||||
const group = (Array.isArray(groups) ? groups : []).find((item) => {
|
||||
const itemName = normalizeString(item?.name).toLowerCase();
|
||||
if (!itemName || itemName !== normalized) return false;
|
||||
return !item.platform || item.platform === 'openai';
|
||||
});
|
||||
if (group) {
|
||||
matched.push(group);
|
||||
} else {
|
||||
missing.push(targetName);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length) {
|
||||
throw new Error(`SUB2API 中未找到以下 openai 分组:${missing.join('、')}。`);
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
function normalizeSub2ApiProxyPreference(value) {
|
||||
return normalizeString(value);
|
||||
}
|
||||
|
||||
function resolveSub2ApiProxyPreference(state = {}) {
|
||||
if (state.sub2apiDefaultProxyName !== undefined) {
|
||||
return normalizeSub2ApiProxyPreference(state.sub2apiDefaultProxyName);
|
||||
}
|
||||
return DEFAULT_PROXY_NAME;
|
||||
}
|
||||
|
||||
function resolveSub2ApiAccountPriority(state = {}) {
|
||||
const rawValue = normalizeString(state.sub2apiAccountPriority);
|
||||
if (!rawValue) {
|
||||
return DEFAULT_PRIORITY;
|
||||
}
|
||||
const numeric = Number(rawValue);
|
||||
if (!Number.isSafeInteger(numeric) || numeric < 1) {
|
||||
throw new Error('SUB2API 账号优先级必须是大于等于 1 的整数。');
|
||||
}
|
||||
return numeric;
|
||||
}
|
||||
|
||||
function normalizeProxyId(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return null;
|
||||
}
|
||||
const normalized = Number(value);
|
||||
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function buildProxyDisplayName(proxy = {}) {
|
||||
const id = normalizeProxyId(proxy.id);
|
||||
const name = normalizeString(proxy.name);
|
||||
const protocol = normalizeString(proxy.protocol);
|
||||
const host = normalizeString(proxy.host);
|
||||
const port = proxy.port === undefined || proxy.port === null ? '' : normalizeString(proxy.port);
|
||||
const address = protocol && host && port ? `${protocol}://${host}:${port}` : '';
|
||||
return [
|
||||
name || '(未命名代理)',
|
||||
id ? `#${id}` : '',
|
||||
address,
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function buildProxySearchText(proxy = {}) {
|
||||
return [
|
||||
proxy.id,
|
||||
proxy.name,
|
||||
proxy.protocol,
|
||||
proxy.host,
|
||||
proxy.port,
|
||||
buildProxyDisplayName(proxy),
|
||||
]
|
||||
.filter((value) => value !== undefined && value !== null && value !== '')
|
||||
.map((value) => normalizeString(value).toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function isActiveProxy(proxy = {}) {
|
||||
const status = normalizeString(proxy.status).toLowerCase();
|
||||
return !status || status === 'active';
|
||||
}
|
||||
|
||||
function findSub2ApiProxy(proxies = [], preference = '') {
|
||||
const activeProxies = (Array.isArray(proxies) ? proxies : [])
|
||||
.filter(isActiveProxy)
|
||||
.filter((proxy) => normalizeProxyId(proxy.id));
|
||||
const normalizedPreference = normalizeSub2ApiProxyPreference(preference).toLowerCase();
|
||||
const preferredId = normalizeProxyId(normalizedPreference);
|
||||
|
||||
if (preferredId) {
|
||||
const matchedById = activeProxies.find((proxy) => normalizeProxyId(proxy.id) === preferredId);
|
||||
return {
|
||||
proxy: matchedById || null,
|
||||
reason: matchedById ? 'id' : 'missing-id',
|
||||
candidates: activeProxies,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedPreference) {
|
||||
const exactMatches = activeProxies.filter((proxy) => normalizeString(proxy.name).toLowerCase() === normalizedPreference);
|
||||
if (exactMatches.length === 1) {
|
||||
return { proxy: exactMatches[0], reason: 'name', candidates: activeProxies };
|
||||
}
|
||||
if (exactMatches.length > 1) {
|
||||
return { proxy: null, reason: 'ambiguous-name', candidates: exactMatches };
|
||||
}
|
||||
|
||||
const fuzzyMatches = activeProxies.filter((proxy) => buildProxySearchText(proxy).includes(normalizedPreference));
|
||||
if (fuzzyMatches.length === 1) {
|
||||
return { proxy: fuzzyMatches[0], reason: 'fuzzy', candidates: activeProxies };
|
||||
}
|
||||
if (fuzzyMatches.length > 1) {
|
||||
return { proxy: null, reason: 'ambiguous-fuzzy', candidates: fuzzyMatches };
|
||||
}
|
||||
|
||||
return { proxy: null, reason: 'missing-name', candidates: activeProxies };
|
||||
}
|
||||
|
||||
if (activeProxies.length === 1) {
|
||||
return { proxy: activeProxies[0], reason: 'single-active', candidates: activeProxies };
|
||||
}
|
||||
return {
|
||||
proxy: null,
|
||||
reason: activeProxies.length ? 'no-preference' : 'none-active',
|
||||
candidates: activeProxies,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveSub2ApiProxy(origin, token, preference = '', options = {}) {
|
||||
const proxies = await requestJson(origin, '/api/v1/admin/proxies/all?with_count=true', {
|
||||
method: 'GET',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
});
|
||||
if (!Array.isArray(proxies)) {
|
||||
throw new Error('SUB2API 代理列表返回格式异常,无法自动选择代理。');
|
||||
}
|
||||
|
||||
const { proxy, reason, candidates } = findSub2ApiProxy(proxies, preference);
|
||||
if (proxy) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
const configured = normalizeSub2ApiProxyPreference(preference) || '(未配置)';
|
||||
const available = (candidates || [])
|
||||
.slice(0, 8)
|
||||
.map(buildProxyDisplayName)
|
||||
.join(';') || '无可用代理';
|
||||
if (reason === 'ambiguous-name' || reason === 'ambiguous-fuzzy') {
|
||||
throw new Error(`SUB2API 默认代理“${configured}”匹配到多个代理,请改填代理 ID。候选:${available}`);
|
||||
}
|
||||
if (reason === 'missing-id') {
|
||||
throw new Error(`SUB2API 默认代理 ID “${configured}”不存在或未启用。可用代理:${available}`);
|
||||
}
|
||||
if (reason === 'missing-name') {
|
||||
throw new Error(`SUB2API 默认代理“${configured}”不存在或未启用。可用代理:${available}`);
|
||||
}
|
||||
if (reason === 'no-preference') {
|
||||
throw new Error(`SUB2API 存在多个可用代理,请在侧边栏填写默认代理名称或 ID;留空则不使用代理。可用代理:${available}`);
|
||||
}
|
||||
throw new Error('SUB2API 没有可用代理;请检查默认代理配置,或将其留空以禁用代理。');
|
||||
}
|
||||
|
||||
function buildDraftAccountName(groupName) {
|
||||
const prefix = normalizeString(groupName || DEFAULT_SUB2API_GROUP_NAME)
|
||||
.replace(/[^\w\u4e00-\u9fa5-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '') || DEFAULT_SUB2API_GROUP_NAME;
|
||||
const stamp = new Date().toISOString().replace(/\D/g, '').slice(2, 14);
|
||||
const random = Math.floor(Math.random() * 9000 + 1000);
|
||||
return `${prefix}-${stamp}-${random}`;
|
||||
}
|
||||
|
||||
function parseLocalhostCallback(rawUrl, visibleStep = 10) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(rawUrl);
|
||||
} catch {
|
||||
throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址格式无效。`);
|
||||
}
|
||||
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
throw new Error('回调 URL 协议不正确。');
|
||||
}
|
||||
if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) {
|
||||
throw new Error(`步骤 ${visibleStep} 只接受 localhost / 127.0.0.1 回调地址。`);
|
||||
}
|
||||
if (parsed.pathname !== '/auth/callback') {
|
||||
throw new Error('回调 URL 路径必须是 /auth/callback。');
|
||||
}
|
||||
|
||||
const code = normalizeString(parsed.searchParams.get('code'));
|
||||
const state = normalizeString(parsed.searchParams.get('state'));
|
||||
if (!code || !state) {
|
||||
throw new Error('回调 URL 中缺少 code 或 state。');
|
||||
}
|
||||
|
||||
return {
|
||||
url: parsed.toString(),
|
||||
code,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
function buildOpenAiCredentials(exchangeData) {
|
||||
const credentials = {};
|
||||
const allowedKeys = [
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'id_token',
|
||||
'expires_at',
|
||||
'email',
|
||||
'chatgpt_account_id',
|
||||
'chatgpt_user_id',
|
||||
'organization_id',
|
||||
'plan_type',
|
||||
'client_id',
|
||||
];
|
||||
|
||||
for (const key of allowedKeys) {
|
||||
if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
|
||||
credentials[key] = exchangeData[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!credentials.access_token) {
|
||||
throw new Error('SUB2API 交换授权码后未返回 access_token。');
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
function buildOpenAiExtra(exchangeData) {
|
||||
const extra = {};
|
||||
const allowedKeys = ['email', 'name', 'privacy_mode'];
|
||||
|
||||
for (const key of allowedKeys) {
|
||||
if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
|
||||
extra[key] = exchangeData[key];
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(extra).length ? extra : undefined;
|
||||
}
|
||||
|
||||
async function logWithOptions(message, level = 'info', options = {}) {
|
||||
await addLog(message, level, options.logOptions || {});
|
||||
}
|
||||
|
||||
async function generateOpenAiAuthUrl(state = {}, options = {}) {
|
||||
const logLabel = normalizeString(options.logLabel) || 'OAuth 刷新';
|
||||
const redirectUri = normalizeRedirectUri(options.redirectUri || DEFAULT_REDIRECT_URI);
|
||||
const groupNames = normalizeSub2ApiGroupNames(state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME);
|
||||
const groupName = groupNames[0] || DEFAULT_SUB2API_GROUP_NAME;
|
||||
|
||||
await logWithOptions(`${logLabel}:正在通过 SUB2API 管理接口登录并生成 OpenAI Auth 链接...`, 'info', options);
|
||||
const { origin, token } = await loginSub2Api(state, options);
|
||||
const groups = await getGroupsByNames(origin, token, groupNames, options);
|
||||
const group = groups[0];
|
||||
const proxyPreference = resolveSub2ApiProxyPreference(state);
|
||||
const proxy = proxyPreference ? await resolveSub2ApiProxy(origin, token, proxyPreference, options) : null;
|
||||
const proxyId = normalizeProxyId(proxy?.id);
|
||||
const draftName = buildDraftAccountName(group.name || groupName);
|
||||
const groupLabel = groups.map((item) => `${item.name}(#${item.id})`).join('、');
|
||||
|
||||
await logWithOptions(`${logLabel}:已登录 SUB2API,使用分组 ${groupLabel}。`, 'info', options);
|
||||
if (proxy) {
|
||||
await logWithOptions(`${logLabel}:已选择 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`, 'info', options);
|
||||
} else {
|
||||
await logWithOptions(`${logLabel}:未配置 SUB2API 默认代理,本次将不使用代理。`, 'info', options);
|
||||
}
|
||||
|
||||
const authRequestBody = { redirect_uri: redirectUri };
|
||||
if (proxyId) {
|
||||
authRequestBody.proxy_id = proxyId;
|
||||
}
|
||||
|
||||
const authData = await requestJson(origin, '/api/v1/admin/openai/generate-auth-url', {
|
||||
method: 'POST',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
body: authRequestBody,
|
||||
});
|
||||
|
||||
const oauthUrl = normalizeString(authData?.auth_url || authData?.authUrl);
|
||||
const sessionId = normalizeString(authData?.session_id || authData?.sessionId);
|
||||
const oauthState = normalizeString(authData?.state || extractStateFromAuthUrl(oauthUrl));
|
||||
|
||||
if (!oauthUrl || !sessionId) {
|
||||
throw new Error('SUB2API 未返回完整的 auth_url / session_id。');
|
||||
}
|
||||
|
||||
await logWithOptions(`${logLabel}:已获取 SUB2API OAuth 链接:${oauthUrl.slice(0, 96)}...`, 'ok', options);
|
||||
return {
|
||||
oauthUrl,
|
||||
sub2apiSessionId: sessionId,
|
||||
sub2apiOAuthState: oauthState,
|
||||
sub2apiGroupId: group.id,
|
||||
sub2apiGroupIds: groups.map((item) => item.id),
|
||||
sub2apiDraftName: draftName,
|
||||
sub2apiProxyId: proxyId,
|
||||
};
|
||||
}
|
||||
|
||||
async function submitOpenAiCallback(state = {}, options = {}) {
|
||||
const visibleStep = Number(options.visibleStep || state.visibleStep) || 10;
|
||||
const callback = parseLocalhostCallback(state.localhostUrl || '', visibleStep);
|
||||
const flowEmail = normalizeString(state.email);
|
||||
const sessionId = normalizeString(state.sub2apiSessionId);
|
||||
const expectedState = normalizeString(state.sub2apiOAuthState);
|
||||
const logLabel = normalizeString(options.logLabel) || `步骤 ${visibleStep}`;
|
||||
|
||||
if (!sessionId) {
|
||||
throw new Error('缺少 SUB2API session_id,请重新执行步骤 1。');
|
||||
}
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error('本次 localhost 回调中的 state 与步骤 1 生成的 state 不一致,请重新执行步骤 1。');
|
||||
}
|
||||
|
||||
const { origin, token } = await loginSub2Api(state, options);
|
||||
const proxyPreference = resolveSub2ApiProxyPreference(state);
|
||||
const preferredProxyId = normalizeProxyId(state.sub2apiProxyId);
|
||||
const proxySelector = preferredProxyId || proxyPreference;
|
||||
const proxy = proxySelector ? await resolveSub2ApiProxy(origin, token, proxySelector, options) : null;
|
||||
const proxyId = normalizeProxyId(proxy?.id);
|
||||
const accountPriority = resolveSub2ApiAccountPriority(state);
|
||||
const storedGroupIds = Array.isArray(state.sub2apiGroupIds) ? state.sub2apiGroupIds : [];
|
||||
const groupIdsFromState = storedGroupIds
|
||||
.map((id) => Number(id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
const groups = groupIdsFromState.length
|
||||
? groupIdsFromState.map((id) => ({ id }))
|
||||
: (state.sub2apiGroupId
|
||||
? [{ id: state.sub2apiGroupId, name: state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME }]
|
||||
: await getGroupsByNames(origin, token, state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME, options));
|
||||
|
||||
await logWithOptions(`${logLabel}:正在通过 SUB2API 管理接口交换 OpenAI 授权码...`, 'info', options);
|
||||
if (proxy) {
|
||||
await logWithOptions(`${logLabel}:使用 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`, 'info', options);
|
||||
} else {
|
||||
await logWithOptions(`${logLabel}:未配置 SUB2API 默认代理,本次将不使用代理。`, 'info', options);
|
||||
}
|
||||
|
||||
const exchangeRequestBody = {
|
||||
session_id: sessionId,
|
||||
code: callback.code,
|
||||
state: callback.state,
|
||||
};
|
||||
if (proxyId) {
|
||||
exchangeRequestBody.proxy_id = proxyId;
|
||||
}
|
||||
|
||||
const exchangeData = await requestJson(origin, '/api/v1/admin/openai/exchange-code', {
|
||||
method: 'POST',
|
||||
token,
|
||||
timeoutMs: options.timeoutMs,
|
||||
body: exchangeRequestBody,
|
||||
});
|
||||
|
||||
const credentials = buildOpenAiCredentials(exchangeData);
|
||||
const extra = buildOpenAiExtra(exchangeData);
|
||||
const resolvedEmail = normalizeString(exchangeData?.email || credentials?.email);
|
||||
const groupIds = groups
|
||||
.map((group) => Number(group.id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
if (!groupIds.length) {
|
||||
throw new Error('SUB2API 返回的目标分组 ID 无效。');
|
||||
}
|
||||
|
||||
const accountName = resolvedEmail
|
||||
|| flowEmail
|
||||
|| normalizeString(state.sub2apiDraftName)
|
||||
|| buildDraftAccountName(state.sub2apiGroupName || DEFAULT_SUB2API_GROUP_NAME);
|
||||
const createPayload = {
|
||||
name: accountName,
|
||||
notes: '',
|
||||
platform: 'openai',
|
||||
type: 'oauth',
|
||||
credentials,
|
||||
concurrency: DEFAULT_CONCURRENCY,
|
||||
priority: accountPriority,
|
||||
rate_multiplier: DEFAULT_RATE_MULTIPLIER,
|
||||
group_ids: groupIds,
|
||||
auto_pause_on_expired: true,
|
||||
};
|
||||
if (proxyId) {
|
||||
createPayload.proxy_id = proxyId;
|
||||
}
|
||||
if (extra) {
|
||||
createPayload.extra = extra;
|
||||
}
|
||||
|
||||
await logWithOptions(`${logLabel}:授权码交换成功,正在创建 SUB2API 账号(名称:${accountName})...`, 'info', options);
|
||||
const createdAccount = await requestJson(origin, '/api/v1/admin/accounts', {
|
||||
method: 'POST',
|
||||
token,
|
||||
timeoutMs: options.createTimeoutMs,
|
||||
body: createPayload,
|
||||
});
|
||||
|
||||
const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`;
|
||||
await logWithOptions(verifiedStatus, 'ok', options);
|
||||
return {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
buildDraftAccountName,
|
||||
buildOpenAiCredentials,
|
||||
buildOpenAiExtra,
|
||||
buildProxyDisplayName,
|
||||
extractStateFromAuthUrl,
|
||||
generateOpenAiAuthUrl,
|
||||
getGroupsByNames,
|
||||
loginSub2Api,
|
||||
normalizeProxyId,
|
||||
normalizeRedirectUri,
|
||||
normalizeSub2ApiGroupNames,
|
||||
parseLocalhostCallback,
|
||||
requestJson,
|
||||
resolveSub2ApiAccountPriority,
|
||||
resolveSub2ApiProxy,
|
||||
submitOpenAiCallback,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createSub2ApiApi,
|
||||
};
|
||||
});
|
||||
+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,27 +409,25 @@
|
||||
}
|
||||
|
||||
function getVerificationPollPayload(step, state, overrides = {}) {
|
||||
if (typeof externalBuildVerificationPollPayload === 'function') {
|
||||
return externalBuildVerificationPollPayload(step, state, overrides);
|
||||
}
|
||||
const normalizedStep = Number(step) === 4 ? 4 : 8;
|
||||
const is2925Provider = state?.mailProvider === '2925';
|
||||
const mail2925MatchTargetEmail = is2925Provider
|
||||
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive';
|
||||
if (step === 4) {
|
||||
return {
|
||||
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(4, state),
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
|
||||
targetEmail: state.email,
|
||||
mail2925MatchTargetEmail,
|
||||
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
|
||||
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(8, state),
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
||||
targetEmail: String(state?.step8VerificationTargetEmail || '').trim() || state.email,
|
||||
flowId: String(state?.activeFlowId || '').trim(),
|
||||
step: normalizedStep,
|
||||
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(normalizedStep, state),
|
||||
senderFilters: [],
|
||||
subjectFilters: [],
|
||||
requiredKeywords: [],
|
||||
codePatterns: [],
|
||||
targetEmail: normalizedStep === 4
|
||||
? state.email
|
||||
: (String(state?.step8VerificationTargetEmail || '').trim() || state.email),
|
||||
targetEmailHints: [],
|
||||
mail2925MatchTargetEmail,
|
||||
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
|
||||
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
|
||||
|
||||
+47
-8
@@ -98,6 +98,39 @@ function getTargetEmailMatchState(text, targetEmail) {
|
||||
return { matches: false, hasExplicitEmail: false };
|
||||
}
|
||||
|
||||
function normalizeRulePatternList(patterns = []) {
|
||||
return Array.isArray(patterns) ? patterns : [];
|
||||
}
|
||||
|
||||
function extractCodeByRulePatterns(text, patterns = []) {
|
||||
const normalizedText = String(text || '');
|
||||
for (const pattern of normalizeRulePatternList(patterns)) {
|
||||
try {
|
||||
const source = String(pattern?.source || '').trim();
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
|
||||
const match = normalizedText.match(new RegExp(source, flags));
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
for (let index = 1; index < match.length; index += 1) {
|
||||
const candidate = String(match[index] || '').trim();
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
if (String(match[0] || '').trim()) {
|
||||
return String(match[0] || '').trim();
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore invalid runtime rule patterns and continue with other candidates.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const MONTH_INDEX_MAP = {
|
||||
jan: 0,
|
||||
feb: 1,
|
||||
@@ -165,16 +198,17 @@ function parseGmailTimestampText(rawText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
function extractVerificationCode(text, options = {}) {
|
||||
const normalized = String(text || '');
|
||||
const matchedByRule = extractCodeByRulePatterns(normalized, options?.codePatterns);
|
||||
if (matchedByRule) {
|
||||
return matchedByRule;
|
||||
}
|
||||
|
||||
const cnMatch = normalized.match(/(?:验证码|代码)[^0-9]{0,16}(\d{6})/i);
|
||||
if (cnMatch) return cnMatch[1];
|
||||
|
||||
const openAiLoginMatch = normalized.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (openAiLoginMatch) return openAiLoginMatch[1];
|
||||
|
||||
const enMatch = normalized.match(/(?:verification\s+code|temporary\s+verification\s+code|your\s+chatgpt\s+code|code(?:\s+is)?)[^0-9]{0,16}(\d{6})/i);
|
||||
const enMatch = normalized.match(/(?:verification\s+code|temporary\s+verification\s+code|log-?in\s+code|enter\s+this\s+code|code(?:\s+is)?)[^0-9]{0,24}(\d{6})/i);
|
||||
if (enMatch) return enMatch[1];
|
||||
|
||||
const plainMatch = normalized.match(/\b(\d{6})\b/);
|
||||
@@ -357,7 +391,7 @@ function collectThreadRows() {
|
||||
if (
|
||||
row.matches('tr.zA')
|
||||
|| row.querySelector('.bog, .y6, .y2, .afn, [data-thread-id], [data-legacy-thread-id], [data-legacy-last-message-id]')
|
||||
|| /openai|chatgpt|verify|verification|code|验证码/i.test(text)
|
||||
|| /verify|verification|code|验证码|log-?in/i.test(text)
|
||||
) {
|
||||
rows.push(row);
|
||||
}
|
||||
@@ -545,6 +579,7 @@ async function openRowAndGetMessageText(row) {
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
const {
|
||||
codePatterns = [],
|
||||
senderFilters = [],
|
||||
subjectFilters = [],
|
||||
maxAttempts = 5,
|
||||
@@ -618,7 +653,9 @@ async function handlePollEmail(step, payload) {
|
||||
}
|
||||
|
||||
const previewTargetState = getTargetEmailMatchState(preview.combinedText, targetEmail);
|
||||
const previewCode = extractVerificationCode(preview.combinedText);
|
||||
const previewCode = extractVerificationCode(preview.combinedText, {
|
||||
codePatterns,
|
||||
});
|
||||
if (previewCode) {
|
||||
if (excludedCodeSet.has(previewCode)) {
|
||||
log(`步骤 ${step}:跳过排除的验证码:${previewCode}`, 'info');
|
||||
@@ -644,7 +681,9 @@ async function handlePollEmail(step, payload) {
|
||||
|
||||
const openedText = await openRowAndGetMessageText(row);
|
||||
const openedTargetState = getTargetEmailMatchState(openedText, targetEmail);
|
||||
const bodyCode = extractVerificationCode(openedText);
|
||||
const bodyCode = extractVerificationCode(openedText, {
|
||||
codePatterns,
|
||||
});
|
||||
if (!bodyCode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+49
-6
@@ -43,6 +43,39 @@ if (shouldHandlePollEmailInCurrentFrame) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function normalizeRulePatternList(patterns = []) {
|
||||
return Array.isArray(patterns) ? patterns : [];
|
||||
}
|
||||
|
||||
function extractCodeByRulePatterns(text, patterns = []) {
|
||||
const normalizedText = String(text || '');
|
||||
for (const pattern of normalizeRulePatternList(patterns)) {
|
||||
try {
|
||||
const source = String(pattern?.source || '').trim();
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
|
||||
const match = normalizedText.match(new RegExp(source, flags));
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
for (let index = 1; index < match.length; index += 1) {
|
||||
const candidate = String(match[index] || '').trim();
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
if (String(match[0] || '').trim()) {
|
||||
return String(match[0] || '').trim();
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore invalid runtime rule patterns and continue with other candidates.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isVisibleElement(node) {
|
||||
return Boolean(node instanceof HTMLElement)
|
||||
&& (Boolean(node.offsetParent) || getComputedStyle(node).position === 'fixed');
|
||||
@@ -82,12 +115,15 @@ if (shouldHandlePollEmailInCurrentFrame) {
|
||||
].join('::')).slice(0, 240);
|
||||
}
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
function extractVerificationCode(text, options = {}) {
|
||||
const matchedByRule = extractCodeByRulePatterns(text, options?.codePatterns);
|
||||
if (matchedByRule) return matchedByRule;
|
||||
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = text.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchLoginCode) return matchLoginCode[1];
|
||||
|
||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
@@ -276,7 +312,14 @@ if (shouldHandlePollEmailInCurrentFrame) {
|
||||
}
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload;
|
||||
const {
|
||||
codePatterns = [],
|
||||
senderFilters,
|
||||
subjectFilters,
|
||||
maxAttempts,
|
||||
intervalMs,
|
||||
excludeCodes = [],
|
||||
} = payload;
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
const FALLBACK_AFTER = 3;
|
||||
const pollSessionKey = normalizePollSessionKey(payload);
|
||||
@@ -327,7 +370,7 @@ if (shouldHandlePollEmailInCurrentFrame) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let code = extractVerificationCode(meta.combinedText);
|
||||
let code = extractVerificationCode(meta.combinedText, { codePatterns });
|
||||
let opened = null;
|
||||
|
||||
if (!code) {
|
||||
@@ -339,7 +382,7 @@ if (shouldHandlePollEmailInCurrentFrame) {
|
||||
if (!openedSenderMatch && !openedSubjectMatch && !senderMatch && !subjectMatch) {
|
||||
continue;
|
||||
}
|
||||
code = extractVerificationCode(opened.combinedText);
|
||||
code = extractVerificationCode(opened.combinedText, { codePatterns });
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
|
||||
@@ -60,12 +60,55 @@ function normalizeText(value) {
|
||||
return (value || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
function normalizeRulePatternList(patterns = []) {
|
||||
return Array.isArray(patterns) ? patterns : [];
|
||||
}
|
||||
|
||||
function extractCodeByRulePatterns(text, patterns = []) {
|
||||
const normalizedText = String(text || '');
|
||||
for (const pattern of normalizeRulePatternList(patterns)) {
|
||||
try {
|
||||
const source = String(pattern?.source || '').trim();
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
|
||||
const match = normalizedText.match(new RegExp(source, flags));
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
for (let index = 1; index < match.length; index += 1) {
|
||||
const candidate = String(match[index] || '').trim();
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
if (String(match[0] || '').trim()) {
|
||||
return String(match[0] || '').trim();
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore invalid runtime rule patterns and continue with other candidates.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function matchesKeywordHints(text, keywords = []) {
|
||||
const normalizedText = normalizeText(text);
|
||||
const normalizedKeywords = Array.isArray(keywords) ? keywords : [];
|
||||
return normalizedKeywords.some((keyword) => normalizedText.includes(normalizeText(keyword)));
|
||||
}
|
||||
|
||||
function extractVerificationCode(text, options = {}) {
|
||||
const matchedByRule = extractCodeByRulePatterns(text, options?.codePatterns);
|
||||
if (matchedByRule) {
|
||||
return matchedByRule;
|
||||
}
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = text.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchLoginCode) return matchLoginCode[1];
|
||||
|
||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
@@ -76,7 +119,7 @@ function extractVerificationCode(text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function rowMatchesFilters(mail, senderFilters, subjectFilters, targetEmail) {
|
||||
function rowMatchesFilters(mail, senderFilters, subjectFilters, targetEmail, options = {}) {
|
||||
const sender = normalizeText(mail.sender);
|
||||
const subject = normalizeText(mail.subject);
|
||||
const mailbox = normalizeText(mail.mailbox);
|
||||
@@ -87,8 +130,12 @@ function rowMatchesFilters(mail, senderFilters, subjectFilters, targetEmail) {
|
||||
const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()) || combined.includes(f.toLowerCase()));
|
||||
const mailboxMatch = Boolean(targetLocal) && mailbox.includes(targetLocal);
|
||||
const forwardedDuck = /duckduckgo|forward(?:ed)?\s*by/i.test(mail.combinedText);
|
||||
const code = extractVerificationCode(mail.combinedText);
|
||||
const keywordMatch = /openai|chatgpt|verify|verification|confirm|log-?in|验证码|代码/.test(combined);
|
||||
const code = extractVerificationCode(mail.combinedText, {
|
||||
codePatterns: options?.codePatterns,
|
||||
});
|
||||
const keywordMatch = options?.requiredKeywords?.length
|
||||
? matchesKeywordHints(combined, options.requiredKeywords)
|
||||
: /verify|verification|confirm|log-?in|验证码|代码/.test(combined);
|
||||
|
||||
if (mailboxMatch) return { matched: true, mailboxMatch, code };
|
||||
if (senderMatch || subjectMatch) return { matched: true, mailboxMatch: false, code };
|
||||
@@ -170,6 +217,8 @@ async function deleteCurrentMailboxMessage(step) {
|
||||
|
||||
async function handleMailboxPollEmail(step, payload) {
|
||||
const {
|
||||
codePatterns = [],
|
||||
requiredKeywords = [],
|
||||
senderFilters = [],
|
||||
subjectFilters = [],
|
||||
maxAttempts = 20,
|
||||
@@ -208,14 +257,19 @@ async function handleMailboxPollEmail(step, payload) {
|
||||
if (seenMailIds.has(mail.mailId)) continue;
|
||||
if (!useFallback && existingMailIds.has(mail.mailId)) continue;
|
||||
|
||||
const match = rowMatchesFilters(mail, senderFilters, subjectFilters, '');
|
||||
const match = rowMatchesFilters(mail, senderFilters, subjectFilters, '', {
|
||||
codePatterns,
|
||||
requiredKeywords,
|
||||
});
|
||||
if (!match.matched) continue;
|
||||
|
||||
candidates.push({ ...mail, code: match.code });
|
||||
}
|
||||
|
||||
for (const mail of candidates) {
|
||||
const code = mail.code || extractVerificationCode(mail.combinedText);
|
||||
const code = mail.code || extractVerificationCode(mail.combinedText, {
|
||||
codePatterns,
|
||||
});
|
||||
if (!code) continue;
|
||||
if (excludedCodeSet.has(code)) {
|
||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
||||
|
||||
+60
-12
@@ -73,6 +73,39 @@ function normalizeText(value) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function normalizeRulePatternList(patterns = []) {
|
||||
return Array.isArray(patterns) ? patterns : [];
|
||||
}
|
||||
|
||||
function extractCodeByRulePatterns(text, patterns = []) {
|
||||
const normalizedText = String(text || '');
|
||||
for (const pattern of normalizeRulePatternList(patterns)) {
|
||||
try {
|
||||
const source = String(pattern?.source || '').trim();
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
|
||||
const match = normalizedText.match(new RegExp(source, flags));
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
for (let index = 1; index < match.length; index += 1) {
|
||||
const candidate = String(match[index] || '').trim();
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
if (String(match[0] || '').trim()) {
|
||||
return String(match[0] || '').trim();
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore invalid runtime rule patterns and continue with other candidates.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getNetEaseMailLabel(hostname) {
|
||||
const currentHostname = String(
|
||||
hostname || (typeof location !== 'undefined' ? location.hostname : '') || ''
|
||||
@@ -129,7 +162,7 @@ function isLikelyMailItemNode(node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /发件人|验证码|verification|chatgpt|openai|code|log-?in/i.test(summaryText);
|
||||
return /发件人|验证码|verification|code|log-?in/i.test(summaryText);
|
||||
}
|
||||
|
||||
function findMailItems() {
|
||||
@@ -406,7 +439,7 @@ function selectOpenedMailTextCandidate(item, candidates = [], options = {}) {
|
||||
if (sender && lower.includes(sender)) {
|
||||
return true;
|
||||
}
|
||||
return Boolean(extractVerificationCode(candidate) && /chatgpt|openai|verification|验证码|log-?in\s+code/i.test(lower));
|
||||
return Boolean(extractVerificationCode(candidate, { codePatterns: options.codePatterns }) && /verification|验证码|log-?in\s+code|enter\s+this\s+code|code/i.test(lower));
|
||||
}) || source[0] || '';
|
||||
|
||||
const filteredCandidates = candidates.filter((candidate) => !excludedSet.has(normalizeText(candidate)));
|
||||
@@ -443,9 +476,11 @@ async function returnToInbox() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function openMailAndGetMessageText(item) {
|
||||
async function openMailAndGetMessageText(item, options = {}) {
|
||||
const beforeCandidates = collectOpenedMailTextCandidates();
|
||||
const beforeText = selectOpenedMailTextCandidate(item, beforeCandidates);
|
||||
const beforeText = selectOpenedMailTextCandidate(item, beforeCandidates, {
|
||||
codePatterns: options.codePatterns,
|
||||
});
|
||||
if (typeof simulateClick === 'function') {
|
||||
simulateClick(item);
|
||||
} else {
|
||||
@@ -456,6 +491,7 @@ async function openMailAndGetMessageText(item) {
|
||||
for (let i = 0; i < 24; i += 1) {
|
||||
await sleep(250);
|
||||
const candidate = readOpenedMailText(item, {
|
||||
codePatterns: options.codePatterns,
|
||||
excludedTexts: beforeCandidates,
|
||||
allowExcludedFallback: false,
|
||||
});
|
||||
@@ -463,7 +499,7 @@ async function openMailAndGetMessageText(item) {
|
||||
continue;
|
||||
}
|
||||
openedText = candidate;
|
||||
if (extractVerificationCode(candidate)) {
|
||||
if (extractVerificationCode(candidate, { codePatterns: options.codePatterns })) {
|
||||
break;
|
||||
}
|
||||
if (candidate !== beforeText && candidate.length > beforeText.length + 24) {
|
||||
@@ -488,7 +524,15 @@ function scheduleEmailCleanup(item, step) {
|
||||
// ============================================================
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [], filterAfterTimestamp = 0 } = payload;
|
||||
const {
|
||||
codePatterns = [],
|
||||
senderFilters,
|
||||
subjectFilters,
|
||||
maxAttempts,
|
||||
intervalMs,
|
||||
excludeCodes = [],
|
||||
filterAfterTimestamp = 0,
|
||||
} = payload;
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
|
||||
const mailLabel = getNetEaseMailLabel();
|
||||
@@ -581,12 +625,12 @@ async function handlePollEmail(step, payload) {
|
||||
});
|
||||
|
||||
if (senderMatch || subjectMatch) {
|
||||
let code = extractVerificationCode(combinedText);
|
||||
let code = extractVerificationCode(combinedText, { codePatterns });
|
||||
let codeSource = '邮件列表';
|
||||
|
||||
if (!code) {
|
||||
const openedText = await openMailAndGetMessageText(item);
|
||||
code = extractVerificationCode(openedText);
|
||||
const openedText = await openMailAndGetMessageText(item, { codePatterns });
|
||||
code = extractVerificationCode(openedText, { codePatterns });
|
||||
if (code) {
|
||||
codeSource = '邮件正文';
|
||||
}
|
||||
@@ -716,12 +760,16 @@ async function refreshInbox() {
|
||||
// Verification Code Extraction
|
||||
// ============================================================
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
function extractVerificationCode(text, options = {}) {
|
||||
const matchedByRule = extractCodeByRulePatterns(text, options?.codePatterns);
|
||||
if (matchedByRule) {
|
||||
return matchedByRule;
|
||||
}
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = text.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchLoginCode) return matchLoginCode[1];
|
||||
|
||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
|
||||
+90
-21
@@ -712,7 +712,56 @@ function matchesMailFilters(text, senderFilters, subjectFilters) {
|
||||
return senderMatch || subjectMatch;
|
||||
}
|
||||
|
||||
function extractStrictChatGPTVerificationCode(text) {
|
||||
function normalizeRulePatternList(patterns = []) {
|
||||
return Array.isArray(patterns) ? patterns : [];
|
||||
}
|
||||
|
||||
function extractCodeByRulePatterns(text, patterns = []) {
|
||||
const normalizedText = String(text || '');
|
||||
for (const pattern of normalizeRulePatternList(patterns)) {
|
||||
try {
|
||||
const source = String(pattern?.source || '').trim();
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
|
||||
const match = normalizedText.match(new RegExp(source, flags));
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
for (let index = 1; index < match.length; index += 1) {
|
||||
const candidate = String(match[index] || '').trim();
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
if (String(match[0] || '').trim()) {
|
||||
return String(match[0] || '').trim();
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore invalid runtime rule patterns and continue with other candidates.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeTargetEmailHints(hints = [], targetEmail = '') {
|
||||
const normalizedHints = Array.isArray(hints) ? hints : [];
|
||||
const collected = normalizedHints
|
||||
.map((item) => String(item || '').trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
|
||||
if (normalizedTarget) {
|
||||
collected.push(normalizedTarget);
|
||||
const atIndex = normalizedTarget.indexOf('@');
|
||||
if (atIndex > 0) {
|
||||
collected.push(`${normalizedTarget.slice(0, atIndex)}=${normalizedTarget.slice(atIndex + 1)}`);
|
||||
}
|
||||
}
|
||||
return [...new Set(collected)];
|
||||
}
|
||||
|
||||
function extractLegacyStrictVerificationCode(text) {
|
||||
const normalized = String(text || '');
|
||||
const patterns = [
|
||||
/your\s+(?:temporary\s+)?chatgpt\s+(?:(?:log-?in|login)\s+)?code\s+is[\s\S]{0,80}?(\d{6})/i,
|
||||
@@ -784,11 +833,16 @@ function findSafeStandaloneSixDigitCode(text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractVerificationCode(text, strictChatGPTCodeOnly = false) {
|
||||
const strictCode = extractStrictChatGPTVerificationCode(text);
|
||||
if (strictChatGPTCodeOnly) {
|
||||
return strictCode;
|
||||
function extractVerificationCode(text, options = {}) {
|
||||
const legacyStrictMode = typeof options === 'boolean' ? options : false;
|
||||
const strictMode = legacyStrictMode || Boolean(options?.strictMode);
|
||||
const codePatterns = legacyStrictMode ? [] : options?.codePatterns;
|
||||
const strictCode = extractLegacyStrictVerificationCode(text);
|
||||
const matchedByRule = extractCodeByRulePatterns(text, codePatterns);
|
||||
if (strictMode) {
|
||||
return matchedByRule || strictCode;
|
||||
}
|
||||
if (matchedByRule) return matchedByRule;
|
||||
if (strictCode) return strictCode;
|
||||
|
||||
const normalized = String(text || '');
|
||||
@@ -796,11 +850,8 @@ function extractVerificationCode(text, strictChatGPTCodeOnly = false) {
|
||||
const matchCn = normalized.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = normalized.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
|
||||
const matchChatGPT = normalized.match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i);
|
||||
if (matchChatGPT) return matchChatGPT[1];
|
||||
const matchLoginCode = normalized.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchLoginCode) return matchLoginCode[1];
|
||||
|
||||
const matchEn = normalized.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
@@ -813,9 +864,9 @@ function extractEmails(text = '') {
|
||||
return [...new Set(matches.map((item) => item.toLowerCase()))];
|
||||
}
|
||||
|
||||
function extractForwardedTargetEmails(text = '') {
|
||||
function extractForwardedTargetEmails(text = '', targetEmailHints = []) {
|
||||
const normalizedText = String(text || '').toLowerCase();
|
||||
const matches = normalizedText.match(/bounce\+[a-z0-9._%+-]*-([a-z0-9._%+-]+)=([a-z0-9.-]+\.[a-z]{2,})@(?:tm\d*\.openai\.com|em\d+\.tm\.openai\.com)/gi) || [];
|
||||
const matches = normalizedText.match(/bounce\+[a-z0-9._%+-]*-([a-z0-9._%+-]+)=([a-z0-9.-]+\.[a-z]{2,})@[a-z0-9.-]+\.[a-z]{2,}/gi) || [];
|
||||
const decoded = matches
|
||||
.map((candidate) => {
|
||||
const match = String(candidate || '').match(/bounce\+[a-z0-9._%+-]*-([a-z0-9._%+-]+)=([a-z0-9.-]+\.[a-z]{2,})@/i);
|
||||
@@ -825,7 +876,19 @@ function extractForwardedTargetEmails(text = '') {
|
||||
return `${match[1].toLowerCase()}@${match[2].toLowerCase()}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return [...new Set(decoded)];
|
||||
const hinted = normalizeTargetEmailHints(targetEmailHints)
|
||||
.filter((hint) => hint.includes('@') || hint.includes('='))
|
||||
.flatMap((hint) => {
|
||||
if (hint.includes('@')) {
|
||||
return normalizedText.includes(hint) ? [hint] : [];
|
||||
}
|
||||
const match = hint.match(/^([^=]+)=([^=]+)$/);
|
||||
if (!match || !normalizedText.includes(hint)) {
|
||||
return [];
|
||||
}
|
||||
return [`${match[1]}@${match[2]}`];
|
||||
});
|
||||
return [...new Set([...decoded, ...hinted])];
|
||||
}
|
||||
|
||||
function emailMatchesTarget(candidate, targetEmail) {
|
||||
@@ -838,19 +901,20 @@ function emailMatchesTarget(candidate, targetEmail) {
|
||||
return normalizedCandidate === normalizedTarget;
|
||||
}
|
||||
|
||||
function getTargetEmailMatchState(text, targetEmail) {
|
||||
function getTargetEmailMatchState(text, targetEmail, options = {}) {
|
||||
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
|
||||
if (!normalizedTarget) {
|
||||
return { matches: true, hasExplicitEmail: false };
|
||||
}
|
||||
|
||||
const normalizedText = String(text || '').toLowerCase();
|
||||
if (normalizedText.includes(normalizedTarget)) {
|
||||
const targetEmailHints = normalizeTargetEmailHints(options?.targetEmailHints, normalizedTarget);
|
||||
if (targetEmailHints.some((hint) => normalizedText.includes(hint))) {
|
||||
return { matches: true, hasExplicitEmail: true };
|
||||
}
|
||||
|
||||
const extractedEmails = extractEmails(normalizedText);
|
||||
const forwardedTargetEmails = extractForwardedTargetEmails(normalizedText);
|
||||
const forwardedTargetEmails = extractForwardedTargetEmails(normalizedText, targetEmailHints);
|
||||
if (!extractedEmails.length) {
|
||||
return forwardedTargetEmails.length
|
||||
? {
|
||||
@@ -1219,14 +1283,15 @@ async function ensureMail2925Session(payload = {}) {
|
||||
async function handlePollEmail(step, payload) {
|
||||
await ensureSeenCodesSession(step, payload);
|
||||
const {
|
||||
codePatterns = [],
|
||||
senderFilters,
|
||||
subjectFilters,
|
||||
maxAttempts,
|
||||
intervalMs,
|
||||
filterAfterTimestamp = 0,
|
||||
excludeCodes = [],
|
||||
strictChatGPTCodeOnly = false,
|
||||
targetEmail = '',
|
||||
targetEmailHints = [],
|
||||
mail2925MatchTargetEmail = false,
|
||||
} = payload || {};
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
@@ -1290,21 +1355,25 @@ async function handlePollEmail(step, payload) {
|
||||
continue;
|
||||
}
|
||||
const previewTargetState = mail2925MatchTargetEmail
|
||||
? getTargetEmailMatchState(previewText, targetEmail)
|
||||
? getTargetEmailMatchState(previewText, targetEmail, { targetEmailHints })
|
||||
: { matches: true, hasExplicitEmail: false };
|
||||
if (mail2925MatchTargetEmail && previewTargetState.hasExplicitEmail && !previewTargetState.matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previewCode = extractVerificationCode(previewText, strictChatGPTCodeOnly);
|
||||
const previewCode = extractVerificationCode(previewText, {
|
||||
codePatterns,
|
||||
});
|
||||
const openedText = await openMailAndDeleteAfterRead(item, step);
|
||||
const openedTargetState = mail2925MatchTargetEmail
|
||||
? getTargetEmailMatchState(openedText, targetEmail)
|
||||
? getTargetEmailMatchState(openedText, targetEmail, { targetEmailHints })
|
||||
: { matches: true, hasExplicitEmail: false };
|
||||
if (mail2925MatchTargetEmail && openedTargetState.hasExplicitEmail && !openedTargetState.matches) {
|
||||
continue;
|
||||
}
|
||||
const bodyCode = extractVerificationCode(openedText, strictChatGPTCodeOnly);
|
||||
const bodyCode = extractVerificationCode(openedText, {
|
||||
codePatterns,
|
||||
});
|
||||
const candidateCode = bodyCode || previewCode;
|
||||
|
||||
if (!candidateCode) {
|
||||
|
||||
+50
-5
@@ -50,12 +50,52 @@ function getCurrentMailIds() {
|
||||
return ids;
|
||||
}
|
||||
|
||||
function normalizeRulePatternList(patterns = []) {
|
||||
return Array.isArray(patterns) ? patterns : [];
|
||||
}
|
||||
|
||||
function extractCodeByRulePatterns(text, patterns = []) {
|
||||
const normalizedText = String(text || '');
|
||||
for (const pattern of normalizeRulePatternList(patterns)) {
|
||||
try {
|
||||
const source = String(pattern?.source || '').trim();
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
|
||||
const match = normalizedText.match(new RegExp(source, flags));
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
for (let index = 1; index < match.length; index += 1) {
|
||||
const candidate = String(match[index] || '').trim();
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
if (String(match[0] || '').trim()) {
|
||||
return String(match[0] || '').trim();
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore invalid runtime rule patterns and continue with other candidates.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Email Polling
|
||||
// ============================================================
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload;
|
||||
const {
|
||||
codePatterns = [],
|
||||
senderFilters,
|
||||
subjectFilters,
|
||||
maxAttempts,
|
||||
intervalMs,
|
||||
excludeCodes = [],
|
||||
} = payload;
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
|
||||
log(`步骤 ${step}:开始轮询邮箱(最多 ${maxAttempts} 次,每 ${intervalMs / 1000} 秒一次)`);
|
||||
@@ -103,7 +143,9 @@ async function handlePollEmail(step, payload) {
|
||||
const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()));
|
||||
|
||||
if (senderMatch || subjectMatch) {
|
||||
const code = extractVerificationCode(subject + ' ' + digest);
|
||||
const code = extractVerificationCode(subject + ' ' + digest, {
|
||||
codePatterns,
|
||||
});
|
||||
if (code) {
|
||||
if (excludedCodeSet.has(code)) {
|
||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
||||
@@ -169,13 +211,16 @@ async function refreshInbox() {
|
||||
// Verification Code Extraction
|
||||
// ============================================================
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
function extractVerificationCode(text, options = {}) {
|
||||
const matchedByRule = extractCodeByRulePatterns(text, options?.codePatterns);
|
||||
if (matchedByRule) return matchedByRule;
|
||||
|
||||
// Pattern 1: Chinese format "代码为 370794" or "验证码...370794"
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = text.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchLoginCode) return matchLoginCode[1];
|
||||
|
||||
// Pattern 2: English format "code is 370794" or "code: 370794"
|
||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
|
||||
+90
-26
@@ -1,6 +1,7 @@
|
||||
(function attachStepDefinitions(root, factory) {
|
||||
root.MultiPageStepDefinitions = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createStepDefinitionsModule() {
|
||||
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
@@ -88,11 +89,20 @@
|
||||
: SIGNUP_METHOD_EMAIL;
|
||||
}
|
||||
|
||||
function normalizeActiveFlowId(value = '', fallback = DEFAULT_ACTIVE_FLOW_ID) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
const fallbackValue = String(fallback || '').trim().toLowerCase();
|
||||
return fallbackValue || DEFAULT_ACTIVE_FLOW_ID;
|
||||
}
|
||||
|
||||
function getResolvedSignupMethod(options = {}) {
|
||||
return normalizeSignupMethod(options?.resolvedSignupMethod || options?.signupMethod);
|
||||
}
|
||||
|
||||
function getModeStepDefinitions(options = {}) {
|
||||
function getOpenAiModeStepDefinitions(options = {}) {
|
||||
if (!isPlusModeEnabled(options)) {
|
||||
return NORMAL_STEP_DEFINITIONS;
|
||||
}
|
||||
@@ -103,20 +113,20 @@
|
||||
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS;
|
||||
}
|
||||
|
||||
function getPlusPaymentStepTitle(options = {}) {
|
||||
function getOpenAiPlusPaymentStepTitle(options = {}) {
|
||||
if (!isPlusModeEnabled(options)) {
|
||||
return '';
|
||||
}
|
||||
const paymentStep = getModeStepDefinitions({
|
||||
const paymentStep = getOpenAiModeStepDefinitions({
|
||||
...options,
|
||||
plusModeEnabled: true,
|
||||
}).find((step) => step.key === PLUS_PAYMENT_STEP_KEY);
|
||||
return paymentStep?.title || '';
|
||||
}
|
||||
|
||||
function getResolvedStepTitle(step = {}, options = {}) {
|
||||
function getOpenAiResolvedStepTitle(step = {}, options = {}) {
|
||||
if (isPlusModeEnabled(options) && step.key === PLUS_PAYMENT_STEP_KEY) {
|
||||
return getPlusPaymentStepTitle(options) || step.title;
|
||||
return getOpenAiPlusPaymentStepTitle(options) || step.title;
|
||||
}
|
||||
const signupMethod = getResolvedSignupMethod(options);
|
||||
if (signupMethod === SIGNUP_METHOD_PHONE && PHONE_SIGNUP_TITLE_OVERRIDES[step.key]) {
|
||||
@@ -125,37 +135,83 @@
|
||||
return step.title;
|
||||
}
|
||||
|
||||
function cloneSteps(steps = [], options = {}) {
|
||||
const FLOW_DEFINITION_BUILDERS = Object.freeze({
|
||||
openai: {
|
||||
getAllSteps() {
|
||||
const keyed = new Map();
|
||||
for (const step of [
|
||||
...NORMAL_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_STEP_DEFINITIONS,
|
||||
...PLUS_GOPAY_STEP_DEFINITIONS,
|
||||
...PLUS_GPC_STEP_DEFINITIONS,
|
||||
]) {
|
||||
keyed.set(`${step.id}:${step.key}`, step);
|
||||
}
|
||||
return Array.from(keyed.values()).sort((left, right) => {
|
||||
const leftOrder = Number.isFinite(left.order) ? left.order : left.id;
|
||||
const rightOrder = Number.isFinite(right.order) ? right.order : right.id;
|
||||
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
||||
return left.id - right.id;
|
||||
});
|
||||
},
|
||||
getModeStepDefinitions: getOpenAiModeStepDefinitions,
|
||||
getPlusPaymentStepTitle: getOpenAiPlusPaymentStepTitle,
|
||||
resolveStepTitle: getOpenAiResolvedStepTitle,
|
||||
},
|
||||
});
|
||||
|
||||
function hasFlow(flowId) {
|
||||
const normalizedFlowId = normalizeActiveFlowId(flowId, '');
|
||||
return Boolean(normalizedFlowId && FLOW_DEFINITION_BUILDERS[normalizedFlowId]);
|
||||
}
|
||||
|
||||
function getRegisteredFlowIds() {
|
||||
return Object.keys(FLOW_DEFINITION_BUILDERS);
|
||||
}
|
||||
|
||||
function getFlowDefinitionBuilder(options = {}) {
|
||||
const flowId = normalizeActiveFlowId(options?.activeFlowId, DEFAULT_ACTIVE_FLOW_ID);
|
||||
return {
|
||||
flowId,
|
||||
builder: FLOW_DEFINITION_BUILDERS[flowId] || null,
|
||||
};
|
||||
}
|
||||
|
||||
function cloneSteps(steps = [], options = {}, flowId = DEFAULT_ACTIVE_FLOW_ID) {
|
||||
const { builder } = getFlowDefinitionBuilder({ activeFlowId: flowId });
|
||||
return steps.map((step) => ({
|
||||
...step,
|
||||
title: getResolvedStepTitle(step, options),
|
||||
flowId,
|
||||
title: builder?.resolveStepTitle ? builder.resolveStepTitle(step, options) : step.title,
|
||||
}));
|
||||
}
|
||||
|
||||
function getSteps(options = {}) {
|
||||
return cloneSteps(getModeStepDefinitions(options), options);
|
||||
const { flowId, builder } = getFlowDefinitionBuilder(options);
|
||||
if (!builder?.getModeStepDefinitions) {
|
||||
return [];
|
||||
}
|
||||
return cloneSteps(builder.getModeStepDefinitions(options), options, flowId);
|
||||
}
|
||||
|
||||
function getAllSteps() {
|
||||
const keyed = new Map();
|
||||
for (const step of [
|
||||
...NORMAL_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_STEP_DEFINITIONS,
|
||||
...PLUS_GOPAY_STEP_DEFINITIONS,
|
||||
...PLUS_GPC_STEP_DEFINITIONS,
|
||||
]) {
|
||||
keyed.set(`${step.id}:${step.key}`, step);
|
||||
function getAllSteps(options = {}) {
|
||||
const { flowId, builder } = getFlowDefinitionBuilder(options);
|
||||
if (!builder?.getAllSteps) {
|
||||
return [];
|
||||
}
|
||||
return cloneSteps(Array.from(keyed.values()).sort((left, right) => {
|
||||
const leftOrder = Number.isFinite(left.order) ? left.order : left.id;
|
||||
const rightOrder = Number.isFinite(right.order) ? right.order : right.id;
|
||||
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
||||
return left.id - right.id;
|
||||
}));
|
||||
return cloneSteps(builder.getAllSteps(options), options, flowId);
|
||||
}
|
||||
|
||||
function getPlusPaymentStepTitle(options = {}) {
|
||||
const { builder } = getFlowDefinitionBuilder(options);
|
||||
if (!builder?.getPlusPaymentStepTitle) {
|
||||
return '';
|
||||
}
|
||||
return builder.getPlusPaymentStepTitle(options);
|
||||
}
|
||||
|
||||
function getStepIds(options = {}) {
|
||||
return getModeStepDefinitions(options)
|
||||
return getSteps(options)
|
||||
.map((step) => Number(step.id))
|
||||
.filter(Number.isFinite)
|
||||
.sort((left, right) => left - right);
|
||||
@@ -168,11 +224,16 @@
|
||||
|
||||
function getStepById(id, options = {}) {
|
||||
const numericId = Number(id);
|
||||
const match = getModeStepDefinitions(options).find((step) => step.id === numericId);
|
||||
return match ? cloneSteps([match], options)[0] : null;
|
||||
const { flowId, builder } = getFlowDefinitionBuilder(options);
|
||||
if (!builder?.getModeStepDefinitions) {
|
||||
return null;
|
||||
}
|
||||
const match = builder.getModeStepDefinitions(options).find((step) => step.id === numericId);
|
||||
return match ? cloneSteps([match], options, flowId)[0] : null;
|
||||
}
|
||||
|
||||
return {
|
||||
DEFAULT_ACTIVE_FLOW_ID,
|
||||
STEP_DEFINITIONS: NORMAL_STEP_DEFINITIONS,
|
||||
NORMAL_STEP_DEFINITIONS,
|
||||
PLUS_STEP_DEFINITIONS: PLUS_PAYPAL_STEP_DEFINITIONS,
|
||||
@@ -184,10 +245,13 @@
|
||||
getAllSteps,
|
||||
getLastStepId,
|
||||
getPlusPaymentStepTitle,
|
||||
getRegisteredFlowIds,
|
||||
getStepById,
|
||||
getStepIds,
|
||||
getSteps,
|
||||
hasFlow,
|
||||
isPlusModeEnabled,
|
||||
normalizeActiveFlowId,
|
||||
normalizePlusPaymentMethod,
|
||||
normalizeSignupMethod,
|
||||
};
|
||||
|
||||
@@ -338,6 +338,8 @@ OAuth 登录流程打开新标签页,避免复用旧页面导致状态污染
|
||||
|
||||
## 8. 接码平台多平台适配:HeroSMS + 5sim
|
||||
|
||||
边界说明:接码平台多平台适配当前只属于 OpenAI 注册 / OAuth 链路,不作为多注册 flow 架构的 core 通用服务。后续新增的注册项目默认不接入接码;只有出现第二个真实项目也需要短信生命周期时,再按实际复用点抽象 provider 层。
|
||||
|
||||
### 目标
|
||||
|
||||
手机号验证 Step 9 支持按平台切换接码能力:
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
# 多注册流程侧边栏能力矩阵
|
||||
|
||||
本文解决的是一个很容易被低估的问题:sidepanel 现在不只是“渲染 UI”,它还承担了大量业务约束。如果未来要支持多个 flow,不能只做动态渲染,还必须把“哪些能力允许显示、允许切换、允许启动”做成统一能力矩阵。
|
||||
|
||||
## 1. 当前源码里的真实问题
|
||||
|
||||
当前手机号注册能力至少同时受这几组条件约束:
|
||||
|
||||
- `phoneVerificationEnabled`
|
||||
- `plusModeEnabled`
|
||||
- `contributionMode`
|
||||
- `panelMode === 'cpa'` 时还会触发额外提醒
|
||||
|
||||
而这些约束现在分散在多个地方:
|
||||
|
||||
- `background.js`:`canUsePhoneSignup`
|
||||
- `background/message-router.js`:同类判断
|
||||
- `sidepanel/sidepanel.js`:`canSelectPhoneSignupMethod`
|
||||
- `sidepanel/sidepanel.js`:`shouldWarnCpaPhoneSignup`
|
||||
|
||||
这说明 sidepanel 当前既在做展示,也在做策略判定,而且判定逻辑有重复。
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
后续 sidepanel 至少要拆成三层能力判断:
|
||||
|
||||
1. `flowCapabilities`
|
||||
2. `panelCapabilities`
|
||||
3. `runtimeLocks`
|
||||
|
||||
最终所有显示 / 禁用 / 启动校验都只读这三层组合结果。
|
||||
|
||||
## 3. 三层能力定义
|
||||
|
||||
### 3.1 `flowCapabilities`
|
||||
|
||||
表示“当前 flow 业务上支持什么”。
|
||||
|
||||
示例:
|
||||
|
||||
```js
|
||||
flowCapabilities.openai = {
|
||||
supportsEmailSignup: true,
|
||||
supportsPhoneSignup: true,
|
||||
supportsPhoneVerificationSettings: true,
|
||||
supportsPlusMode: true,
|
||||
supportsContributionMode: true,
|
||||
supportsPlatformBinding: ['cpa', 'sub2api', 'codex2api'],
|
||||
supportsLuckmail: true,
|
||||
supportsOauthTimeoutBudget: true,
|
||||
stepDefinitionMode: 'openai-dynamic',
|
||||
};
|
||||
|
||||
flowCapabilities.siteA = {
|
||||
supportsEmailSignup: true,
|
||||
supportsPhoneSignup: false,
|
||||
supportsPhoneVerificationSettings: false,
|
||||
supportsPlusMode: false,
|
||||
supportsContributionMode: false,
|
||||
supportsPlatformBinding: ['siteA-panel'],
|
||||
supportsLuckmail: false,
|
||||
supportsOauthTimeoutBudget: false,
|
||||
stepDefinitionMode: 'siteA',
|
||||
};
|
||||
```
|
||||
|
||||
原则:
|
||||
|
||||
- 没有真实需求,不要默认给新 flow 打开手机号、Plus、LuckMail
|
||||
- 新 flow 默认从最小能力集合开始
|
||||
|
||||
### 3.2 `panelCapabilities`
|
||||
|
||||
表示“当前面板来源允许什么交互”。
|
||||
|
||||
示例:
|
||||
|
||||
```js
|
||||
panelCapabilities = {
|
||||
cpa: {
|
||||
supportsPhoneSignup: true,
|
||||
requiresPhoneSignupWarning: true,
|
||||
},
|
||||
sub2api: {
|
||||
supportsPhoneSignup: true,
|
||||
requiresPhoneSignupWarning: false,
|
||||
},
|
||||
codex2api: {
|
||||
supportsPhoneSignup: true,
|
||||
requiresPhoneSignupWarning: false,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
注意:`contributionMode` 不是新的 `panelMode`,它是独立运行态锁。
|
||||
|
||||
### 3.3 `runtimeLocks`
|
||||
|
||||
表示“当前这一次运行或当前 UI 状态是否允许操作”。
|
||||
|
||||
示例:
|
||||
|
||||
```js
|
||||
runtimeLocks = {
|
||||
autoRunLocked: false,
|
||||
contributionMode: false,
|
||||
plusModeEnabled: false,
|
||||
phoneVerificationEnabled: true,
|
||||
settingsMenuLocked: false,
|
||||
};
|
||||
```
|
||||
|
||||
它决定的是:
|
||||
|
||||
- 现在能不能切换注册方式
|
||||
- 能不能改 flow
|
||||
- 能不能导入导出配置
|
||||
- 能不能打开某些配置区
|
||||
|
||||
## 4. 统一选择器
|
||||
|
||||
建议提供一个中心 selector:
|
||||
|
||||
```js
|
||||
resolveSidepanelCapabilities({
|
||||
activeFlowId,
|
||||
panelMode,
|
||||
state,
|
||||
});
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```js
|
||||
{
|
||||
effectiveSignupMethods: ['email', 'phone'],
|
||||
canShowPhoneSettings: true,
|
||||
canSelectPhoneSignup: true,
|
||||
shouldWarnCpaPhoneSignup: true,
|
||||
canShowPlusSettings: true,
|
||||
canShowLuckmail: true,
|
||||
canExportSettings: true,
|
||||
canSwitchFlow: false,
|
||||
stepDefinitionOptions: {},
|
||||
}
|
||||
```
|
||||
|
||||
以后 sidepanel 与 background 都只消费这份结果,不再各写一套判断。
|
||||
|
||||
## 5. OpenAI 当前矩阵
|
||||
|
||||
### 5.1 仅看 flow 能力
|
||||
|
||||
OpenAI 当前业务上支持:
|
||||
|
||||
- 邮箱注册
|
||||
- 手机号注册
|
||||
- OAuth 登录
|
||||
- 平台回调绑定
|
||||
- Plus
|
||||
- LuckMail
|
||||
- 接码配置
|
||||
|
||||
### 5.2 加上 runtime lock 后的真实结果
|
||||
|
||||
OpenAI 的手机号注册只有在以下条件同时成立时才可选:
|
||||
|
||||
- `flowCapabilities.openai.supportsPhoneSignup === true`
|
||||
- `phoneVerificationEnabled === true`
|
||||
- `plusModeEnabled === false`
|
||||
- `contributionMode === false`
|
||||
|
||||
也就是说,手机号注册不是一个“单纯 UI 开关”,而是能力矩阵结果。
|
||||
|
||||
### 5.3 加上 panel 约束后的额外行为
|
||||
|
||||
当满足手机号注册可用,且当前 `panelMode === 'cpa'` 时:
|
||||
|
||||
- 不阻止
|
||||
- 但要提示风险
|
||||
|
||||
这类“允许但要提醒”的逻辑也必须归到能力矩阵里,而不是散落在点击事件中。
|
||||
|
||||
## 6. 步骤列表也属于能力矩阵的一部分
|
||||
|
||||
当前步骤定义主要受:
|
||||
|
||||
- `plusModeEnabled`
|
||||
- `signupMethod`
|
||||
|
||||
影响,并通过 `data/step-definitions.js` 动态切换。
|
||||
|
||||
多 flow 后必须升级成:
|
||||
|
||||
```js
|
||||
getStepDefinitions({
|
||||
activeFlowId,
|
||||
signupVariant,
|
||||
panelMode,
|
||||
plusModeEnabled,
|
||||
contributionMode,
|
||||
});
|
||||
```
|
||||
|
||||
也就是说,步骤列表不再是全局 `10 步 / 13 步` 二选一,而是 flow-aware 的定义。
|
||||
|
||||
## 7. Sidepanel 分层职责
|
||||
|
||||
### 7.1 展示层
|
||||
|
||||
只负责:
|
||||
|
||||
- show / hide
|
||||
- enabled / disabled
|
||||
- 文案与提示
|
||||
|
||||
### 7.2 能力判定层
|
||||
|
||||
只负责:
|
||||
|
||||
- `resolveSidepanelCapabilities()`
|
||||
- `validateAutoRunStart()`
|
||||
- `validateModeSwitch()`
|
||||
|
||||
### 7.3 后台复核层
|
||||
|
||||
后台在收到启动、切换注册方式、切换 flow 等消息时,仍要复核一遍同一套能力判断,避免 sidepanel 以外的入口绕过约束。
|
||||
|
||||
## 8. 新 flow 的默认能力策略
|
||||
|
||||
后续新增 flow 时,默认按以下最小集合开始:
|
||||
|
||||
- 仅邮箱注册
|
||||
- 无手机号注册
|
||||
- 无 Plus
|
||||
- 无 LuckMail
|
||||
- 无贡献模式
|
||||
- 无 OpenAI 平台回调面板
|
||||
|
||||
只有真实需求出现时,再逐项打开 capability。
|
||||
|
||||
## 9. 迁移顺序
|
||||
|
||||
1. 提炼 `flowCapabilitiesRegistry`
|
||||
2. 提炼 `panelCapabilitiesRegistry`
|
||||
3. 新增 `resolveSidepanelCapabilities()` 与 `validateAutoRunStart()`
|
||||
4. 让 `sidepanel.js`、`background.js`、`message-router.js` 共用同一套 selector
|
||||
5. 再把步骤定义切换也改成 flow-aware
|
||||
|
||||
## 10. 本文对应解决的缺口
|
||||
|
||||
本文主要补齐以下缺口:
|
||||
|
||||
- sidepanel 不只是渲染层,还承接业务约束
|
||||
- 手机号注册能力由多个开关共同决定,当前逻辑分散且重复
|
||||
- `contributionMode` 与 `panelMode` 容易混淆
|
||||
- 多 flow 之后,步骤列表也必须纳入能力矩阵,而不是继续全局写死
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
# 多注册流程来源与驱动注册设计
|
||||
|
||||
本文解决的是另一个会在第二个 flow 接入时立刻爆炸的问题:当前项目虽然有 `tab-runtime`,但“来源 source 是谁、该注入什么脚本、URL family 怎么判定、localhost callback 应该清谁”仍然带着明显的 OpenAI 单流程假设。
|
||||
|
||||
## 1. 当前源码里的真实耦合
|
||||
|
||||
### 1.1 `signup-page` 其实不是抽象 source,而是 OpenAI Auth 标签页别名
|
||||
|
||||
目前这些地方都把 OpenAI Auth / Consent / Add-phone 页面统称为 `signup-page`:
|
||||
|
||||
- `content/utils.js` 的 `detectScriptSource()`
|
||||
- `background/navigation-utils.js` 的 `matchesSourceUrlFamily()`
|
||||
- `background/tab-runtime.js` 的注册表、冲突清理、callback 清理
|
||||
|
||||
这说明当前的 source 体系不是“可扩展的来源注册表”,而是“OpenAI 时代遗留的 source 命名”。
|
||||
|
||||
### 1.2 `content/utils.js` 的默认回退不安全
|
||||
|
||||
当前 `detectScriptSource()` 在无法识别页面时会回退成 `vps-panel`。这对单流程时期问题不大,但多 flow 下会出现两个风险:
|
||||
|
||||
1. 新 flow 页面被误识别成 `vps-panel`
|
||||
2. `PING / READY / COMPLETE` 日志和标签注册打到错误 source
|
||||
|
||||
### 1.3 `tab-runtime` 的 localhost cleanup 直接写死 `registry['signup-page']`
|
||||
|
||||
`closeLocalhostCallbackTabs()` 现在默认把匹配 callback 的残留 tab 从 `signup-page` 名下清掉。将来只要第二个 flow 也有 callback,或者 callback 不是来自 OpenAI Auth 页,这里就会清错或漏清。
|
||||
|
||||
### 1.4 manifest 的静态注入列表目前基本围绕 OpenAI
|
||||
|
||||
`manifest.json` 里的静态内容脚本匹配域名,目前只有 OpenAI Auth 页是明确的 flow 页面入口。第二个 flow 如果沿用这套做法,后续只会不断在 manifest 里继续加分散规则,缺少统一 source 定义中心。
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
后续必须拆成两层注册表:
|
||||
|
||||
1. `sourceRegistry`:管理“页面来源与标签页生命周期”
|
||||
2. `driverRegistry`:管理“这个来源页面由哪个 content driver 负责,支持哪些命令”
|
||||
|
||||
这两层不要再混成一个 `signup-page` 字符串。
|
||||
|
||||
## 3. `sourceRegistry` 设计
|
||||
|
||||
建议 source 用稳定 ID,而不是模糊业务词。
|
||||
|
||||
```js
|
||||
sourceRegistry = {
|
||||
'openai-auth': {
|
||||
flowId: 'openai',
|
||||
kind: 'flow-page',
|
||||
label: 'OpenAI 认证页',
|
||||
hostPatterns: [
|
||||
'https://auth.openai.com/*',
|
||||
'https://auth0.openai.com/*',
|
||||
'https://accounts.openai.com/*',
|
||||
],
|
||||
familyMatcher: 'openai-auth-family',
|
||||
injectFiles: [
|
||||
'content/activation-utils.js',
|
||||
'content/utils.js',
|
||||
'content/operation-delay.js',
|
||||
'content/auth-page-recovery.js',
|
||||
'content/phone-country-utils.js',
|
||||
'content/phone-auth.js',
|
||||
'content/signup-page.js',
|
||||
],
|
||||
cleanupScopes: ['oauth-localhost-callback'],
|
||||
},
|
||||
|
||||
'mail-gmail': {
|
||||
flowId: null,
|
||||
kind: 'mail-provider',
|
||||
label: 'Gmail',
|
||||
hostPatterns: ['https://mail.google.com/*'],
|
||||
familyMatcher: 'gmail-family',
|
||||
injectFiles: ['content/activation-utils.js', 'content/utils.js', 'content/gmail-mail.js'],
|
||||
},
|
||||
|
||||
'panel-sub2api': {
|
||||
flowId: 'openai',
|
||||
kind: 'panel-page',
|
||||
label: 'SUB2API 后台',
|
||||
dynamicOnly: true,
|
||||
familyMatcher: 'sub2api-panel-family',
|
||||
injectFiles: ['content/utils.js', 'content/sub2api-panel.js'],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 3.1 source 里至少要有这些字段
|
||||
|
||||
- `id`
|
||||
- `flowId`
|
||||
- `kind`
|
||||
- `label`
|
||||
- `hostPatterns`
|
||||
- `familyMatcher`
|
||||
- `injectFiles`
|
||||
- `readyPolicy`
|
||||
- `cleanupScopes`
|
||||
- `tabReusePolicy`
|
||||
|
||||
### 3.2 source 不等于 flow
|
||||
|
||||
一个 flow 会有多个 source:
|
||||
|
||||
- `openai-entry`
|
||||
- `openai-auth`
|
||||
- `openai-plus-checkout`
|
||||
- `openai-paypal`
|
||||
- `openai-gopay`
|
||||
- `panel-sub2api`
|
||||
|
||||
同样,一个 source 也可能不属于具体 flow,例如:
|
||||
|
||||
- `mail-gmail`
|
||||
- `mail-163`
|
||||
- `mail-2925`
|
||||
- `mail-inbucket`
|
||||
|
||||
## 4. `driverRegistry` 设计
|
||||
|
||||
`driverRegistry` 解决的是“页面上能做什么”,不是“标签页如何复用”。
|
||||
|
||||
```js
|
||||
driverRegistry = {
|
||||
'openai-auth': {
|
||||
sourceId: 'openai-auth',
|
||||
driverId: 'content/signup-page',
|
||||
commands: [
|
||||
'OPEN_SIGNUP',
|
||||
'SUBMIT_SIGNUP_IDENTIFIER',
|
||||
'SUBMIT_PASSWORD',
|
||||
'SUBMIT_PROFILE',
|
||||
'SUBMIT_LOGIN_CODE',
|
||||
'SUBMIT_PHONE_CODE',
|
||||
'DETECT_AUTH_STATE',
|
||||
],
|
||||
},
|
||||
|
||||
'mail-gmail': {
|
||||
sourceId: 'mail-gmail',
|
||||
driverId: 'content/gmail-mail',
|
||||
commands: ['POLL_MAILBOX', 'OPEN_MESSAGE', 'DELETE_MESSAGE'],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
这样做的好处是:
|
||||
|
||||
- `tab-runtime` 只关心 source
|
||||
- workflow / mail service 只关心 driver capability
|
||||
- 后续换内容脚本实现时,不需要改 tab 生命周期代码
|
||||
|
||||
## 5. `signup-page` 的迁移策略
|
||||
|
||||
`signup-page` 不能直接继续作为未来通用 source 名字。
|
||||
|
||||
建议迁移方式:
|
||||
|
||||
1. 新增正式 source:`openai-auth`
|
||||
2. 在 `sourceRegistry.aliases` 里临时保留:
|
||||
- `signup-page -> openai-auth`
|
||||
3. `getSourceLabel()`、`matchesSourceUrlFamily()`、`ensureContentScriptReadyOnTab()` 全部优先读注册表
|
||||
4. 等 OpenAI 全链路迁完后,再删除 `signup-page` 别名
|
||||
|
||||
## 6. localhost callback cleanup 怎么改
|
||||
|
||||
当前 callback 清理逻辑最大的问题是“只知道 callback URL,不知道 callback 属于哪个 flow source”。
|
||||
|
||||
建议改成:
|
||||
|
||||
```js
|
||||
callbackRegistry = {
|
||||
'oauth-localhost-callback': {
|
||||
ownerSourceId: 'openai-auth',
|
||||
matcher: isLocalhostOAuthCallbackUrl,
|
||||
clearOwnerTabOnClose: true,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
`closeLocalhostCallbackTabs()` 的输入要升级为:
|
||||
|
||||
- `cleanupScope`
|
||||
- `callbackUrl`
|
||||
- `ownerSourceId`
|
||||
|
||||
而不是默认拿 `signup-page`。
|
||||
|
||||
## 7. `content/utils.js` 的改法
|
||||
|
||||
### 7.1 禁止默认回退成 `vps-panel`
|
||||
|
||||
新的规则应该是:
|
||||
|
||||
- 有 `window.__MULTIPAGE_SOURCE` 时,以注入 source 为准
|
||||
- 没有显式 source 时,只在静态 host map 里做白名单匹配
|
||||
- 仍无法识别时返回 `unknown-source`
|
||||
|
||||
`unknown-source` 不参与 tab 注册,不自动 `reportReady()`,只记录诊断日志。
|
||||
|
||||
### 7.2 child frame ready 规则也要转到注册表
|
||||
|
||||
当前 `shouldReportReadyForFrame()` 里写死了多个 source 名称。后续应改成 source metadata:
|
||||
|
||||
- `readyPolicy: 'top-frame-only'`
|
||||
- `readyPolicy: 'allow-child-frame'`
|
||||
|
||||
## 8. manifest / 动态注入策略
|
||||
|
||||
原则上以后由 `sourceRegistry` 作为单一事实来源,manifest 只是实现载体。
|
||||
|
||||
建议约定:
|
||||
|
||||
- 稳定公共页面可继续走静态 `content_scripts`
|
||||
- 变动大、域名多、只在运行期打开的页面改走 `chrome.scripting.executeScript`
|
||||
- 每个 source 明确自己的 `hostPatterns` 和 `injectFiles`
|
||||
- 新增 flow 时,不允许直接在业务代码里散写 `injectFiles = [...]`
|
||||
|
||||
## 9. 与网络策略的关系
|
||||
|
||||
`sourceRegistry` 只负责页面来源,不负责代理策略;但每个 flow 应同时提供自己的 `networkProfile`,至少包含:
|
||||
|
||||
- `guardDomains`
|
||||
- `probeEndpoints`
|
||||
- `failCloseDomains`
|
||||
|
||||
否则 `ip-proxy-core` 仍会继续写死 `chatgpt.com / openai.com`。
|
||||
|
||||
## 10. 第一阶段迁移顺序
|
||||
|
||||
1. 引入 `sourceRegistry / driverRegistry` 与旧 source alias。
|
||||
2. 把 `getSourceLabel()`、`matchesSourceUrlFamily()`、`ensureContentScriptReadyOnTab()` 改成读注册表。
|
||||
3. 去掉 `detectScriptSource()` 的 `vps-panel` 默认回退。
|
||||
4. 改 `closeLocalhostCallbackTabs()`,不再写死 `signup-page`。
|
||||
5. 第二个 flow 接入时,严格要求先注册 source 和 driver,再写步骤逻辑。
|
||||
|
||||
## 11. 本文对应解决的缺口
|
||||
|
||||
本文主要补齐以下缺口:
|
||||
|
||||
- `signup-page` 实际是 OpenAI source,不是通用注册页
|
||||
- source URL family / content ready / localhost callback cleanup 没有统一注册表
|
||||
- manifest 与动态注入没有统一入口
|
||||
- `content/utils.js` 的默认 source 回退会污染未来 flow
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# 多注册流程架构边界
|
||||
|
||||
本文记录后续从“OpenAI 注册扩展”升级到“多注册流程扩展”时的模块边界。当前结论是:需要引入多 flow 架构,但不要把所有能力都提前抽成通用服务。
|
||||
|
||||
## 1. 总体判断
|
||||
|
||||
后续新增注册项目时,不应继续在现有 OpenAI 步骤里堆 `if site === ...`。不同网站的注册入口、验证码、资料页、风控页、回调页和成功判定都可能完全不同,应按独立 flow 处理。
|
||||
|
||||
目标形态:
|
||||
|
||||
```txt
|
||||
core/
|
||||
flow-registry
|
||||
workflow-engine
|
||||
runtime-state
|
||||
tab-runtime
|
||||
logging
|
||||
account-artifacts
|
||||
email
|
||||
mail-code-polling
|
||||
network-proxy
|
||||
|
||||
flows/
|
||||
openai/
|
||||
workflow.js
|
||||
content-driver.js
|
||||
mail-rules.js
|
||||
network-profile.js
|
||||
phone-verification-flow.js
|
||||
phone-sms-providers/
|
||||
|
||||
site-a/
|
||||
workflow.js
|
||||
content-driver.js
|
||||
mail-rules.js
|
||||
network-profile.js
|
||||
```
|
||||
|
||||
第一阶段不需要立刻调整成上述目录,只需要按这个边界重构,避免新增项目继续污染 OpenAI 专用逻辑。
|
||||
|
||||
## 2. 必须通用化的部分
|
||||
|
||||
- `activeFlowId / runId / nodeId` 状态模型:替代只适合单流程的 `currentStep / stepStatuses`,同时避免与现有外部接口里的远端 `flow_id` 语义冲突。
|
||||
- Workflow engine:负责节点执行、跳转、停止、恢复、重试和超时,不理解具体网站页面。
|
||||
- Flow registry:负责注册 OpenAI、后续网站 A、网站 B 等独立流程。
|
||||
- Tab runtime:标签页注册、自动化窗口锁定、脚本注入、消息超时、页面 ready 检查。
|
||||
- Logging / status:结构化日志、节点状态、运行进度、停止和失败展示。
|
||||
- Account artifact store:记录账号产物、身份、凭据、回调结果和失败信息。
|
||||
- Email identity:邮箱生成、邮箱池、别名、转发收件目标等身份能力。
|
||||
- Mail polling:邮件验证码、magic link、激活链接的轮询框架;具体过滤和提取规则由 flow 提供。
|
||||
- IP proxy / network policy:代理应用、切换、出口检测、防泄漏;目标域名和探测 URL 由 flow 提供。
|
||||
- Sidepanel dynamic renderer:按 flow capability 显示配置区、步骤列表和运行状态。
|
||||
- Settings schema:通用配置与 flow 私有配置分层导入导出。
|
||||
- Error taxonomy / recovery policy:通用层只处理执行框架错误,业务错误由 flow 自己分类。
|
||||
|
||||
## 3. 暂不通用化的部分
|
||||
|
||||
以下能力当前只属于 OpenAI flow,不进入 core:
|
||||
|
||||
- 手机接码:HeroSMS / 5sim / NexSMS、取号、复用、轮询、换号、释放、手机号注册和后置 add-phone。
|
||||
- LuckMail:当前 `project_code = openai` 的购邮、复用、已用/保留/禁用管理。
|
||||
- OpenAI / ChatGPT 页面 DOM 操作。
|
||||
- OpenAI OAuth 授权、localhost callback、CPA / SUB2API / Codex2API 绑定。
|
||||
- ChatGPT Plus、PayPal、GoPay、GPC 相关支付流程。
|
||||
- OpenAI 的验证码邮件过滤规则、登录状态判断、add-phone fatal 判断。
|
||||
- OpenAI 专属账号产物字段,如 Plus checkout、OAuth state、平台验证字段。
|
||||
|
||||
原因:当前除了 OpenAI,新项目暂不需要手机接码。过早把接码抽成通用服务会让新 flow 被迫理解手机号订单、短信平台、号码复用和页面 resend 细节,增加维护成本。
|
||||
|
||||
## 4. 手机接码边界
|
||||
|
||||
手机接码先保持为 OpenAI 专属能力。
|
||||
|
||||
当前这些模块在逻辑上归属 `flows/openai`,即使物理路径暂时还在旧目录:
|
||||
|
||||
- `background/phone-verification-flow.js`
|
||||
- `phone-sms/providers/hero-sms.js`
|
||||
- `phone-sms/providers/five-sim.js`
|
||||
- `phone-sms/providers/registry.js`
|
||||
- `content/phone-auth.js`
|
||||
- `content/phone-country-utils.js`
|
||||
- sidepanel 中的接码配置区
|
||||
- 相关测试:`tests/phone-verification-flow.test.js`、`tests/five-sim-provider.test.js`、`tests/sidepanel-phone-verification-settings.test.js`
|
||||
|
||||
后续新增 flow 默认 `sms: false`,不显示接码配置,不加载接码步骤,也不要求 workflow engine 理解短信订单。
|
||||
|
||||
如果未来第二个真实 flow 也需要接码,再按事实抽象,优先抽 provider API,而不是直接抽完整手机号验证编排:
|
||||
|
||||
```js
|
||||
flow.phoneVerification = {
|
||||
acquirePolicy,
|
||||
submitPhoneNumber,
|
||||
submitCode,
|
||||
classifyPhoneError,
|
||||
recoverAfterTimeout,
|
||||
}
|
||||
```
|
||||
|
||||
只有两个以上 flow 共享相同短信供应商生命周期时,才考虑把 `phone-sms/providers/*` 下沉到 `core/services/sms`。
|
||||
|
||||
## 5. 迁移顺序
|
||||
|
||||
1. 引入 `activeFlowId / runId / currentNodeId / nodeStatuses / flowContext`,先兼容 OpenAI 旧步骤。
|
||||
2. 把邮箱、邮件轮询、代理、tab runtime、日志和账号记录逐步参数化,去掉 OpenAI 常量。
|
||||
3. 建立 `flows/openai` 边界,把 OpenAI 步骤、content driver、OAuth、Plus 和接码逻辑收拢进去。
|
||||
4. 用第二个真实注册项目验证 flow registry 和 workflow engine,而不是靠假想接口设计过度抽象。
|
||||
5. 稳定后清理旧 `step` 数字兼容层。
|
||||
|
||||
## 6. 新 flow 接入原则
|
||||
|
||||
新增注册项目时,只允许新增该 flow 的 workflow、content driver、mail rules、network profile 和必要的私有配置。不要直接修改 OpenAI 步骤,也不要把 OpenAI 的手机号验证、OAuth、Plus 支付逻辑提升为通用能力。
|
||||
|
||||
通用层只负责“怎么运行一个 flow”,具体 flow 负责“这个网站怎么注册”。
|
||||
|
||||
## 7. 配套设计文档
|
||||
|
||||
本边界文档只回答“什么该进 core、什么暂时不要抽”。要真正开工,还需要同时遵守下面四份配套设计:
|
||||
|
||||
- `docs/多注册流程状态迁移设计.md`:解决 `DEFAULT_STATE` 扁平模型、旧 step 兼容、自动运行/日志/账号记录如何迁移。
|
||||
- `docs/多注册流程来源与驱动注册设计.md`:解决 `signup-page`、source family、内容脚本注入和 localhost callback cleanup 的注册表设计。
|
||||
- `docs/多注册流程邮件分层设计.md`:解决 provider driver 与 flow mail rules 的边界,以及 LuckMail 暂不通用化的问题。
|
||||
- `docs/多注册流程侧边栏能力矩阵.md`:解决 sidepanel 展示层与业务约束层混在一起的问题。
|
||||
|
||||
后续新 flow 设计如果与这四份文档冲突,以“先修正文档边界,再动代码”为准,不允许直接在现有 OpenAI 逻辑上叠条件分支。
|
||||
@@ -0,0 +1,248 @@
|
||||
# 多注册流程状态迁移设计
|
||||
|
||||
本文是 `docs/多注册流程架构边界.md` 的落地补充,专门解决“现有扁平状态怎么迁到多 flow 架构”这个最容易把旧逻辑拖垮的问题。
|
||||
|
||||
## 1. 先说结论
|
||||
|
||||
当前项目不能直接在 `DEFAULT_STATE` 上继续堆字段。要支持多个完全不同的注册流程,必须先把“运行态”“持久配置”“共享服务状态”“flow 私有状态”分层,再保留一层 OpenAI 旧步骤兼容视图。
|
||||
|
||||
这次迁移的目标不是一步把全部字段搬完,而是先建立新的状态主模型,让旧代码通过 adapter 继续运行。
|
||||
|
||||
## 2. 当前源码里的真实问题
|
||||
|
||||
### 2.1 `DEFAULT_STATE` 仍然是单流程扁平模型
|
||||
|
||||
`background.js` 里的 `DEFAULT_STATE` 目前把这些不同层次的状态混在一起:
|
||||
|
||||
- 流程进度:`currentStep`、`stepStatuses`
|
||||
- 通用运行态:`automationWindowId`、`tabRegistry`、`logs`
|
||||
- OpenAI OAuth / 平台回调:`oauthUrl`、`localhostUrl`、`sub2apiSessionId`、`codex2apiSessionId`
|
||||
- OpenAI Plus:`plusCheckoutTabId`、`plusCheckoutUrl`、`plusBillingAddress`
|
||||
- OpenAI 接码:`currentPhoneActivation`、`signupPhoneActivation`、`reusablePhoneActivation`
|
||||
- OpenAI LuckMail:`currentLuckmailPurchase`
|
||||
- 共享邮箱运行态:`registrationEmailState`
|
||||
|
||||
这会带来两个直接后果:
|
||||
|
||||
1. 新 flow 一接进来,就只能继续往全局 state 加字段。
|
||||
2. 任何 reset / retry / stop 都容易误清理别的业务域状态。
|
||||
|
||||
### 2.2 旧步骤数字模型已经渗透到多个模块
|
||||
|
||||
当前不只是 `background.js` 依赖 `currentStep / stepStatuses`,下列模块也在按“Step 1~13”理解流程:
|
||||
|
||||
- `background/auto-run-controller.js`
|
||||
- `background/logging-status.js`
|
||||
- `background/account-run-history.js`
|
||||
|
||||
所以新状态模型不能只改 `background.js`,必须给自动运行、日志、账号记录保留兼容出口。
|
||||
|
||||
### 2.3 `registrationEmailState` 名字看起来通用,内部却仍带 OpenAI 身份假设
|
||||
|
||||
`background/registration-email-state.js` 已经把邮箱运行态单独抽出来了,这是好事;但它的 `preserveAccountIdentity` 现在默认保留的是“手机号身份”,也就是:
|
||||
|
||||
- `accountIdentifierType = phone`
|
||||
- `signupPhoneNumber`
|
||||
- `signupPhoneActivation`
|
||||
- `signupPhoneCompletedActivation`
|
||||
|
||||
这对 OpenAI 的 `add-email` 分支是对的,但对未来可能出现的“用户名主身份”“邀请码主身份”“钱包地址主身份”并不通用。
|
||||
|
||||
## 3. 目标状态分层
|
||||
|
||||
建议先建立“概念上的标准状态模型”,物理存储可以分阶段迁移。
|
||||
|
||||
```js
|
||||
chrome.storage.session.runtimeState = {
|
||||
activeFlowId: 'openai',
|
||||
activeRunId: 'run_20260512_xxx',
|
||||
currentNodeId: 'submit-signup-email',
|
||||
nodeStatuses: {
|
||||
'open-chatgpt': 'completed',
|
||||
'submit-signup-email': 'running',
|
||||
},
|
||||
|
||||
shared: {
|
||||
automationWindowId: 123,
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
logs: [],
|
||||
autoRun: {},
|
||||
accountArtifacts: {},
|
||||
},
|
||||
|
||||
services: {
|
||||
mail: {},
|
||||
proxy: {},
|
||||
accountPools: {},
|
||||
},
|
||||
|
||||
flows: {
|
||||
openai: {
|
||||
auth: {},
|
||||
platformBinding: {},
|
||||
plus: {},
|
||||
phoneVerification: {},
|
||||
luckmail: {},
|
||||
identity: {},
|
||||
},
|
||||
},
|
||||
|
||||
legacyStepCompat: {
|
||||
currentStep: 2,
|
||||
stepStatuses: { 1: 'completed', 2: 'running' },
|
||||
},
|
||||
};
|
||||
|
||||
chrome.storage.local.settings = {
|
||||
schemaVersion: 2,
|
||||
|
||||
shared: {
|
||||
autoRunDefaults: {},
|
||||
logging: {},
|
||||
},
|
||||
|
||||
services: {
|
||||
mailProviders: {},
|
||||
ipProxy: {},
|
||||
hotmailPool: {},
|
||||
mail2925Pool: {},
|
||||
icloud: {},
|
||||
customEmailPool: {},
|
||||
},
|
||||
|
||||
flows: {
|
||||
openai: {
|
||||
signupMethod: 'email',
|
||||
phoneVerificationEnabled: false,
|
||||
plusModeEnabled: false,
|
||||
plusPaymentMethod: 'paypal',
|
||||
sub2api: {},
|
||||
codex2api: {},
|
||||
luckmail: {},
|
||||
smsProviders: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 4. 四层状态边界
|
||||
|
||||
### 4.1 共享运行态 `runtimeState.shared`
|
||||
|
||||
只放“运行任何 flow 都需要”的内容:
|
||||
|
||||
- `activeFlowId / activeRunId / currentNodeId / nodeStatuses`
|
||||
- `automationWindowId`
|
||||
- `tabRegistry`
|
||||
- `sourceLastUrls`
|
||||
- `logs`
|
||||
- 自动运行轮次、暂停、计时器计划
|
||||
- 账号产物总览,如最终身份、最终邮箱、最终手机号、完成时间
|
||||
|
||||
这里不要再放 OpenAI 特有概念,例如 `plusCheckoutTabId`、`currentPhoneActivation`。
|
||||
|
||||
### 4.2 共享服务状态 `runtimeState.services`
|
||||
|
||||
放“可被多个 flow 复用的服务运行态”,而不是具体网站流程状态:
|
||||
|
||||
- 邮箱 provider 当前会话
|
||||
- 2925 / Hotmail / iCloud / Cloud Mail 等共享邮箱服务上下文
|
||||
- IP 代理当前应用结果、探测结果、池游标
|
||||
- 可复用的共享账号池游标
|
||||
|
||||
注意:`LuckMail` 目前不进这一层,它仍属于 OpenAI flow 私有能力。
|
||||
|
||||
### 4.3 flow 私有运行态 `runtimeState.flows[flowId]`
|
||||
|
||||
每个 flow 自己保管自己的业务运行态。当前 OpenAI 至少应拆成:
|
||||
|
||||
- `auth`:`oauthUrl`、`localhostUrl`
|
||||
- `platformBinding`:`sub2api*`、`codex2api*`
|
||||
- `plus`:`plusCheckoutTabId`、`plusCheckoutUrl`、`plusBillingAddress`
|
||||
- `phoneVerification`:`currentPhoneActivation`、`signupPhoneActivation`、`reusablePhoneActivation`
|
||||
- `luckmail`:`currentLuckmailPurchase`、`currentLuckmailMailCursor`
|
||||
- `identity`:OpenAI 特有的登录方式冻结结果、`step8VerificationTargetEmail` 等
|
||||
|
||||
### 4.4 持久配置 `settings`
|
||||
|
||||
持久配置必须按“共享 / 服务 / flow 私有”拆开,不再把所有配置都平铺到同一个 namespace。
|
||||
|
||||
特别注意:
|
||||
|
||||
- `signupMethod` 先保留在 `flows.openai`,不要急着抽成全局通用枚举。
|
||||
- `plusModeEnabled`、`plusPaymentMethod`、`gopay*`、`paypal*` 都属于 OpenAI flow 私有配置。
|
||||
- `phoneVerificationEnabled` 当前也属于 OpenAI flow 私有配置。
|
||||
|
||||
## 5. 旧步骤兼容层怎么做
|
||||
|
||||
迁移阶段仍要保留 `currentStep / stepStatuses`,但它们不再是主数据,只是 `legacyStepCompat` 的派生视图。
|
||||
|
||||
建议规则:
|
||||
|
||||
1. 新的 canonical 状态只写 `activeFlowId / currentNodeId / nodeStatuses`
|
||||
2. OpenAI flow 通过 node-to-step 映射生成 `legacyStepCompat`
|
||||
3. `getState()` 对旧模块仍返回:
|
||||
- `currentStep`
|
||||
- `stepStatuses`
|
||||
- OpenAI 旧字段镜像
|
||||
4. `setStepStatus(step, status)` 内部先更新 node,再回写 legacy step 视图
|
||||
|
||||
这样可以保证:
|
||||
|
||||
- `background/auto-run-controller.js` 先不重写也能跑
|
||||
- `background/logging-status.js` 先不重写也能继续渲染
|
||||
- `background/account-run-history.js` 仍能按旧语义落盘
|
||||
|
||||
## 6. 账号记录与自动运行的兼容策略
|
||||
|
||||
### 6.1 自动运行
|
||||
|
||||
`background/auto-run-controller.js` 当前按数字步骤推断进度、失败点和恢复点。迁移阶段要增加一层统一 selector:
|
||||
|
||||
- `getActiveRunProgress(state)`
|
||||
- `getLegacyStepCompat(state)`
|
||||
- `inferResumeNode(state, flowId)`
|
||||
|
||||
第一阶段可以继续让 OpenAI flow 通过旧 step 恢复;第二个 flow 接入时再真正改成 node-based resume。
|
||||
|
||||
### 6.2 账号记录
|
||||
|
||||
`background/account-run-history.js` 不能只记“失败在第几步”,还要开始记录:
|
||||
|
||||
- `activeFlowId`
|
||||
- `activeRunId`
|
||||
- `currentNodeId`
|
||||
- `nodeStatuses` 摘要
|
||||
- `legacyFailedStep`(仅 OpenAI 兼容)
|
||||
|
||||
否则未来多 flow 下,单看 `step7_failed` 已经没有意义。
|
||||
|
||||
## 7. 命名约束
|
||||
|
||||
仓库里已经存在别的 `flow_id` 语义,用在 GPC / GoPay 远端任务里。为了避免冲突:
|
||||
|
||||
- 扩展内部运行态统一使用 `activeFlowId`
|
||||
- 当前轮标识统一使用 `activeRunId`
|
||||
- 节点标识统一使用 `currentNodeId`
|
||||
- 不新增裸字段 `flowId`
|
||||
|
||||
`flow_id` 只保留给外部接口 payload 兼容使用。
|
||||
|
||||
## 8. 第一阶段迁移顺序
|
||||
|
||||
1. 新增状态 selector / patch helper,不改业务步骤行为。
|
||||
2. 给 OpenAI flow 建立 `flowState.openai` 命名空间,同时保留旧字段镜像。
|
||||
3. 把日志、自动运行、账号记录改为优先读 selector,不直接拼 `stepStatuses`。
|
||||
4. 把新加字段一律放进 `shared / services / flows.openai`,禁止继续向 `DEFAULT_STATE` 顶层加 OpenAI 字段。
|
||||
5. 第二个 flow 接入前,再清理旧 step 镜像的写路径。
|
||||
|
||||
## 9. 本文对应解决的缺口
|
||||
|
||||
本文主要补齐以下缺口:
|
||||
|
||||
- `DEFAULT_STATE` 扁平模型没有命名空间
|
||||
- 旧步骤状态与新 node 状态如何并存
|
||||
- 自动运行 / 日志 / 账号记录如何不被状态迁移打断
|
||||
- `registrationEmailState` 当前仍隐含“手机号是唯一可保留主身份”的假设
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
# 多注册流程邮件分层设计
|
||||
|
||||
本文专门解决“邮件轮询虽然看起来像公共能力,但实际 OpenAI 规则已经写进 provider 脚本和后台轮询里”这个问题。
|
||||
|
||||
## 1. 先说结论
|
||||
|
||||
邮件相关能力必须拆成两层,不能再只写一句“mail polling 通用化”:
|
||||
|
||||
1. `mail provider driver`
|
||||
2. `flow mail rules`
|
||||
|
||||
前者负责“怎么从 Gmail / 163 / 2925 / Inbucket / API 邮箱里拿到邮件”,后者负责“哪封邮件算当前 flow 需要的验证码 / magic link / 激活链接”。
|
||||
|
||||
## 2. 当前源码里的真实耦合
|
||||
|
||||
### 2.1 后台轮询 payload 已经带着 OpenAI 过滤条件
|
||||
|
||||
`background/verification-flow.js` 当前在 `getVerificationPollPayload()` 里直接写死:
|
||||
|
||||
- `senderFilters: ['openai', 'noreply', 'verify', 'auth', ...]`
|
||||
- `subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', ...]`
|
||||
|
||||
这不是 provider 通用逻辑,而是 OpenAI flow 规则。
|
||||
|
||||
### 2.2 内容脚本也已经写死 OpenAI / ChatGPT 正则
|
||||
|
||||
当前多个 provider 脚本里直接内嵌了 OpenAI / ChatGPT 识别:
|
||||
|
||||
- `content/gmail-mail.js`
|
||||
- `content/mail-163.js`
|
||||
- `content/mail-2925.js`
|
||||
- `content/inbucket-mail.js`
|
||||
|
||||
例如:
|
||||
|
||||
- `openai`
|
||||
- `chatgpt`
|
||||
- `verification`
|
||||
- `log-in code`
|
||||
- `strictChatGPTCodeOnly`
|
||||
|
||||
这意味着现在不是“通用 provider + OpenAI 规则”,而是“OpenAI 规则渗透进 provider 实现本身”。
|
||||
|
||||
### 2.3 LuckMail 不能直接算进共享邮件层
|
||||
|
||||
LuckMail 当前还有一个额外边界:它不只是“邮箱提供商”,还绑定了 OpenAI 项目购邮与复用语义。
|
||||
|
||||
当前事实包括:
|
||||
|
||||
- `DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai'`
|
||||
- sidepanel 明确只展示 `openai` 项目的 LuckMail 邮箱
|
||||
|
||||
所以 LuckMail 当前应留在 OpenAI flow 内,不直接并入通用 `mailProviders`。
|
||||
|
||||
## 3. 目标分层
|
||||
|
||||
## 3.1 第一层:`mail provider driver`
|
||||
|
||||
这一层只负责 provider 访问与消息归一化,不理解具体网站业务。
|
||||
|
||||
职责包括:
|
||||
|
||||
- 打开 provider 页面或请求 provider API
|
||||
- 等待页面 ready / 会话可用
|
||||
- 列邮件、开邮件、取正文、删邮件
|
||||
- 会话级去重
|
||||
- 输出统一格式的消息对象
|
||||
|
||||
建议输出结构:
|
||||
|
||||
```js
|
||||
{
|
||||
providerId: 'gmail',
|
||||
messageId: 'abc123',
|
||||
receivedAt: 1710000000000,
|
||||
sender: 'OpenAI <noreply@openai.com>',
|
||||
subject: 'Your code is 123456',
|
||||
previewText: 'Your code is 123456',
|
||||
normalizedText: 'your code is 123456',
|
||||
html: '<html>...</html>',
|
||||
links: ['https://...'],
|
||||
targetHints: ['user@example.com'],
|
||||
metadata: {},
|
||||
}
|
||||
```
|
||||
|
||||
这一层不要再写:
|
||||
|
||||
- “这是不是 OpenAI 邮件”
|
||||
- “是不是 ChatGPT 登录码”
|
||||
- “这个 code 是否必须 6 位”
|
||||
|
||||
## 3.2 第二层:`flow mail rules`
|
||||
|
||||
这一层由具体 flow 定义:
|
||||
|
||||
- 要找的邮件类型
|
||||
- 过滤条件
|
||||
- 选择规则
|
||||
- 提取规则
|
||||
- 命中后的消费动作
|
||||
|
||||
建议接口:
|
||||
|
||||
```js
|
||||
flow.mailRules = {
|
||||
signupCode: {
|
||||
buildQuery(state) {},
|
||||
matchMessage(message, state) {},
|
||||
extractArtifact(message, state) {},
|
||||
afterConsume(message, provider, state) {},
|
||||
},
|
||||
loginCode: {
|
||||
buildQuery(state) {},
|
||||
matchMessage(message, state) {},
|
||||
extractArtifact(message, state) {},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `buildQuery` 负责时间窗、目标邮箱、允许的发件人线索
|
||||
- `matchMessage` 负责判定这是不是当前 flow 的邮件
|
||||
- `extractArtifact` 负责提取 `code / magic-link / activation-link`
|
||||
- `afterConsume` 负责删除邮件、记录 cursor、保存已试 code
|
||||
|
||||
## 4. 建议的共享 mail service 接口
|
||||
|
||||
```js
|
||||
mailService.poll({
|
||||
providerId: 'gmail',
|
||||
rule: flow.mailRules.signupCode,
|
||||
state,
|
||||
maxAttempts: 5,
|
||||
intervalMs: 3000,
|
||||
});
|
||||
```
|
||||
|
||||
内部流程:
|
||||
|
||||
1. provider driver 按 query 拉取候选消息
|
||||
2. rule 决定哪封命中
|
||||
3. rule 提取 artifact
|
||||
4. service 返回统一结果
|
||||
|
||||
返回值示例:
|
||||
|
||||
```js
|
||||
{
|
||||
ok: true,
|
||||
artifact: {
|
||||
type: 'code',
|
||||
value: '123456',
|
||||
},
|
||||
message: { ...normalizedMessage },
|
||||
}
|
||||
```
|
||||
|
||||
## 5. OpenAI flow 当前应该拆出的规则
|
||||
|
||||
OpenAI 至少需要单独拆出这些规则文件:
|
||||
|
||||
- `flows/openai/mail-rules/signup-code.js`
|
||||
- `flows/openai/mail-rules/login-code.js`
|
||||
- `flows/openai/mail-rules/add-email-code.js`
|
||||
|
||||
它们负责承接现在散落在这些地方的规则:
|
||||
|
||||
- `verification-flow.js` 中的 sender / subject filters
|
||||
- `gmail-mail.js` / `mail-163.js` / `mail-2925.js` / `inbucket-mail.js` 中的 OpenAI / ChatGPT 正则
|
||||
- `strictChatGPTCodeOnly`
|
||||
- Step 4 与 Step 8 不同的时间窗和目标邮箱逻辑
|
||||
|
||||
## 6. provider driver 的拆分建议
|
||||
|
||||
### 6.1 可以进入共享层的 provider
|
||||
|
||||
这些 provider 的访问方式本身有共享价值,可以做成共享 mail driver:
|
||||
|
||||
- Gmail
|
||||
- 163 / 126 / 163 VIP
|
||||
- 2925
|
||||
- Inbucket
|
||||
- iCloud Mail
|
||||
- Hotmail helper / Graph Mail
|
||||
- Cloud Mail / SkyMail
|
||||
|
||||
### 6.2 暂时不进入共享层的 provider
|
||||
|
||||
- LuckMail:当前仍是 OpenAI 项目购邮与验证码轮询能力的一部分
|
||||
|
||||
如果未来第二个 flow 也明确复用 LuckMail,且复用的是“同一套 project / token / 生命周期”,再考虑把它抽出来。
|
||||
|
||||
## 7. `registrationEmailState` 与邮件层的关系
|
||||
|
||||
`registrationEmailState` 负责“当前流程邮箱身份”,不是“provider 拉信规则”。
|
||||
|
||||
所以后续边界应是:
|
||||
|
||||
- `registrationEmailState`:运行态身份记录
|
||||
- `mail provider driver`:拉信能力
|
||||
- `flow mail rules`:判定与提取
|
||||
|
||||
不要再在 `registrationEmailState` 或 provider driver 里夹带 OpenAI `add-email` 业务判断。
|
||||
|
||||
## 8. 迁移顺序
|
||||
|
||||
1. 先把 `background/verification-flow.js` 里的查询构造迁到 `flows/openai/mail-rules/*`
|
||||
2. 再把各 provider 脚本里的 OpenAI / ChatGPT 正则搬出到 rule 层
|
||||
3. 保留 provider 里的“消息抓取 / 页面操作 / 删除策略”
|
||||
4. 等第二个 flow 接入时,只新增它自己的 `mail-rules`,不改 Gmail / 163 / 2925 driver
|
||||
|
||||
## 9. 本文对应解决的缺口
|
||||
|
||||
本文主要补齐以下缺口:
|
||||
|
||||
- “Mail polling 通用化”表述过于粗糙,没有说明 provider 与 rule 两层
|
||||
- provider 内容脚本里已经嵌入 OpenAI 规则
|
||||
- `strictChatGPTCodeOnly` 这类开关的归属边界不清
|
||||
- LuckMail 当前其实还是 OpenAI flow 私有邮件能力,不是共享 provider
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
(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';
|
||||
const OPENAI_CODE_PATTERNS = Object.freeze([
|
||||
Object.freeze({
|
||||
source: '(?:chatgpt\\s+log-?in\\s+code|enter\\s+this\\s+code)[^0-9]{0,24}(\\d{6})',
|
||||
flags: 'i',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: 'your\\s+chatgpt\\s+code\\s+is\\s+(\\d{6})',
|
||||
flags: 'i',
|
||||
}),
|
||||
Object.freeze({
|
||||
source: '(?:verification\\s+code|temporary\\s+verification\\s+code|your\\s+chatgpt\\s+code|code(?:\\s+is)?)[^0-9]{0,16}(\\d{6})',
|
||||
flags: 'i',
|
||||
}),
|
||||
]);
|
||||
const OPENAI_REQUIRED_KEYWORDS = Object.freeze([
|
||||
'openai',
|
||||
'chatgpt',
|
||||
'verify',
|
||||
'verification',
|
||||
'confirm',
|
||||
'验证码',
|
||||
'代码',
|
||||
]);
|
||||
|
||||
function buildTargetEmailHints(targetEmail = '') {
|
||||
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
|
||||
if (!normalizedTarget) {
|
||||
return [];
|
||||
}
|
||||
const hints = [normalizedTarget];
|
||||
const atIndex = normalizedTarget.indexOf('@');
|
||||
if (atIndex > 0) {
|
||||
hints.push(`${normalizedTarget.slice(0, atIndex)}=${normalizedTarget.slice(atIndex + 1)}`);
|
||||
}
|
||||
return [...new Set(hints)];
|
||||
}
|
||||
|
||||
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;
|
||||
const targetEmail = signupStep
|
||||
? state?.email
|
||||
: (String(state?.step8VerificationTargetEmail || '').trim() || state?.email);
|
||||
|
||||
return {
|
||||
flowId: 'openai',
|
||||
ruleId: signupStep ? SIGNUP_CODE_RULE_ID : LOGIN_CODE_RULE_ID,
|
||||
step: normalizedStep,
|
||||
artifactType: 'code',
|
||||
codePatterns: OPENAI_CODE_PATTERNS,
|
||||
filterAfterTimestamp: mail2925Provider
|
||||
? 0
|
||||
: getHotmailVerificationRequestTimestamp(normalizedStep, state),
|
||||
requiredKeywords: signupStep
|
||||
? OPENAI_REQUIRED_KEYWORDS
|
||||
: [...OPENAI_REQUIRED_KEYWORDS, 'login'],
|
||||
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,
|
||||
targetEmailHints: buildTargetEmailHints(targetEmail),
|
||||
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,
|
||||
};
|
||||
});
|
||||
+60
-13
@@ -32,13 +32,49 @@
|
||||
: HOTMAIL_SERVICE_MODE_LOCAL;
|
||||
}
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
function normalizeRulePatternList(patterns = []) {
|
||||
return Array.isArray(patterns) ? patterns : [];
|
||||
}
|
||||
|
||||
function extractCodeByRulePatterns(text, patterns = []) {
|
||||
const normalizedText = String(text || '');
|
||||
for (const pattern of normalizeRulePatternList(patterns)) {
|
||||
try {
|
||||
const source = String(pattern?.source || '').trim();
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
|
||||
const match = normalizedText.match(new RegExp(source, flags));
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
for (let index = 1; index < match.length; index += 1) {
|
||||
const candidate = String(match[index] || '').trim();
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
if (String(match[0] || '').trim()) {
|
||||
return String(match[0] || '').trim();
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore invalid runtime rule patterns and continue with other candidates.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractVerificationCode(text, options = {}) {
|
||||
const source = String(text || '');
|
||||
const matchedByRule = extractCodeByRulePatterns(source, options?.codePatterns);
|
||||
if (matchedByRule) return matchedByRule;
|
||||
|
||||
const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchOpenAiLogin = source.match(/(?:chatgpt\s+log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
||||
const matchLoginCode = source.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchLoginCode) return matchLoginCode[1];
|
||||
|
||||
const matchEn = source.match(/code(?:\s+is|[\s:])+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1];
|
||||
@@ -47,7 +83,7 @@
|
||||
return matchStandalone ? matchStandalone[1] : null;
|
||||
}
|
||||
|
||||
function extractVerificationCodeFromMessage(message = {}) {
|
||||
function extractVerificationCodeFromMessage(message = {}, options = {}) {
|
||||
const sender = firstNonEmptyString([
|
||||
message?.from?.emailAddress?.address,
|
||||
message?.sender,
|
||||
@@ -55,7 +91,9 @@
|
||||
]);
|
||||
const subject = firstNonEmptyString([message?.subject]);
|
||||
const preview = firstNonEmptyString([message?.bodyPreview, message?.preview, message?.text]);
|
||||
return extractVerificationCode([subject, preview, sender].filter(Boolean).join(' '));
|
||||
return extractVerificationCode([subject, preview, sender].filter(Boolean).join(' '), {
|
||||
codePatterns: options?.codePatterns,
|
||||
});
|
||||
}
|
||||
|
||||
function getLatestHotmailMessage(messages) {
|
||||
@@ -138,6 +176,10 @@
|
||||
function messageMatchesFilters(message, filters = {}) {
|
||||
const senderFilters = (filters.senderFilters || []).map(normalizeText).filter(Boolean);
|
||||
const subjectFilters = (filters.subjectFilters || []).map(normalizeText).filter(Boolean);
|
||||
const requiredKeywords = (filters.requiredKeywords || []).map(normalizeText).filter(Boolean);
|
||||
const hasSenderFilters = senderFilters.length > 0;
|
||||
const hasSubjectFilters = subjectFilters.length > 0;
|
||||
const hasKeywordHints = requiredKeywords.length > 0;
|
||||
const afterTimestamp = normalizeTimestamp(filters.afterTimestamp);
|
||||
const receivedAt = normalizeTimestamp(message?.receivedDateTime);
|
||||
if (afterTimestamp && receivedAt && receivedAt < afterTimestamp) {
|
||||
@@ -148,20 +190,25 @@
|
||||
const subject = normalizeText(message?.subject);
|
||||
const preview = String(message?.bodyPreview || '');
|
||||
const combinedText = [subject, sender, preview].filter(Boolean).join(' ');
|
||||
const code = extractVerificationCode(combinedText);
|
||||
const code = extractVerificationCode(combinedText, {
|
||||
codePatterns: filters.codePatterns,
|
||||
});
|
||||
const excludedCodes = new Set((filters.excludeCodes || []).filter(Boolean));
|
||||
if (code && excludedCodes.has(code)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const senderMatch = senderFilters.length === 0
|
||||
? true
|
||||
: senderFilters.some((item) => sender.includes(item) || normalizeText(preview).includes(item));
|
||||
const subjectMatch = subjectFilters.length === 0
|
||||
? true
|
||||
: subjectFilters.some((item) => subject.includes(item) || normalizeText(preview).includes(item));
|
||||
const senderMatch = hasSenderFilters
|
||||
? senderFilters.some((item) => sender.includes(item) || normalizeText(preview).includes(item))
|
||||
: false;
|
||||
const subjectMatch = hasSubjectFilters
|
||||
? subjectFilters.some((item) => subject.includes(item) || normalizeText(preview).includes(item))
|
||||
: false;
|
||||
const keywordMatch = hasKeywordHints
|
||||
? requiredKeywords.some((item) => normalizeText(combinedText).includes(item))
|
||||
: false;
|
||||
|
||||
if (!senderMatch && !subjectMatch) {
|
||||
if ((hasSenderFilters || hasSubjectFilters || hasKeywordHints) && !senderMatch && !subjectMatch && !keywordMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "codex-oauth-automation-extension",
|
||||
"version": "9.7",
|
||||
"version_name": "Ultra9.7",
|
||||
"version": "9.9",
|
||||
"version_name": "Ultra9.9",
|
||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||
"permissions": [
|
||||
"sidePanel",
|
||||
@@ -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"
|
||||
|
||||
+64
-25
@@ -1,8 +1,4 @@
|
||||
(function attachMicrosoftEmailHelpers(globalScope) {
|
||||
const OPENAI_SENDER_PATTERNS = [
|
||||
/openai\.com/i,
|
||||
/auth0\.openai\.com/i,
|
||||
];
|
||||
const CODE_PATTERN = /\b(\d{6})\b/;
|
||||
const GRAPH_SCOPES = 'offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/User.Read';
|
||||
const GRAPH_DEFAULT_SCOPE = 'https://graph.microsoft.com/.default';
|
||||
@@ -179,6 +175,57 @@
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeRulePatternList(patterns = []) {
|
||||
return Array.isArray(patterns) ? patterns : [];
|
||||
}
|
||||
|
||||
function extractCodeByRulePatterns(text, patterns = []) {
|
||||
const normalizedText = String(text || '');
|
||||
for (const pattern of normalizeRulePatternList(patterns)) {
|
||||
try {
|
||||
const source = String(pattern?.source || '').trim();
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
|
||||
const match = normalizedText.match(new RegExp(source, flags));
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
for (let index = 1; index < match.length; index += 1) {
|
||||
const candidate = String(match[index] || '').trim();
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
if (String(match[0] || '').trim()) {
|
||||
return String(match[0] || '').trim();
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore invalid runtime rule patterns and continue with other candidates.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractVerificationCode(text, options = {}) {
|
||||
const source = String(text || '');
|
||||
const matchedByRule = extractCodeByRulePatterns(source, options?.codePatterns);
|
||||
if (matchedByRule) return matchedByRule;
|
||||
|
||||
const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchLoginCode = source.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||
if (matchLoginCode) return matchLoginCode[1];
|
||||
|
||||
const matchEn = source.match(/code(?:\s+is|[\s:])+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1];
|
||||
|
||||
const matchStandalone = source.match(CODE_PATTERN);
|
||||
return matchStandalone?.[1] || '';
|
||||
}
|
||||
|
||||
function getMessageSender(message) {
|
||||
return String(
|
||||
message?.from?.emailAddress?.address
|
||||
@@ -203,22 +250,13 @@
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function isOpenAiMessage(message) {
|
||||
const sender = getMessageSender(message);
|
||||
if (OPENAI_SENDER_PATTERNS.some((pattern) => pattern.test(sender))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const searchText = getMessageSearchText(message);
|
||||
return OPENAI_SENDER_PATTERNS.some((pattern) => pattern.test(searchText));
|
||||
}
|
||||
|
||||
function extractVerificationCodeFromMessages(messages, options = {}) {
|
||||
const filterAfterTimestamp = Number(options.filterAfterTimestamp || 0) || 0;
|
||||
const senderFilters = (options.senderFilters || []).map(normalizeFilterValue).filter(Boolean);
|
||||
const subjectFilters = (options.subjectFilters || []).map(normalizeFilterValue).filter(Boolean);
|
||||
const requiredKeywords = (options.requiredKeywords || []).map(normalizeFilterValue).filter(Boolean);
|
||||
const excludedCodes = new Set((options.excludeCodes || []).map((value) => String(value || '').trim()).filter(Boolean));
|
||||
const hasExplicitFilters = senderFilters.length > 0 || subjectFilters.length > 0;
|
||||
const hasExplicitFilters = senderFilters.length > 0 || subjectFilters.length > 0 || requiredKeywords.length > 0;
|
||||
|
||||
const sortedMessages = (Array.isArray(messages) ? messages : [])
|
||||
.map((raw) => normalizeMessage(raw, raw?.mailbox))
|
||||
@@ -234,23 +272,23 @@
|
||||
const subject = normalizeFilterValue(message?.subject);
|
||||
const preview = normalizeFilterValue(message?.bodyPreview);
|
||||
const searchText = normalizeFilterValue(getMessageSearchText(message));
|
||||
const codeMatch = getMessageSearchText(message).match(CODE_PATTERN);
|
||||
const code = codeMatch?.[1] || '';
|
||||
const code = extractVerificationCode(getMessageSearchText(message), {
|
||||
codePatterns: options.codePatterns,
|
||||
});
|
||||
if (!code || excludedCodes.has(code)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasExplicitFilters && !isOpenAiMessage(message)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const senderMatched = senderFilters.length === 0
|
||||
? true
|
||||
? false
|
||||
: senderFilters.some((filter) => sender.includes(filter) || preview.includes(filter) || searchText.includes(filter));
|
||||
const subjectMatched = subjectFilters.length === 0
|
||||
? true
|
||||
? false
|
||||
: subjectFilters.some((filter) => subject.includes(filter) || preview.includes(filter) || searchText.includes(filter));
|
||||
if (!senderMatched && !subjectMatched) {
|
||||
const keywordMatched = requiredKeywords.length === 0
|
||||
? false
|
||||
: requiredKeywords.some((filter) => preview.includes(filter) || searchText.includes(filter));
|
||||
if (hasExplicitFilters && !senderMatched && !subjectMatched && !keywordMatched) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -401,6 +439,8 @@
|
||||
filterAfterTimestamp,
|
||||
senderFilters,
|
||||
subjectFilters,
|
||||
requiredKeywords: options.requiredKeywords,
|
||||
codePatterns: options.codePatterns,
|
||||
excludeCodes,
|
||||
});
|
||||
if (match) {
|
||||
@@ -437,7 +477,6 @@
|
||||
fetchOutlookMessages,
|
||||
getMessageSender,
|
||||
getMessageTimestamp,
|
||||
isOpenAiMessage,
|
||||
normalizeMailboxId,
|
||||
normalizeMailboxLabel,
|
||||
normalizeMessage,
|
||||
|
||||
+38
-44
@@ -122,12 +122,6 @@ def compact_text(value, limit=400):
|
||||
def log_info(message):
|
||||
print(f"[HotmailHelper] {message}", flush=True)
|
||||
|
||||
|
||||
def is_openai_sender(address):
|
||||
sender = str(address or "").strip().lower()
|
||||
return "openai" in sender
|
||||
|
||||
|
||||
def get_message_body_content(message):
|
||||
body = message.get("body") or {}
|
||||
if not isinstance(body, dict):
|
||||
@@ -135,36 +129,6 @@ def get_message_body_content(message):
|
||||
return str(body.get("content") or "").strip()
|
||||
|
||||
|
||||
def log_openai_messages(messages, transport=""):
|
||||
for message in messages or []:
|
||||
sender_info = message.get("from", {}).get("emailAddress", {}) or {}
|
||||
sender = str(sender_info.get("address") or "").strip()
|
||||
if not is_openai_sender(sender):
|
||||
continue
|
||||
|
||||
sender_name = str(sender_info.get("name") or "").strip()
|
||||
mailbox = str(message.get("mailbox") or "").strip() or "INBOX"
|
||||
subject = str(message.get("subject") or "").strip()
|
||||
transport_label = str(transport or "").strip() or "unknown"
|
||||
base = (
|
||||
f"transport={transport_label} mailbox={mailbox} sender={sender} "
|
||||
f"senderName={sender_name or '-'} subject={subject}"
|
||||
)
|
||||
|
||||
log_info(f"openai mail received {base}")
|
||||
|
||||
body_content = get_message_body_content(message)
|
||||
if body_content:
|
||||
log_info(f"openai mail full body start {base}")
|
||||
print(body_content, flush=True)
|
||||
log_info("openai mail full body end")
|
||||
continue
|
||||
|
||||
preview = str(message.get("bodyPreview") or "").strip()
|
||||
if preview:
|
||||
log_info(f"openai mail preview {base} preview={compact_text(preview, 1000)}")
|
||||
|
||||
|
||||
def get_proxy_debug_context():
|
||||
names = ["all_proxy", "http_proxy", "https_proxy", "ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY"]
|
||||
parts = []
|
||||
@@ -725,7 +689,6 @@ def collect_messages(email_addr, client_id, refresh_token, mailboxes, top):
|
||||
try:
|
||||
log_info(f"message collection start transport={transport_name}")
|
||||
result = collector(email_addr, client_id, refresh_token, mailboxes, top)
|
||||
log_openai_messages(result.get("messages") or [], transport=transport_name)
|
||||
log_info(
|
||||
f"message collection success transport={transport_name} "
|
||||
f"tokenEndpoint={result['token_payload'].get('token_endpoint', '')}"
|
||||
@@ -739,10 +702,37 @@ def collect_messages(email_addr, client_id, refresh_token, mailboxes, top):
|
||||
raise RuntimeError(f"Message collection failed on all transports: {' | '.join(errors)}")
|
||||
|
||||
|
||||
def extract_code(text):
|
||||
def extract_code(text, code_patterns=None):
|
||||
source = str(text or "")
|
||||
for pattern in code_patterns or []:
|
||||
try:
|
||||
source_pattern = str((pattern or {}).get("source") or "").strip()
|
||||
if not source_pattern:
|
||||
continue
|
||||
flags = str((pattern or {}).get("flags") or "").lower()
|
||||
re_flags = 0
|
||||
if "i" in flags:
|
||||
re_flags |= re.IGNORECASE
|
||||
if "m" in flags:
|
||||
re_flags |= re.MULTILINE
|
||||
if "s" in flags:
|
||||
re_flags |= re.DOTALL
|
||||
match = re.search(source_pattern, source, flags=re_flags)
|
||||
if not match:
|
||||
continue
|
||||
if match.lastindex:
|
||||
for group_index in range(1, match.lastindex + 1):
|
||||
candidate = str(match.group(group_index) or "").strip()
|
||||
if candidate:
|
||||
return candidate
|
||||
candidate = str(match.group(0) or "").strip()
|
||||
if candidate:
|
||||
return candidate
|
||||
except re.error:
|
||||
continue
|
||||
patterns = [
|
||||
r"(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})",
|
||||
r"(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})",
|
||||
r"code(?:\s+is|[\s:])+(\d{6})",
|
||||
r"\b(\d{6})\b",
|
||||
]
|
||||
@@ -753,9 +743,10 @@ def extract_code(text):
|
||||
return ""
|
||||
|
||||
|
||||
def select_latest_code(messages, sender_filters, subject_filters, exclude_codes, filter_after_timestamp):
|
||||
def select_latest_code(messages, sender_filters, subject_filters, exclude_codes, filter_after_timestamp, required_keywords=None, code_patterns=None):
|
||||
sender_keywords = [str(item).strip().lower() for item in sender_filters or [] if str(item).strip()]
|
||||
subject_keywords = [str(item).strip().lower() for item in subject_filters or [] if str(item).strip()]
|
||||
required_keyword_hints = [str(item).strip().lower() for item in required_keywords or [] if str(item).strip()]
|
||||
excluded = {str(item).strip() for item in exclude_codes or [] if str(item).strip()}
|
||||
|
||||
def match_message(message, apply_time_filter):
|
||||
@@ -767,17 +758,18 @@ def select_latest_code(messages, sender_filters, subject_filters, exclude_codes,
|
||||
subject = str(message.get("subject", ""))
|
||||
preview = str(message.get("bodyPreview", ""))
|
||||
combined = " ".join([sender, subject.lower(), preview.lower()])
|
||||
code = extract_code(" ".join([subject, preview, sender]))
|
||||
code = extract_code(" ".join([subject, preview, sender]), code_patterns=code_patterns)
|
||||
if not code:
|
||||
body_content = get_message_body_content(message)
|
||||
if body_content:
|
||||
code = extract_code(" ".join([subject, body_content, sender]))
|
||||
code = extract_code(" ".join([subject, body_content, sender]), code_patterns=code_patterns)
|
||||
if not code or code in excluded:
|
||||
return None
|
||||
|
||||
sender_ok = not sender_keywords or any(keyword in combined for keyword in sender_keywords)
|
||||
subject_ok = not subject_keywords or any(keyword in combined for keyword in subject_keywords)
|
||||
if not sender_ok and not subject_ok:
|
||||
sender_ok = bool(sender_keywords) and any(keyword in combined for keyword in sender_keywords)
|
||||
subject_ok = bool(subject_keywords) and any(keyword in combined for keyword in subject_keywords)
|
||||
keyword_ok = bool(required_keyword_hints) and any(keyword in combined for keyword in required_keyword_hints)
|
||||
if (sender_keywords or subject_keywords or required_keyword_hints) and not sender_ok and not subject_ok and not keyword_ok:
|
||||
return None
|
||||
|
||||
return {"code": code, "message": message}
|
||||
@@ -862,6 +854,8 @@ class HotmailHelperHandler(BaseHTTPRequestHandler):
|
||||
payload.get("subjectFilters") or [],
|
||||
payload.get("excludeCodes") or [],
|
||||
int(payload.get("filterAfterTimestamp") or 0),
|
||||
payload.get("requiredKeywords") or [],
|
||||
payload.get("codePatterns") or [],
|
||||
)
|
||||
json_response(self, 200, {
|
||||
"ok": True,
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
(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 MODE_SWITCH_RELEVANT_KEYS = Object.freeze([
|
||||
'activeFlowId',
|
||||
'contributionMode',
|
||||
'panelMode',
|
||||
'phoneVerificationEnabled',
|
||||
'plusModeEnabled',
|
||||
'signupMethod',
|
||||
]);
|
||||
|
||||
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 getPanelModeLabel(panelMode = '') {
|
||||
const normalized = normalizePanelMode(panelMode);
|
||||
if (normalized === 'sub2api') {
|
||||
return 'SUB2API';
|
||||
}
|
||||
if (normalized === 'codex2api') {
|
||||
return 'Codex2API';
|
||||
}
|
||||
return 'CPA';
|
||||
}
|
||||
|
||||
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 normalizeChangedKeys(values = []) {
|
||||
const list = Array.isArray(values) ? values : [];
|
||||
const seen = new Set();
|
||||
const normalized = [];
|
||||
list.forEach((value) => {
|
||||
const key = String(value || '').trim();
|
||||
if (!key || seen.has(key)) {
|
||||
return;
|
||||
}
|
||||
seen.add(key);
|
||||
normalized.push(key);
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
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 buildPhoneSignupValidationError(capabilityState = {}) {
|
||||
const flowState = capabilityState.flowCapabilities || {};
|
||||
const panelState = capabilityState.panelCapabilities || {};
|
||||
const runtimeLocks = capabilityState.runtimeLocks || {};
|
||||
|
||||
if (!flowState.supportsPhoneSignup) {
|
||||
return {
|
||||
code: 'phone_signup_flow_unsupported',
|
||||
message: '当前 flow 不支持手机号注册。',
|
||||
};
|
||||
}
|
||||
if (!panelState.supportsPhoneSignup) {
|
||||
return {
|
||||
code: 'phone_signup_panel_unsupported',
|
||||
message: `当前面板模式 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 不支持手机号注册。`,
|
||||
};
|
||||
}
|
||||
if (!runtimeLocks.phoneVerificationEnabled) {
|
||||
return {
|
||||
code: 'phone_signup_phone_verification_disabled',
|
||||
message: '请先开启接码功能后再使用手机号注册。',
|
||||
};
|
||||
}
|
||||
if (runtimeLocks.plusModeEnabled) {
|
||||
return {
|
||||
code: 'phone_signup_plus_mode_locked',
|
||||
message: 'Plus 模式开启时不能使用手机号注册。',
|
||||
};
|
||||
}
|
||||
if (runtimeLocks.contributionMode) {
|
||||
return {
|
||||
code: 'phone_signup_contribution_mode_locked',
|
||||
message: '贡献模式开启时不能使用手机号注册。',
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: 'phone_signup_unavailable',
|
||||
message: '当前设置暂不支持手机号注册。',
|
||||
};
|
||||
}
|
||||
|
||||
function validateAutoRunStart(options = {}) {
|
||||
const state = options?.state || {};
|
||||
const capabilityState = resolveSidepanelCapabilities(options);
|
||||
const errors = [];
|
||||
|
||||
if (
|
||||
Array.isArray(capabilityState.supportedPanelModes)
|
||||
&& capabilityState.supportedPanelModes.length > 0
|
||||
&& capabilityState.canUseSelectedPanelMode === false
|
||||
) {
|
||||
errors.push({
|
||||
code: 'panel_mode_unsupported',
|
||||
message: `当前 flow 不支持 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 面板模式。`,
|
||||
});
|
||||
}
|
||||
|
||||
if (Boolean(state?.plusModeEnabled) && !capabilityState.flowCapabilities?.supportsPlusMode) {
|
||||
errors.push({
|
||||
code: 'plus_mode_unsupported',
|
||||
message: '当前 flow 不支持 Plus 模式。',
|
||||
});
|
||||
}
|
||||
|
||||
if (Boolean(state?.contributionMode) && !capabilityState.flowCapabilities?.supportsContributionMode) {
|
||||
errors.push({
|
||||
code: 'contribution_mode_unsupported',
|
||||
message: '当前 flow 不支持贡献模式。',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
capabilityState.requestedSignupMethod === SIGNUP_METHOD_PHONE
|
||||
&& capabilityState.effectiveSignupMethod !== SIGNUP_METHOD_PHONE
|
||||
) {
|
||||
errors.push(buildPhoneSignupValidationError(capabilityState));
|
||||
}
|
||||
|
||||
return {
|
||||
ok: errors.length === 0,
|
||||
errors,
|
||||
capabilityState,
|
||||
};
|
||||
}
|
||||
|
||||
function validateModeSwitch(options = {}) {
|
||||
const state = options?.state || {};
|
||||
const changedKeys = normalizeChangedKeys(
|
||||
options?.changedKeys !== undefined
|
||||
? options.changedKeys
|
||||
: Object.keys(state || {})
|
||||
);
|
||||
const changedKeySet = new Set(changedKeys);
|
||||
const capabilityState = resolveSidepanelCapabilities(options);
|
||||
const errors = [];
|
||||
const normalizedUpdates = {};
|
||||
const flowState = capabilityState.flowCapabilities || {};
|
||||
const requestedPhoneSignup = capabilityState.requestedSignupMethod === SIGNUP_METHOD_PHONE;
|
||||
const shouldReconcileSignupMethod = MODE_SWITCH_RELEVANT_KEYS.some((key) => changedKeySet.has(key));
|
||||
|
||||
if (
|
||||
changedKeySet.has('panelMode')
|
||||
&& Array.isArray(capabilityState.supportedPanelModes)
|
||||
&& capabilityState.supportedPanelModes.length > 0
|
||||
&& capabilityState.canUseSelectedPanelMode === false
|
||||
) {
|
||||
normalizedUpdates.panelMode = capabilityState.effectivePanelMode;
|
||||
errors.push({
|
||||
code: 'panel_mode_unsupported',
|
||||
message: `当前 flow 不支持 ${getPanelModeLabel(capabilityState.requestedPanelMode)} 面板模式。`,
|
||||
});
|
||||
}
|
||||
|
||||
if (changedKeySet.has('plusModeEnabled') && Boolean(state?.plusModeEnabled) && !flowState.supportsPlusMode) {
|
||||
normalizedUpdates.plusModeEnabled = false;
|
||||
errors.push({
|
||||
code: 'plus_mode_unsupported',
|
||||
message: '当前 flow 不支持 Plus 模式。',
|
||||
});
|
||||
}
|
||||
|
||||
if (changedKeySet.has('contributionMode') && Boolean(state?.contributionMode) && !flowState.supportsContributionMode) {
|
||||
normalizedUpdates.contributionMode = false;
|
||||
errors.push({
|
||||
code: 'contribution_mode_unsupported',
|
||||
message: '当前 flow 不支持贡献模式。',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
changedKeySet.has('phoneVerificationEnabled')
|
||||
&& Boolean(state?.phoneVerificationEnabled)
|
||||
&& !flowState.supportsPhoneVerificationSettings
|
||||
) {
|
||||
normalizedUpdates.phoneVerificationEnabled = false;
|
||||
errors.push({
|
||||
code: 'phone_verification_unsupported',
|
||||
message: '当前 flow 不支持接码配置。',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
shouldReconcileSignupMethod
|
||||
&& requestedPhoneSignup
|
||||
&& capabilityState.effectiveSignupMethod !== SIGNUP_METHOD_PHONE
|
||||
) {
|
||||
normalizedUpdates.signupMethod = capabilityState.effectiveSignupMethod;
|
||||
errors.push(buildPhoneSignupValidationError(capabilityState));
|
||||
}
|
||||
|
||||
return {
|
||||
ok: errors.length === 0,
|
||||
changedKeys,
|
||||
capabilityState,
|
||||
errors,
|
||||
normalizedUpdates,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
validateAutoRunStart,
|
||||
validateModeSwitch,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
});
|
||||
@@ -110,6 +110,14 @@
|
||||
|
||||
<section id="data-section">
|
||||
<div id="settings-card" class="data-card">
|
||||
<div class="data-row">
|
||||
<span class="data-label">Flow</span>
|
||||
<select id="select-flow" class="data-select" aria-label="Flow 选择">
|
||||
<option value="codex" selected>codex</option>
|
||||
<option value="pending-1" disabled>待添加</option>
|
||||
<option value="pending-2" disabled>待添加</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">来源</span>
|
||||
<select id="select-panel-mode" class="data-select">
|
||||
@@ -1732,6 +1740,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>
|
||||
|
||||
+474
-31
@@ -517,6 +517,8 @@ 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 latestState = null;
|
||||
let currentPlusModeEnabled = false;
|
||||
let currentPlusPaymentMethod = DEFAULT_PLUS_PAYMENT_METHOD;
|
||||
let currentSignupMethod = DEFAULT_SIGNUP_METHOD;
|
||||
@@ -788,6 +790,7 @@ function initPhoneVerificationSectionExpandedState() {
|
||||
}
|
||||
|
||||
function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
|
||||
const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID !== 'undefined' ? DEFAULT_ACTIVE_FLOW_ID : 'openai';
|
||||
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
|
||||
const rawPaymentMethod = typeof options === 'string'
|
||||
? options
|
||||
@@ -795,7 +798,11 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
|
||||
const rawSignupMethod = typeof options === 'string'
|
||||
? currentSignupMethod
|
||||
: (options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
|
||||
const activeFlowId = typeof options === 'string'
|
||||
? ((typeof latestState !== 'undefined' ? latestState?.activeFlowId : '') || defaultFlowId)
|
||||
: (options.activeFlowId || (typeof latestState !== 'undefined' ? latestState?.activeFlowId : '') || defaultFlowId);
|
||||
return (window.MultiPageStepDefinitions?.getSteps?.({
|
||||
activeFlowId: String(activeFlowId || '').trim().toLowerCase() || defaultFlowId,
|
||||
plusModeEnabled,
|
||||
plusPaymentMethod: normalizePlusPaymentMethod(rawPaymentMethod),
|
||||
signupMethod: normalizeSignupMethod(rawSignupMethod),
|
||||
@@ -829,6 +836,7 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
|
||||
currentPlusPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod);
|
||||
currentSignupMethod = normalizeSignupMethod(rawSignupMethod);
|
||||
stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled, {
|
||||
activeFlowId: options?.activeFlowId,
|
||||
plusPaymentMethod: currentPlusPaymentMethod,
|
||||
signupMethod: currentSignupMethod,
|
||||
});
|
||||
@@ -1116,7 +1124,6 @@ function validateCurrentRegistrationEmail(email = inputEmail.value.trim(), optio
|
||||
return false;
|
||||
}
|
||||
|
||||
let latestState = null;
|
||||
let currentAutoRun = {
|
||||
autoRunning: false,
|
||||
phase: 'idle',
|
||||
@@ -1931,6 +1938,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 +3510,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 +3618,7 @@ function collectSettingsPayload() {
|
||||
});
|
||||
return {
|
||||
...(contributionModeEnabled ? {} : {
|
||||
panelMode: selectPanelMode.value,
|
||||
panelMode: effectivePanelMode,
|
||||
}),
|
||||
vpsUrl: inputVpsUrl.value.trim(),
|
||||
vpsPassword: inputVpsPassword.value,
|
||||
@@ -3565,9 +3656,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 +3771,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 +7304,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 +7388,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 +7470,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 +7554,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 +7668,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 +7725,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 +7909,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';
|
||||
@@ -8246,6 +8530,7 @@ function renderStepsList() {
|
||||
}
|
||||
|
||||
function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOrOptions = {}, maybeOptions = {}) {
|
||||
const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID !== 'undefined' ? DEFAULT_ACTIVE_FLOW_ID : 'openai';
|
||||
const nextPlusModeEnabled = Boolean(plusModeEnabled);
|
||||
const options = typeof plusPaymentMethodOrOptions === 'string'
|
||||
? maybeOptions
|
||||
@@ -8255,9 +8540,15 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
|
||||
: (options.plusPaymentMethod || getSelectedPlusPaymentMethod(latestState));
|
||||
const nextSignupMethod = normalizeSignupMethod(options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
|
||||
const nextPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod);
|
||||
const nextActiveFlowId = String(
|
||||
options.activeFlowId
|
||||
|| (typeof latestState !== 'undefined' ? latestState?.activeFlowId : '')
|
||||
|| defaultFlowId
|
||||
).trim().toLowerCase() || defaultFlowId;
|
||||
const rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||
const currentPaymentStep = stepDefinitions.find((step) => step.key === 'paypal-approve');
|
||||
const nextPaymentTitle = rootScope.MultiPageStepDefinitions?.getPlusPaymentStepTitle?.({
|
||||
activeFlowId: nextActiveFlowId,
|
||||
plusModeEnabled: nextPlusModeEnabled,
|
||||
plusPaymentMethod: nextPaymentMethod,
|
||||
signupMethod: nextSignupMethod,
|
||||
@@ -8273,6 +8564,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
|
||||
}
|
||||
|
||||
rebuildStepDefinitionState(nextPlusModeEnabled, {
|
||||
activeFlowId: nextActiveFlowId,
|
||||
plusPaymentMethod: nextPaymentMethod,
|
||||
signupMethod: nextSignupMethod,
|
||||
});
|
||||
@@ -8285,9 +8577,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 +10076,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 +10130,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 +10552,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;
|
||||
}
|
||||
@@ -11492,6 +11848,37 @@ async function startAutoRunFromCurrentSettings() {
|
||||
if (typeof persistCurrentSettingsForAction === 'function') {
|
||||
await persistCurrentSettingsForAction();
|
||||
}
|
||||
const autoRunStartValidation = (() => {
|
||||
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;
|
||||
if (!registry?.validateAutoRunStart) {
|
||||
return { ok: true, errors: [] };
|
||||
}
|
||||
const validationState = {
|
||||
...(latestState || {}),
|
||||
panelMode: typeof getSelectedPanelMode === 'function' ? getSelectedPanelMode() : latestState?.panelMode,
|
||||
signupMethod: typeof getSelectedSignupMethod === 'function' ? getSelectedSignupMethod() : latestState?.signupMethod,
|
||||
phoneVerificationEnabled: typeof inputPhoneVerificationEnabled !== 'undefined' && inputPhoneVerificationEnabled
|
||||
? Boolean(inputPhoneVerificationEnabled.checked)
|
||||
: Boolean(latestState?.phoneVerificationEnabled),
|
||||
plusModeEnabled: typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
|
||||
? Boolean(inputPlusModeEnabled.checked)
|
||||
: Boolean(latestState?.plusModeEnabled),
|
||||
contributionMode: Boolean(latestState?.contributionMode),
|
||||
};
|
||||
return registry.validateAutoRunStart({
|
||||
activeFlowId: validationState.activeFlowId,
|
||||
panelMode: validationState.panelMode,
|
||||
signupMethod: validationState.signupMethod,
|
||||
state: validationState,
|
||||
});
|
||||
})();
|
||||
if (autoRunStartValidation?.ok === false) {
|
||||
clearPendingAutoRunStartRunCount();
|
||||
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
|
||||
}
|
||||
if (!(await ensureGpcApiKeyReadyForStart())) {
|
||||
clearPendingAutoRunStartRunCount();
|
||||
return false;
|
||||
@@ -11799,7 +12186,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 +12213,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 +12290,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 +12441,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 +14277,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 });
|
||||
|
||||
@@ -145,3 +145,109 @@ return {
|
||||
assert.equal(snapshot.autoRunTotalRuns, 1);
|
||||
assert.equal(snapshot.autoRunAttemptRun, 0);
|
||||
});
|
||||
|
||||
test('launchAutoRunTimerPlan cancels an invalid scheduled start before restarting auto-run', async () => {
|
||||
const api = new Function(`
|
||||
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
||||
const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
|
||||
const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
|
||||
const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
|
||||
|
||||
let autoRunTimerLaunching = false;
|
||||
let autoRunActive = false;
|
||||
let autoRunCurrentRun = 0;
|
||||
let autoRunTotalRuns = 1;
|
||||
let autoRunAttemptRun = 0;
|
||||
let autoRunSessionId = 0;
|
||||
|
||||
const state = {
|
||||
activeFlowId: 'site-a',
|
||||
panelMode: 'cpa',
|
||||
signupMethod: 'phone',
|
||||
autoRunDelayEnabled: false,
|
||||
autoRunTimerPlan: {
|
||||
kind: AUTO_RUN_TIMER_KIND_SCHEDULED_START,
|
||||
fireAt: Date.now() + 60_000,
|
||||
totalRuns: 2,
|
||||
autoRunSkipFailures: false,
|
||||
autoRunSessionId: 0,
|
||||
countdownTitle: '已计划自动运行',
|
||||
countdownNote: '等待启动',
|
||||
},
|
||||
};
|
||||
|
||||
let startCalls = 0;
|
||||
let clearStopCalls = 0;
|
||||
let clearAlarmCalls = 0;
|
||||
const broadcasts = [];
|
||||
const logs = [];
|
||||
|
||||
async function getState() {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
function getPendingAutoRunTimerPlan() {
|
||||
return state.autoRunTimerPlan;
|
||||
}
|
||||
|
||||
async function clearAutoRunTimerAlarm() {
|
||||
clearAlarmCalls += 1;
|
||||
}
|
||||
|
||||
async function broadcastAutoRunStatus(phase, statusPayload, statePayload) {
|
||||
broadcasts.push({ phase, statusPayload, statePayload });
|
||||
}
|
||||
async function addLog(message, level) {
|
||||
logs.push({ message, level });
|
||||
}
|
||||
async function setAutoRunDelayEnabledState() {}
|
||||
function serializeAutoRunRoundSummaries(totalRuns, summaries = []) {
|
||||
return Array.isArray(summaries) ? summaries : [];
|
||||
}
|
||||
function clearStopRequest() {
|
||||
clearStopCalls += 1;
|
||||
}
|
||||
function startAutoRunLoop() {
|
||||
startCalls += 1;
|
||||
}
|
||||
function validateAutoRunStartState() {
|
||||
return {
|
||||
ok: false,
|
||||
errors: [{ message: '当前 flow 不支持手机号注册。' }],
|
||||
};
|
||||
}
|
||||
|
||||
${helperBundle}
|
||||
|
||||
return {
|
||||
launchAutoRunTimerPlan,
|
||||
snapshot() {
|
||||
return {
|
||||
startCalls,
|
||||
clearStopCalls,
|
||||
clearAlarmCalls,
|
||||
broadcasts,
|
||||
logs,
|
||||
autoRunCurrentRun,
|
||||
autoRunTotalRuns,
|
||||
autoRunAttemptRun,
|
||||
};
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const started = await api.launchAutoRunTimerPlan('alarm');
|
||||
const snapshot = api.snapshot();
|
||||
|
||||
assert.equal(started, false);
|
||||
assert.equal(snapshot.startCalls, 0);
|
||||
assert.equal(snapshot.clearStopCalls, 0);
|
||||
assert.equal(snapshot.clearAlarmCalls, 1);
|
||||
assert.equal(snapshot.broadcasts.length, 1);
|
||||
assert.equal(snapshot.broadcasts[0].phase, 'idle');
|
||||
assert.match(snapshot.logs[0].message, /自动运行计划已取消:当前 flow 不支持手机号注册。/);
|
||||
assert.equal(snapshot.logs[0].level, 'error');
|
||||
assert.equal(snapshot.autoRunCurrentRun, 0);
|
||||
assert.equal(snapshot.autoRunTotalRuns, 1);
|
||||
assert.equal(snapshot.autoRunAttemptRun, 0);
|
||||
});
|
||||
|
||||
@@ -73,6 +73,8 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
};
|
||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; }
|
||||
function normalizeSignupMethod(value = '') { return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'; }
|
||||
function resolveSignupMethod(state = {}) { return normalizeSignupMethod(state?.signupMethod); }
|
||||
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
|
||||
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
|
||||
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
|
||||
@@ -163,6 +165,8 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
};
|
||||
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
||||
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; }
|
||||
function normalizeSignupMethod(value = '') { return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'; }
|
||||
function resolveSignupMethod(state = {}) { return normalizeSignupMethod(state?.signupMethod); }
|
||||
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
|
||||
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
|
||||
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
|
||||
|
||||
@@ -351,6 +351,65 @@ test('message router re-syncs contribution mode before AUTO_RUN when sidepanel p
|
||||
]);
|
||||
});
|
||||
|
||||
test('message router blocks AUTO_RUN and SCHEDULE_AUTO_RUN when shared auto-run validation fails', async () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
|
||||
const calls = [];
|
||||
const router = api.createMessageRouter({
|
||||
clearStopRequest: () => {},
|
||||
getPendingAutoRunTimerPlan: () => null,
|
||||
getState: async () => ({
|
||||
activeFlowId: 'site-a',
|
||||
panelMode: 'cpa',
|
||||
signupMethod: 'phone',
|
||||
stepStatuses: {},
|
||||
}),
|
||||
normalizeRunCount: (value) => Number(value) || 1,
|
||||
scheduleAutoRun: async () => {
|
||||
calls.push({ type: 'scheduleAutoRun' });
|
||||
return { ok: true };
|
||||
},
|
||||
setState: async (updates) => {
|
||||
calls.push({ type: 'setState', updates });
|
||||
},
|
||||
startAutoRunLoop: () => {
|
||||
calls.push({ type: 'startAutoRunLoop' });
|
||||
},
|
||||
validateAutoRunStart: () => ({
|
||||
ok: false,
|
||||
errors: [{ message: '当前 flow 不支持手机号注册。' }],
|
||||
}),
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => router.handleMessage({
|
||||
type: 'AUTO_RUN',
|
||||
payload: {
|
||||
totalRuns: 2,
|
||||
autoRunSkipFailures: true,
|
||||
mode: 'restart',
|
||||
},
|
||||
}),
|
||||
/当前 flow 不支持手机号注册。/
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() => router.handleMessage({
|
||||
type: 'SCHEDULE_AUTO_RUN',
|
||||
payload: {
|
||||
totalRuns: 2,
|
||||
delayMinutes: 5,
|
||||
autoRunSkipFailures: false,
|
||||
},
|
||||
}),
|
||||
/当前 flow 不支持手机号注册。/
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(calls, []);
|
||||
});
|
||||
|
||||
test('account run history snapshot sync is disabled in contribution mode', () => {
|
||||
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background forwards mail rule metadata to Hotmail helper and shared picker paths', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/requiredKeywords:\s*pollPayload\.requiredKeywords\s*\|\|\s*\[\]/,
|
||||
'Hotmail helper 请求体应转发 requiredKeywords'
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/codePatterns:\s*pollPayload\.codePatterns\s*\|\|\s*\[\]/,
|
||||
'Hotmail helper 请求体应转发 codePatterns'
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/pickVerificationMessageWithTimeFallback\(fetchResult\.messages,\s*\{[\s\S]*requiredKeywords:\s*pollPayload\.requiredKeywords\s*\|\|\s*\[\],[\s\S]*codePatterns:\s*pollPayload\.codePatterns\s*\|\|\s*\[\]/,
|
||||
'Hotmail API 轮询应把 rule metadata 传给共享验证码筛选器'
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/pickVerificationMessageWithTimeFallback\(messages,\s*\{[\s\S]*requiredKeywords:\s*pollPayload\.requiredKeywords\s*\|\|\s*\[\],[\s\S]*codePatterns:\s*pollPayload\.codePatterns\s*\|\|\s*\[\]/,
|
||||
'Cloudflare Temp Email 轮询也应复用同一套 rule metadata'
|
||||
);
|
||||
});
|
||||
@@ -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,125 @@
|
||||
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',
|
||||
codePatterns: [
|
||||
{
|
||||
source: '(?:chatgpt\\s+log-?in\\s+code|enter\\s+this\\s+code)[^0-9]{0,24}(\\d{6})',
|
||||
flags: 'i',
|
||||
},
|
||||
{
|
||||
source: 'your\\s+chatgpt\\s+code\\s+is\\s+(\\d{6})',
|
||||
flags: 'i',
|
||||
},
|
||||
{
|
||||
source: '(?:verification\\s+code|temporary\\s+verification\\s+code|your\\s+chatgpt\\s+code|code(?:\\s+is)?)[^0-9]{0,16}(\\d{6})',
|
||||
flags: 'i',
|
||||
},
|
||||
],
|
||||
filterAfterTimestamp: 0,
|
||||
requiredKeywords: ['openai', 'chatgpt', 'verify', 'verification', 'confirm', '验证码', '代码'],
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
|
||||
targetEmail: 'user@example.com',
|
||||
targetEmailHints: ['user@example.com', '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',
|
||||
codePatterns: [
|
||||
{
|
||||
source: '(?:chatgpt\\s+log-?in\\s+code|enter\\s+this\\s+code)[^0-9]{0,24}(\\d{6})',
|
||||
flags: 'i',
|
||||
},
|
||||
{
|
||||
source: 'your\\s+chatgpt\\s+code\\s+is\\s+(\\d{6})',
|
||||
flags: 'i',
|
||||
},
|
||||
{
|
||||
source: '(?:verification\\s+code|temporary\\s+verification\\s+code|your\\s+chatgpt\\s+code|code(?:\\s+is)?)[^0-9]{0,16}(\\d{6})',
|
||||
flags: 'i',
|
||||
},
|
||||
],
|
||||
filterAfterTimestamp: 456,
|
||||
requiredKeywords: ['openai', 'chatgpt', 'verify', 'verification', 'confirm', '验证码', '代码', 'login'],
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
||||
targetEmail: 'login@example.com',
|
||||
targetEmailHints: ['login@example.com', '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,103 @@ 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');
|
||||
});
|
||||
|
||||
test('SAVE_SETTING applies shared mode-switch normalization before persisting incompatible capability flags', async () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = { console };
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
const persistedPayloads = [];
|
||||
let state = {
|
||||
activeFlowId: 'site-a',
|
||||
signupMethod: 'email',
|
||||
phoneVerificationEnabled: false,
|
||||
plusModeEnabled: false,
|
||||
panelMode: 'cpa',
|
||||
};
|
||||
|
||||
const router = api.createMessageRouter({
|
||||
addLog: async () => {},
|
||||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||||
buildPersistentSettingsPayload: (input = {}) => ({
|
||||
plusModeEnabled: Boolean(input.plusModeEnabled),
|
||||
phoneVerificationEnabled: Boolean(input.phoneVerificationEnabled),
|
||||
signupMethod: String(input.signupMethod || 'email'),
|
||||
}),
|
||||
broadcastDataUpdate: () => {},
|
||||
getState: async () => ({ ...state }),
|
||||
resolveSignupMethod: (nextState = {}) => (
|
||||
Boolean(nextState.phoneVerificationEnabled) && Boolean(nextState.plusModeEnabled) ? 'phone' : 'email'
|
||||
),
|
||||
setPersistentSettings: async (updates) => {
|
||||
persistedPayloads.push({ ...updates });
|
||||
},
|
||||
setState: async (updates) => {
|
||||
state = { ...state, ...updates };
|
||||
},
|
||||
validateModeSwitch: () => ({
|
||||
ok: false,
|
||||
errors: [{ code: 'plus_mode_unsupported', message: '当前 flow 不支持 Plus 模式。' }],
|
||||
normalizedUpdates: {
|
||||
plusModeEnabled: false,
|
||||
phoneVerificationEnabled: false,
|
||||
signupMethod: 'email',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'SAVE_SETTING',
|
||||
payload: {
|
||||
plusModeEnabled: true,
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(response.ok, true);
|
||||
assert.equal(state.plusModeEnabled, false);
|
||||
assert.equal(state.phoneVerificationEnabled, false);
|
||||
assert.equal(state.signupMethod, 'email');
|
||||
assert.deepEqual(persistedPayloads[0], {
|
||||
plusModeEnabled: false,
|
||||
phoneVerificationEnabled: false,
|
||||
signupMethod: 'email',
|
||||
});
|
||||
assert.equal(response.modeValidation?.errors?.[0]?.code, 'plus_mode_unsupported');
|
||||
});
|
||||
|
||||
@@ -251,6 +251,11 @@ test('message router persists phone signup identity from step 7 completion paylo
|
||||
};
|
||||
const { router, events } = createRouter({
|
||||
state: { stepStatuses: { 7: 'completed', 8: 'pending' } },
|
||||
getStepDefinitionForState: (step) => (
|
||||
step === 7
|
||||
? { key: 'oauth-login' }
|
||||
: (step === 8 ? { key: 'fetch-login-code' } : null)
|
||||
),
|
||||
});
|
||||
|
||||
await router.handleStepData(7, {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -16,10 +16,24 @@ test('panel bridge module exposes a factory', () => {
|
||||
assert.equal(typeof api?.createPanelBridge, 'function');
|
||||
});
|
||||
|
||||
test('panel bridge requests oauth url with step 7 log label payload', () => {
|
||||
function loadPanelBridgeWithSub2Api() {
|
||||
const sub2apiSource = fs.readFileSync('background/sub2api-api.js', 'utf8');
|
||||
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
|
||||
assert.match(source, /logStep:\s*7/);
|
||||
assert.doesNotMatch(source, /logStep:\s*6/);
|
||||
return new Function('self', `${sub2apiSource}\n${source}; return self.MultiPageBackgroundPanelBridge;`)({});
|
||||
}
|
||||
|
||||
function createSub2ApiResponse(payload, status = 200) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
text: async () => JSON.stringify(payload),
|
||||
};
|
||||
}
|
||||
|
||||
test('panel bridge requests SUB2API oauth url via direct API', () => {
|
||||
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
|
||||
assert.match(source, /generateOpenAiAuthUrl/);
|
||||
assert.doesNotMatch(source, /REQUEST_OAUTH_URL/);
|
||||
});
|
||||
|
||||
test('panel bridge can request codex2api oauth url via protocol', async () => {
|
||||
@@ -117,16 +131,63 @@ test('panel bridge can request cpa oauth url via management api', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('panel bridge forwards SUB2API account priority when requesting oauth url', async () => {
|
||||
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
|
||||
const sentMessages = [];
|
||||
test('panel bridge can request SUB2API oauth url without opening the admin page', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const fetchCalls = [];
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
const parsed = new URL(url);
|
||||
const body = options.body ? JSON.parse(options.body) : null;
|
||||
fetchCalls.push({ path: parsed.pathname, search: parsed.search, method: options.method || 'GET', body });
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
|
||||
if (parsed.pathname === '/api/v1/auth/login') {
|
||||
return createSub2ApiResponse({
|
||||
code: 0,
|
||||
data: { access_token: 'admin-token' },
|
||||
});
|
||||
}
|
||||
if (parsed.pathname === '/api/v1/admin/groups/all') {
|
||||
return createSub2ApiResponse({
|
||||
code: 0,
|
||||
data: [{ id: 5, name: 'codex', platform: 'openai' }],
|
||||
});
|
||||
}
|
||||
if (parsed.pathname === '/api/v1/admin/proxies/all') {
|
||||
return createSub2ApiResponse({
|
||||
code: 0,
|
||||
data: [{
|
||||
id: 7,
|
||||
name: 'shadowrocket',
|
||||
protocol: 'socks5',
|
||||
host: '127.0.0.1',
|
||||
port: 1080,
|
||||
status: 'active',
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (parsed.pathname === '/api/v1/admin/openai/generate-auth-url') {
|
||||
return createSub2ApiResponse({
|
||||
code: 0,
|
||||
data: {
|
||||
auth_url: 'https://auth.openai.com/authorize?state=oauth-state',
|
||||
session_id: 'session-123',
|
||||
state: 'oauth-state',
|
||||
},
|
||||
});
|
||||
}
|
||||
return createSub2ApiResponse({ code: 1, message: `unexpected path ${parsed.pathname}` }, 404);
|
||||
};
|
||||
|
||||
const tabCreates = [];
|
||||
const sentMessages = [];
|
||||
const api = loadPanelBridgeWithSub2Api();
|
||||
const bridge = api.createPanelBridge({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
create: async () => ({ id: 72 }),
|
||||
create: async (payload) => {
|
||||
tabCreates.push(payload);
|
||||
return { id: 72 };
|
||||
},
|
||||
},
|
||||
},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
@@ -137,11 +198,7 @@ test('panel bridge forwards SUB2API account priority when requesting oauth url',
|
||||
rememberSourceLastUrl: async () => {},
|
||||
sendToContentScript: async (sourceName, message, options) => {
|
||||
sentMessages.push({ sourceName, message, options });
|
||||
return {
|
||||
oauthUrl: 'https://auth.openai.com/authorize?state=oauth-state',
|
||||
sub2apiSessionId: 'session-123',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
};
|
||||
return {};
|
||||
},
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
waitForTabUrlFamily: async () => ({ id: 72 }),
|
||||
@@ -149,16 +206,30 @@ test('panel bridge forwards SUB2API account priority when requesting oauth url',
|
||||
SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
|
||||
});
|
||||
|
||||
await bridge.requestOAuthUrlFromPanel({
|
||||
panelMode: 'sub2api',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiGroupName: 'codex',
|
||||
sub2apiAccountPriority: 3,
|
||||
}, { logLabel: '步骤 7' });
|
||||
try {
|
||||
const result = await bridge.requestOAuthUrlFromPanel({
|
||||
panelMode: 'sub2api',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiGroupName: 'codex',
|
||||
sub2apiDefaultProxyName: 'shadowrocket',
|
||||
}, { logLabel: '步骤 7' });
|
||||
|
||||
assert.equal(sentMessages.length, 1);
|
||||
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
|
||||
assert.equal(sentMessages[0].message.payload.sub2apiAccountPriority, 3);
|
||||
const generateCall = fetchCalls.find((call) => call.path === '/api/v1/admin/openai/generate-auth-url');
|
||||
assert.equal(tabCreates.length, 0);
|
||||
assert.equal(sentMessages.length, 0);
|
||||
assert.equal(generateCall.body.proxy_id, 7);
|
||||
assert.deepStrictEqual(result, {
|
||||
oauthUrl: 'https://auth.openai.com/authorize?state=oauth-state',
|
||||
sub2apiSessionId: 'session-123',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
sub2apiGroupId: 5,
|
||||
sub2apiGroupIds: [5],
|
||||
sub2apiDraftName: result.sub2apiDraftName,
|
||||
sub2apiProxyId: 7,
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -80,72 +80,14 @@ test('platform verify module supports codex2api protocol callback exchange', asy
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify retries transient SUB2API oauth/token exchange errors before failing', async () => {
|
||||
test('platform verify retries transient SUB2API oauth/token exchange errors before succeeding', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
|
||||
const logs = [];
|
||||
const attempts = [];
|
||||
let callCount = 0;
|
||||
const executor = api.createStep10Executor({
|
||||
addLog: async (message, level = 'info') => {
|
||||
logs.push({ message, level });
|
||||
},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getPanelMode: () => 'sub2api',
|
||||
getTabId: async () => 12,
|
||||
isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='),
|
||||
isTabAlive: async () => true,
|
||||
normalizeCodex2ApiUrl: (value) => value,
|
||||
normalizeSub2ApiUrl: () => 'https://sub2api.example.com/admin/accounts',
|
||||
rememberSourceLastUrl: async () => {},
|
||||
reuseOrCreateTab: async () => 12,
|
||||
sendToContentScript: async (_source, message) => {
|
||||
attempts.push(message.type);
|
||||
callCount += 1;
|
||||
if (callCount === 1) {
|
||||
return {
|
||||
error: 'request failed: Post "https://auth.openai.com/oauth/token": unexpected EOF',
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
},
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
shouldBypassStep9ForLocalCpa: () => false,
|
||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
|
||||
});
|
||||
|
||||
await executor.executeStep10({
|
||||
panelMode: 'sub2api',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
sub2apiUrl: 'https://sub2api.example.com/admin/accounts',
|
||||
sub2apiEmail: 'flow@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiSessionId: 'session-123',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
});
|
||||
|
||||
assert.equal(callCount, 2);
|
||||
assert.deepStrictEqual(attempts, ['EXECUTE_STEP', 'EXECUTE_STEP']);
|
||||
assert.equal(
|
||||
logs.some((entry) => /临时网络波动/.test(entry.message) && entry.level === 'warn'),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('platform verify retries transient SUB2API token_exchange_user_error before succeeding', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
|
||||
const logs = [];
|
||||
let callCount = 0;
|
||||
let contentScriptCalled = false;
|
||||
const executor = api.createStep10Executor({
|
||||
addLog: async (message, level = 'info') => {
|
||||
logs.push({ message, level });
|
||||
@@ -167,14 +109,22 @@ test('platform verify retries transient SUB2API token_exchange_user_error before
|
||||
rememberSourceLastUrl: async () => {},
|
||||
reuseOrCreateTab: async () => 12,
|
||||
sendToContentScript: async () => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) {
|
||||
return {
|
||||
error: 'token exchange failed: status 400, body: { "error": { "message": "Invalid request. Please try again later.", "type": "invalid_request_error", "param": null, "code": "token_exchange_user_error" } }',
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
contentScriptCalled = true;
|
||||
return {};
|
||||
},
|
||||
createSub2ApiApi: () => ({
|
||||
submitOpenAiCallback: async () => {
|
||||
attempts.push('direct-api');
|
||||
callCount += 1;
|
||||
if (callCount === 1) {
|
||||
throw new Error('request failed: Post "https://auth.openai.com/oauth/token": unexpected EOF');
|
||||
}
|
||||
return {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
verifiedStatus: 'SUB2API 已创建账号 #11',
|
||||
};
|
||||
},
|
||||
}),
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
shouldBypassStep9ForLocalCpa: () => false,
|
||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
|
||||
@@ -191,6 +141,74 @@ test('platform verify retries transient SUB2API token_exchange_user_error before
|
||||
});
|
||||
|
||||
assert.equal(callCount, 2);
|
||||
assert.equal(contentScriptCalled, false);
|
||||
assert.deepStrictEqual(attempts, ['direct-api', 'direct-api']);
|
||||
assert.equal(
|
||||
logs.some((entry) => /临时网络波动/.test(entry.message) && entry.level === 'warn'),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('platform verify retries transient SUB2API token_exchange_user_error before succeeding', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
|
||||
const logs = [];
|
||||
let callCount = 0;
|
||||
let contentScriptCalled = false;
|
||||
const executor = api.createStep10Executor({
|
||||
addLog: async (message, level = 'info') => {
|
||||
logs.push({ message, level });
|
||||
},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getPanelMode: () => 'sub2api',
|
||||
getTabId: async () => 12,
|
||||
isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='),
|
||||
isTabAlive: async () => true,
|
||||
normalizeCodex2ApiUrl: (value) => value,
|
||||
normalizeSub2ApiUrl: () => 'https://sub2api.example.com/admin/accounts',
|
||||
rememberSourceLastUrl: async () => {},
|
||||
reuseOrCreateTab: async () => 12,
|
||||
sendToContentScript: async () => {
|
||||
contentScriptCalled = true;
|
||||
return { ok: true };
|
||||
},
|
||||
createSub2ApiApi: () => ({
|
||||
submitOpenAiCallback: async () => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) {
|
||||
throw new Error('token exchange failed: status 400, body: { "error": { "message": "Invalid request. Please try again later.", "type": "invalid_request_error", "param": null, "code": "token_exchange_user_error" } }');
|
||||
}
|
||||
return {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
verifiedStatus: 'SUB2API 已创建账号 #11',
|
||||
};
|
||||
},
|
||||
}),
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
shouldBypassStep9ForLocalCpa: () => false,
|
||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
|
||||
});
|
||||
|
||||
await executor.executeStep10({
|
||||
panelMode: 'sub2api',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
sub2apiUrl: 'https://sub2api.example.com/admin/accounts',
|
||||
sub2apiEmail: 'flow@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiSessionId: 'session-123',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
});
|
||||
|
||||
assert.equal(callCount, 2);
|
||||
assert.equal(contentScriptCalled, false);
|
||||
assert.equal(
|
||||
logs.some((entry) => /临时网络波动/.test(entry.message) && entry.level === 'warn'),
|
||||
true
|
||||
|
||||
@@ -42,6 +42,20 @@ function createDeps(overrides = {}) {
|
||||
return { deps, logs, completed, uiCalls };
|
||||
}
|
||||
|
||||
function loadStep10WithSub2Api() {
|
||||
const sub2apiSource = fs.readFileSync('background/sub2api-api.js', 'utf8');
|
||||
const step10Source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
return new Function('self', `${sub2apiSource}\n${step10Source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
}
|
||||
|
||||
function createSub2ApiResponse(payload, status = 200) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
text: async () => JSON.stringify(payload),
|
||||
};
|
||||
}
|
||||
|
||||
test('platform verify module submits CPA callback via management API first', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
@@ -240,11 +254,109 @@ test('platform verify module rejects callback when cpa oauth state mismatches',
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify module sends Plus visible step 13 to SUB2API panel', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
test('platform verify module submits Plus visible step 13 to SUB2API via direct API', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const fetchCalls = [];
|
||||
const sentMessages = [];
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
const parsed = new URL(url);
|
||||
const body = options.body ? JSON.parse(options.body) : null;
|
||||
fetchCalls.push({ path: parsed.pathname, method: options.method || 'GET', body });
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
if (parsed.pathname === '/api/v1/auth/login') {
|
||||
return createSub2ApiResponse({ code: 0, data: { access_token: 'admin-token' } });
|
||||
}
|
||||
if (parsed.pathname === '/api/v1/admin/groups/all') {
|
||||
return createSub2ApiResponse({ code: 0, data: [{ id: 5, name: 'codex', platform: 'openai' }] });
|
||||
}
|
||||
if (parsed.pathname === '/api/v1/admin/openai/exchange-code') {
|
||||
return createSub2ApiResponse({
|
||||
code: 0,
|
||||
data: {
|
||||
access_token: 'openai-access',
|
||||
refresh_token: 'openai-refresh',
|
||||
email: 'flow@example.com',
|
||||
},
|
||||
});
|
||||
}
|
||||
if (parsed.pathname === '/api/v1/admin/accounts') {
|
||||
return createSub2ApiResponse({ code: 0, data: { id: 11 } });
|
||||
}
|
||||
return createSub2ApiResponse({ code: 1, message: `unexpected path ${parsed.pathname}` }, 404);
|
||||
};
|
||||
|
||||
const api = loadStep10WithSub2Api();
|
||||
const { deps, completed } = createDeps({
|
||||
getPanelMode: () => 'sub2api',
|
||||
getTabId: async () => 91,
|
||||
isTabAlive: async () => true,
|
||||
sendToContentScript: async (sourceName, message, options) => {
|
||||
sentMessages.push({ sourceName, message, options });
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
try {
|
||||
await executor.executeStep10({
|
||||
panelMode: 'sub2api',
|
||||
visibleStep: 13,
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiGroupName: 'codex',
|
||||
sub2apiSessionId: 'session-1',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
});
|
||||
|
||||
const exchangeCall = fetchCalls.find((call) => call.path === '/api/v1/admin/openai/exchange-code');
|
||||
const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts');
|
||||
assert.equal(sentMessages.length, 0);
|
||||
assert.equal(exchangeCall.body.code, 'callback-code');
|
||||
assert.equal(exchangeCall.body.state, 'oauth-state');
|
||||
assert.deepStrictEqual(createCall.body.group_ids, [5]);
|
||||
assert.deepStrictEqual(completed, [{
|
||||
step: 13,
|
||||
payload: {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
verifiedStatus: 'SUB2API 已创建账号 #11',
|
||||
},
|
||||
}]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify module forwards SUB2API account priority to direct create API', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const fetchCalls = [];
|
||||
const sentMessages = [];
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
const parsed = new URL(url);
|
||||
const body = options.body ? JSON.parse(options.body) : null;
|
||||
fetchCalls.push({ path: parsed.pathname, method: options.method || 'GET', body });
|
||||
|
||||
if (parsed.pathname === '/api/v1/auth/login') {
|
||||
return createSub2ApiResponse({ code: 0, data: { access_token: 'admin-token' } });
|
||||
}
|
||||
if (parsed.pathname === '/api/v1/admin/openai/exchange-code') {
|
||||
return createSub2ApiResponse({
|
||||
code: 0,
|
||||
data: {
|
||||
access_token: 'openai-access',
|
||||
refresh_token: 'openai-refresh',
|
||||
email: 'flow@example.com',
|
||||
},
|
||||
});
|
||||
}
|
||||
if (parsed.pathname === '/api/v1/admin/accounts') {
|
||||
return createSub2ApiResponse({ code: 0, data: { id: 11 } });
|
||||
}
|
||||
return createSub2ApiResponse({ code: 1, message: `unexpected path ${parsed.pathname}` }, 404);
|
||||
};
|
||||
|
||||
const api = loadStep10WithSub2Api();
|
||||
const { deps } = createDeps({
|
||||
getPanelMode: () => 'sub2api',
|
||||
getTabId: async () => 91,
|
||||
@@ -256,50 +368,23 @@ test('platform verify module sends Plus visible step 13 to SUB2API panel', async
|
||||
});
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await executor.executeStep10({
|
||||
panelMode: 'sub2api',
|
||||
visibleStep: 13,
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiSessionId: 'session-1',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
});
|
||||
try {
|
||||
await executor.executeStep10({
|
||||
panelMode: 'sub2api',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiSessionId: 'session-1',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
sub2apiGroupId: 5,
|
||||
sub2apiAccountPriority: 2,
|
||||
});
|
||||
|
||||
assert.equal(sentMessages.length, 1);
|
||||
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
|
||||
assert.equal(sentMessages[0].message.step, 13);
|
||||
});
|
||||
|
||||
test('platform verify module forwards SUB2API account priority to panel', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const sentMessages = [];
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps } = createDeps({
|
||||
getPanelMode: () => 'sub2api',
|
||||
getTabId: async () => 91,
|
||||
isTabAlive: async () => true,
|
||||
sendToContentScript: async (sourceName, message, options) => {
|
||||
sentMessages.push({ sourceName, message, options });
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await executor.executeStep10({
|
||||
panelMode: 'sub2api',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiSessionId: 'session-1',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
sub2apiAccountPriority: 2,
|
||||
});
|
||||
|
||||
assert.equal(sentMessages.length, 1);
|
||||
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
|
||||
assert.equal(sentMessages[0].message.payload.sub2apiAccountPriority, 2);
|
||||
const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts');
|
||||
assert.equal(sentMessages.length, 0);
|
||||
assert.equal(createCall.body.priority, 2);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') parenDepth += 1;
|
||||
if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) signatureEnded = true;
|
||||
}
|
||||
if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('importSettingsBundle normalizes unsupported capability flags before persisting imported settings', async () => {
|
||||
const api = new Function(`
|
||||
const SETTINGS_EXPORT_SCHEMA_VERSION = 1;
|
||||
const DEFAULT_REGISTRATION_EMAIL_STATE = { emailHistory: [] };
|
||||
let persistedUpdates = null;
|
||||
let stateUpdates = null;
|
||||
let broadcastPayload = null;
|
||||
let currentState = {
|
||||
activeFlowId: 'site-a',
|
||||
panelMode: 'sub2api',
|
||||
signupMethod: 'phone',
|
||||
plusModeEnabled: false,
|
||||
phoneVerificationEnabled: false,
|
||||
stepStatuses: {},
|
||||
};
|
||||
async function ensureManualInteractionAllowed() {
|
||||
return currentState;
|
||||
}
|
||||
function buildPersistentSettingsPayload(settings = {}) {
|
||||
return { ...settings };
|
||||
}
|
||||
function validateModeSwitchState() {
|
||||
return {
|
||||
ok: false,
|
||||
errors: [{ code: 'panel_mode_unsupported', message: '当前 flow 不支持 SUB2API 面板模式。' }],
|
||||
normalizedUpdates: {
|
||||
panelMode: 'cpa',
|
||||
plusModeEnabled: false,
|
||||
phoneVerificationEnabled: false,
|
||||
signupMethod: 'email',
|
||||
},
|
||||
};
|
||||
}
|
||||
function resolveSignupMethod(state = {}) {
|
||||
return String(state?.signupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
|
||||
}
|
||||
async function setPersistentSettings(updates) {
|
||||
persistedUpdates = { ...updates };
|
||||
}
|
||||
async function setState(updates) {
|
||||
stateUpdates = { ...updates };
|
||||
currentState = { ...currentState, ...updates };
|
||||
}
|
||||
function broadcastDataUpdate(payload) {
|
||||
broadcastPayload = { ...payload };
|
||||
}
|
||||
async function getState() {
|
||||
return { ...currentState };
|
||||
}
|
||||
${extractFunction('importSettingsBundle')}
|
||||
return {
|
||||
importSettingsBundle,
|
||||
getPersistedUpdates: () => persistedUpdates,
|
||||
getStateUpdates: () => stateUpdates,
|
||||
getBroadcastPayload: () => broadcastPayload,
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.importSettingsBundle({
|
||||
schemaVersion: 1,
|
||||
settings: {
|
||||
panelMode: 'sub2api',
|
||||
plusModeEnabled: true,
|
||||
phoneVerificationEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(api.getPersistedUpdates(), {
|
||||
panelMode: 'cpa',
|
||||
plusModeEnabled: false,
|
||||
phoneVerificationEnabled: false,
|
||||
signupMethod: 'email',
|
||||
});
|
||||
assert.equal(api.getStateUpdates().panelMode, 'cpa');
|
||||
assert.equal(api.getStateUpdates().plusModeEnabled, false);
|
||||
assert.equal(api.getStateUpdates().phoneVerificationEnabled, false);
|
||||
assert.equal(api.getStateUpdates().signupMethod, 'email');
|
||||
assert.equal(api.getBroadcastPayload().panelMode, 'cpa');
|
||||
assert.equal(api.getBroadcastPayload().signupMethod, 'email');
|
||||
assert.equal(result.signupMethod, 'email');
|
||||
});
|
||||
@@ -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 = [];
|
||||
@@ -132,6 +177,7 @@ return {
|
||||
});
|
||||
|
||||
assert.deepEqual(api.getCaptured(), [{
|
||||
activeFlowId: 'openai',
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'gopay',
|
||||
signupMethod: 'phone',
|
||||
|
||||
@@ -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,160 @@
|
||||
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, []);
|
||||
});
|
||||
|
||||
test('flow capability registry exposes shared auto-run validation for phone locks and panel support', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry({
|
||||
flowCapabilities: {
|
||||
openai: api.FLOW_CAPABILITIES.openai,
|
||||
'site-a': {
|
||||
...api.DEFAULT_FLOW_CAPABILITIES,
|
||||
supportsPlatformBinding: ['cpa'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const plusLockedResult = registry.validateAutoRunStart({
|
||||
state: {
|
||||
activeFlowId: 'openai',
|
||||
panelMode: 'cpa',
|
||||
signupMethod: 'phone',
|
||||
phoneVerificationEnabled: true,
|
||||
plusModeEnabled: true,
|
||||
contributionMode: false,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(plusLockedResult.ok, false);
|
||||
assert.equal(plusLockedResult.errors[0].code, 'phone_signup_plus_mode_locked');
|
||||
|
||||
const unsupportedPanelResult = registry.validateAutoRunStart({
|
||||
state: {
|
||||
activeFlowId: 'site-a',
|
||||
panelMode: 'sub2api',
|
||||
signupMethod: 'email',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(unsupportedPanelResult.ok, false);
|
||||
assert.equal(unsupportedPanelResult.errors[0].code, 'panel_mode_unsupported');
|
||||
});
|
||||
|
||||
test('flow capability registry normalizes unsupported mode switches back to the effective capability set', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry({
|
||||
flowCapabilities: {
|
||||
openai: api.FLOW_CAPABILITIES.openai,
|
||||
'site-a': {
|
||||
...api.DEFAULT_FLOW_CAPABILITIES,
|
||||
supportsPlatformBinding: ['cpa'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const validation = registry.validateModeSwitch({
|
||||
state: {
|
||||
activeFlowId: 'site-a',
|
||||
panelMode: 'sub2api',
|
||||
signupMethod: 'phone',
|
||||
phoneVerificationEnabled: true,
|
||||
plusModeEnabled: true,
|
||||
contributionMode: true,
|
||||
},
|
||||
changedKeys: [
|
||||
'panelMode',
|
||||
'signupMethod',
|
||||
'phoneVerificationEnabled',
|
||||
'plusModeEnabled',
|
||||
'contributionMode',
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(validation.ok, false);
|
||||
assert.deepEqual(validation.normalizedUpdates, {
|
||||
panelMode: 'cpa',
|
||||
signupMethod: 'email',
|
||||
phoneVerificationEnabled: false,
|
||||
plusModeEnabled: false,
|
||||
contributionMode: false,
|
||||
});
|
||||
assert.deepEqual(
|
||||
validation.errors.map((entry) => entry.code),
|
||||
[
|
||||
'panel_mode_unsupported',
|
||||
'plus_mode_unsupported',
|
||||
'contribution_mode_unsupported',
|
||||
'phone_verification_unsupported',
|
||||
'phone_signup_flow_unsupported',
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -185,6 +185,15 @@ test('extractVerificationCode returns first six-digit code from multilingual mai
|
||||
assert.equal(extractVerificationCode('No code here'), null);
|
||||
});
|
||||
|
||||
test('extractVerificationCode supports runtime mail rule patterns', () => {
|
||||
assert.equal(
|
||||
extractVerificationCode('Mailbox notice: use pin A-778899 to continue.', {
|
||||
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
|
||||
}),
|
||||
'778899'
|
||||
);
|
||||
});
|
||||
|
||||
test('extractVerificationCodeFromMessage reads code from the latest message subject or preview', () => {
|
||||
assert.equal(
|
||||
extractVerificationCodeFromMessage({
|
||||
@@ -214,6 +223,30 @@ test('extractVerificationCodeFromMessage reads code from the latest message subj
|
||||
);
|
||||
});
|
||||
|
||||
test('pickVerificationMessageWithTimeFallback supports required keyword hints and runtime code patterns', () => {
|
||||
const messages = [
|
||||
{
|
||||
id: 'mail-1',
|
||||
subject: 'Security center',
|
||||
from: { emailAddress: { address: 'alerts@example.com' } },
|
||||
bodyPreview: 'Use pin A-661122 to continue',
|
||||
receivedDateTime: '2026-04-14T10:06:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
const result = pickVerificationMessageWithTimeFallback(messages, {
|
||||
afterTimestamp: 0,
|
||||
senderFilters: [],
|
||||
subjectFilters: [],
|
||||
requiredKeywords: ['security'],
|
||||
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
|
||||
excludeCodes: [],
|
||||
});
|
||||
|
||||
assert.equal(result.match?.code, '661122');
|
||||
assert.equal(result.usedTimeFallback, false);
|
||||
});
|
||||
|
||||
test('getHotmailListToggleLabel reflects expanded state and account count', () => {
|
||||
assert.equal(getHotmailListToggleLabel(false, 0), '展开列表');
|
||||
assert.equal(getHotmailListToggleLabel(false, 7), '展开列表(7)');
|
||||
|
||||
@@ -54,6 +54,8 @@ function extractFunction(name) {
|
||||
test('readOpenedMailBody falls back to thread detail pane and extracts verification code', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
extractFunction('getOpenedMailBodyRoot'),
|
||||
extractFunction('readOpenedMailBody'),
|
||||
@@ -83,6 +85,8 @@ return { readOpenedMailBody, extractVerificationCode };
|
||||
|
||||
test('extractVerificationCode matches the new suspicious log-in mail body', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
@@ -95,6 +99,27 @@ return { extractVerificationCode };
|
||||
assert.equal(api.extractVerificationCode(bodyText), '982219');
|
||||
});
|
||||
|
||||
test('extractVerificationCode supports runtime mail rule patterns', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { extractVerificationCode };
|
||||
`)();
|
||||
|
||||
const bodyText = 'Mailbox notice\nUse verification pin A-445566 to continue.';
|
||||
assert.equal(
|
||||
api.extractVerificationCode(bodyText, {
|
||||
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
|
||||
}),
|
||||
'445566'
|
||||
);
|
||||
});
|
||||
|
||||
test('readOpenedMailBody ignores conversation list rows when no detail pane is open', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
@@ -199,6 +224,8 @@ test('icloud poll session baseline is reused across calls and enables fallback a
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getThreadItemMetadata'),
|
||||
extractFunction('buildItemSignature'),
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
extractFunction('normalizePollSessionKey'),
|
||||
extractFunction('getOrCreatePollSessionBaseline'),
|
||||
@@ -300,6 +327,8 @@ test('icloud step8 polling finds a visible first-row code immediately', async ()
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getThreadItemMetadata'),
|
||||
extractFunction('buildItemSignature'),
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
extractFunction('normalizePollSessionKey'),
|
||||
extractFunction('getOrCreatePollSessionBaseline'),
|
||||
@@ -378,6 +407,8 @@ test('icloud step8 visible first-row code still respects excluded codes', async
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getThreadItemMetadata'),
|
||||
extractFunction('buildItemSignature'),
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
extractFunction('normalizePollSessionKey'),
|
||||
extractFunction('getOrCreatePollSessionBaseline'),
|
||||
|
||||
@@ -166,6 +166,8 @@ test('readOpenedMailText prefers opened body content that contains the verificat
|
||||
extractFunction('collectOpenedMailTextCandidates'),
|
||||
extractFunction('selectOpenedMailTextCandidate'),
|
||||
extractFunction('readOpenedMailText'),
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
@@ -216,6 +218,8 @@ test('openMailAndGetMessageText reads opened body text and returns to inbox', as
|
||||
extractFunction('readOpenedMailText'),
|
||||
extractFunction('returnToInbox'),
|
||||
extractFunction('openMailAndGetMessageText'),
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
@@ -289,6 +293,8 @@ test('openMailAndGetMessageText ignores stale pre-open text that contains an old
|
||||
extractFunction('readOpenedMailText'),
|
||||
extractFunction('returnToInbox'),
|
||||
extractFunction('openMailAndGetMessageText'),
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
@@ -359,6 +365,8 @@ return { openMailAndGetMessageText, mailItem };
|
||||
|
||||
test('extractVerificationCode matches the new suspicious log-in mail body', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
@@ -371,6 +379,27 @@ return { extractVerificationCode };
|
||||
assert.equal(api.extractVerificationCode(bodyText), '982219');
|
||||
});
|
||||
|
||||
test('extractVerificationCode supports runtime mail rule patterns', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { extractVerificationCode };
|
||||
`)();
|
||||
|
||||
const bodyText = 'Security Center\nUse verification pin A-778899 to continue.';
|
||||
assert.equal(
|
||||
api.extractVerificationCode(bodyText, {
|
||||
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
|
||||
}),
|
||||
'778899'
|
||||
);
|
||||
});
|
||||
|
||||
test('handlePollEmail ignores same-minute old snapshot mail before fallback', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
|
||||
@@ -327,6 +327,7 @@ return {
|
||||
test('handlePollEmail skips explicit mismatched target emails when receive-mode matching is enabled', async () => {
|
||||
const bundle = [
|
||||
extractFunction('extractEmails'),
|
||||
extractFunction('normalizeTargetEmailHints'),
|
||||
extractFunction('extractForwardedTargetEmails'),
|
||||
extractFunction('emailMatchesTarget'),
|
||||
extractFunction('getTargetEmailMatchState'),
|
||||
@@ -419,6 +420,34 @@ return {
|
||||
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-2']);
|
||||
});
|
||||
|
||||
test('getTargetEmailMatchState decodes generic forwarded bounce aliases without OpenAI-specific domains', () => {
|
||||
const bundle = [
|
||||
extractFunction('extractEmails'),
|
||||
extractFunction('normalizeTargetEmailHints'),
|
||||
extractFunction('extractForwardedTargetEmails'),
|
||||
extractFunction('emailMatchesTarget'),
|
||||
extractFunction('getTargetEmailMatchState'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { getTargetEmailMatchState };
|
||||
`)();
|
||||
|
||||
const state = api.getTargetEmailMatchState(
|
||||
'Return-Path: <bounce+notice-expected.user=example.com@mailer.forwarder.net>',
|
||||
'expected.user@example.com',
|
||||
{
|
||||
targetEmailHints: ['expected.user@example.com', 'expected.user=example.com'],
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(state, {
|
||||
matches: true,
|
||||
hasExplicitEmail: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('handlePollEmail only accepts 2925 mails inside the fixed lookback window', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
@@ -565,7 +594,9 @@ return {
|
||||
|
||||
test('extractVerificationCode strict mode matches the new suspicious log-in mail body', () => {
|
||||
const bundle = [
|
||||
extractFunction('extractStrictChatGPTVerificationCode'),
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractLegacyStrictVerificationCode'),
|
||||
extractFunction('isLikelyCompactTimeValue'),
|
||||
extractFunction('isLikelyHeaderTimestampCode'),
|
||||
extractFunction('findSafeStandaloneSixDigitCode'),
|
||||
@@ -582,9 +613,36 @@ return { extractVerificationCode };
|
||||
assert.equal(api.extractVerificationCode(bodyText, false), '982219');
|
||||
});
|
||||
|
||||
test('extractVerificationCode supports runtime mail rule patterns', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractLegacyStrictVerificationCode'),
|
||||
extractFunction('isLikelyCompactTimeValue'),
|
||||
extractFunction('isLikelyHeaderTimestampCode'),
|
||||
extractFunction('findSafeStandaloneSixDigitCode'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { extractVerificationCode };
|
||||
`)();
|
||||
|
||||
const bodyText = 'System alert\nUse verification pin A-556677 to continue.';
|
||||
assert.equal(
|
||||
api.extractVerificationCode(bodyText, {
|
||||
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
|
||||
}),
|
||||
'556677'
|
||||
);
|
||||
});
|
||||
|
||||
test('extractVerificationCode ignores compact header time before fallback code', () => {
|
||||
const bundle = [
|
||||
extractFunction('extractStrictChatGPTVerificationCode'),
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractLegacyStrictVerificationCode'),
|
||||
extractFunction('isLikelyCompactTimeValue'),
|
||||
extractFunction('isLikelyHeaderTimestampCode'),
|
||||
extractFunction('findSafeStandaloneSixDigitCode'),
|
||||
|
||||
@@ -64,6 +64,36 @@ test('extractVerificationCodeFromMessages 支持显式过滤条件并跳过排
|
||||
});
|
||||
});
|
||||
|
||||
test('extractVerificationCodeFromMessages supports keyword hints and runtime code patterns without OpenAI-specific fallback', () => {
|
||||
const result = extractVerificationCodeFromMessages([
|
||||
{
|
||||
From: { EmailAddress: { Address: 'alerts@example.com' } },
|
||||
Subject: 'Security center',
|
||||
BodyPreview: 'Use pin A-551199 to continue',
|
||||
ReceivedDateTime: '2026-04-14T10:07:00.000Z',
|
||||
Id: 'mail-1',
|
||||
},
|
||||
{
|
||||
From: { EmailAddress: { Address: 'news@example.com' } },
|
||||
Subject: 'Newsletter',
|
||||
BodyPreview: 'pin A-000000',
|
||||
ReceivedDateTime: '2026-04-14T10:06:00.000Z',
|
||||
Id: 'mail-2',
|
||||
},
|
||||
], {
|
||||
filterAfterTimestamp: 0,
|
||||
senderFilters: [],
|
||||
subjectFilters: [],
|
||||
requiredKeywords: ['security'],
|
||||
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
|
||||
excludeCodes: [],
|
||||
});
|
||||
|
||||
assert.equal(result.code, '551199');
|
||||
assert.equal(result.messageId, 'mail-1');
|
||||
assert.equal(result.sender, 'alerts@example.com');
|
||||
});
|
||||
|
||||
test('normalizeMailboxId 将 Junk 归一为微软邮箱夹 ID', () => {
|
||||
assert.equal(normalizeMailboxId('INBOX'), 'inbox');
|
||||
assert.equal(normalizeMailboxId('junk'), 'junkemail');
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('content/qq-mail.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('qq extractVerificationCode supports runtime mail rule patterns', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
${bundle}
|
||||
return { extractVerificationCode };
|
||||
`)();
|
||||
|
||||
assert.equal(
|
||||
api.extractVerificationCode('Mailbox notice: use pin A-441122 to continue.', {
|
||||
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
|
||||
}),
|
||||
'441122'
|
||||
);
|
||||
});
|
||||
|
||||
test('qq handlePollEmail forwards runtime code patterns to new-mail matching', async () => {
|
||||
const bundle = [
|
||||
extractFunction('getCurrentMailIds'),
|
||||
extractFunction('normalizeRulePatternList'),
|
||||
extractFunction('extractCodeByRulePatterns'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let currentItems = [];
|
||||
let refreshCount = 0;
|
||||
|
||||
function createMailItem(mailId, sender, subject, digest) {
|
||||
return {
|
||||
getAttribute(name) {
|
||||
if (name === 'data-mailid') return mailId;
|
||||
return '';
|
||||
},
|
||||
querySelector(selector) {
|
||||
if (selector === '.cmp-account-nick') return { textContent: sender };
|
||||
if (selector === '.mail-subject') return { textContent: subject };
|
||||
if (selector === '.mail-digest') return { textContent: digest };
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === '.mail-list-page-item[data-mailid]') {
|
||||
return currentItems;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
async function waitForElement() {
|
||||
return true;
|
||||
}
|
||||
async function refreshInbox() {
|
||||
refreshCount += 1;
|
||||
if (refreshCount >= 1) {
|
||||
currentItems = [
|
||||
createMailItem('mail-1', 'alerts@example.com', 'Security center', 'Use pin A-551188 to continue'),
|
||||
];
|
||||
}
|
||||
}
|
||||
async function sleep() {}
|
||||
function log() {}
|
||||
|
||||
${bundle}
|
||||
|
||||
return { handlePollEmail };
|
||||
`)();
|
||||
|
||||
const result = await api.handlePollEmail(4, {
|
||||
senderFilters: ['alerts'],
|
||||
subjectFilters: ['security'],
|
||||
maxAttempts: 2,
|
||||
intervalMs: 1,
|
||||
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
|
||||
});
|
||||
|
||||
assert.equal(result.code, '551188');
|
||||
});
|
||||
@@ -196,6 +196,107 @@ test('startAutoRunFromCurrentSettings freezes run count before async settings sy
|
||||
assert.equal(events[3].message.payload.totalRuns, 20);
|
||||
});
|
||||
|
||||
test('startAutoRunFromCurrentSettings blocks when shared flow capability validation fails', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizePendingAutoRunStartRunCount'),
|
||||
extractFunction('registerPendingAutoRunStartRunCount'),
|
||||
extractFunction('clearPendingAutoRunStartRunCount'),
|
||||
extractFunction('startAutoRunFromCurrentSettings'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const events = [];
|
||||
const latestState = {
|
||||
activeFlowId: 'site-a',
|
||||
panelMode: 'cpa',
|
||||
signupMethod: 'phone',
|
||||
contributionMode: false,
|
||||
phoneVerificationEnabled: true,
|
||||
};
|
||||
const inputAutoSkipFailures = { checked: false };
|
||||
const inputContributionNickname = { value: 'tester' };
|
||||
const inputContributionQq = { value: '123456' };
|
||||
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
|
||||
const inputAutoDelayEnabled = { checked: false };
|
||||
const inputAutoDelayMinutes = { value: '30' };
|
||||
const btnAutoRun = { disabled: false, innerHTML: '' };
|
||||
const inputRunCount = { disabled: false, value: '1' };
|
||||
const inputPhoneVerificationEnabled = { checked: true };
|
||||
const inputPlusModeEnabled = { checked: false };
|
||||
let runCountValue = 1;
|
||||
let pendingAutoRunStartTotalRuns = 0;
|
||||
let pendingAutoRunStartExpiresAt = 0;
|
||||
const chrome = {
|
||||
runtime: {
|
||||
async sendMessage(message) {
|
||||
events.push({ type: 'send', message });
|
||||
return { ok: true };
|
||||
},
|
||||
},
|
||||
};
|
||||
const console = {
|
||||
warn(...args) {
|
||||
events.push({ type: 'warn', args });
|
||||
},
|
||||
};
|
||||
const window = {
|
||||
MultiPageFlowCapabilities: {
|
||||
createFlowCapabilityRegistry() {
|
||||
return {
|
||||
validateAutoRunStart() {
|
||||
return {
|
||||
ok: false,
|
||||
errors: [{ message: '当前 flow 不支持手机号注册。' }],
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
async function sendSidepanelMessage(message) {
|
||||
return chrome.runtime.sendMessage(message);
|
||||
}
|
||||
async function persistCurrentSettingsForAction() {
|
||||
events.push({ type: 'sync-settings' });
|
||||
}
|
||||
function getRunCountValue() { return Math.max(1, Number(runCountValue) || 1); }
|
||||
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
|
||||
function shouldOfferAutoModeChoice() { return false; }
|
||||
async function openAutoStartChoiceDialog() { throw new Error('should not be called'); }
|
||||
function getFirstUnfinishedStep() { return 1; }
|
||||
function getRunningSteps() { return []; }
|
||||
function shouldWarnAutoRunFallbackRisk() { return false; }
|
||||
function isAutoRunFallbackRiskPromptDismissed() { return false; }
|
||||
async function openAutoRunFallbackRiskConfirmModal() { throw new Error('should not be called'); }
|
||||
function setAutoRunFallbackRiskPromptDismissed() {}
|
||||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
||||
async function refreshContributionContentHint() {
|
||||
events.push({ type: 'refresh' });
|
||||
return null;
|
||||
}
|
||||
async function ensureGpcApiKeyReadyForStart() {
|
||||
return true;
|
||||
}
|
||||
${bundle}
|
||||
return {
|
||||
startAutoRunFromCurrentSettings,
|
||||
getEvents() {
|
||||
return events;
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
await assert.rejects(
|
||||
() => api.startAutoRunFromCurrentSettings(),
|
||||
/当前 flow 不支持手机号注册。/
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
api.getEvents().map((entry) => entry.type),
|
||||
['refresh', 'sync-settings']
|
||||
);
|
||||
});
|
||||
|
||||
test('persistCurrentSettingsForAction forces a silent save even when settings are not marked dirty', async () => {
|
||||
const bundle = [
|
||||
extractFunction('waitForSettingsSaveIdle'),
|
||||
|
||||
@@ -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' };
|
||||
|
||||
@@ -106,7 +106,7 @@ return {
|
||||
assert.deepEqual(api.getStepIds(), [7]);
|
||||
assert.deepEqual(api.calls[0], {
|
||||
type: 'getSteps',
|
||||
options: { plusModeEnabled: true, plusPaymentMethod: 'gopay', signupMethod: 'email' },
|
||||
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', signupMethod: 'email' },
|
||||
});
|
||||
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [7] });
|
||||
});
|
||||
@@ -116,6 +116,15 @@ test('sidepanel normalizeSignupMethod stays independent from signup constants du
|
||||
assert.doesNotMatch(source, /SIGNUP_METHOD_(PHONE|EMAIL)/);
|
||||
});
|
||||
|
||||
test('sidepanel initializes latestState before bootstrapping shared step definitions', () => {
|
||||
const latestStateIndex = sidepanelSource.indexOf('let latestState = null;');
|
||||
const bootstrapIndex = sidepanelSource.indexOf('let stepDefinitions = getStepDefinitionsForMode(false, {');
|
||||
|
||||
assert.notEqual(latestStateIndex, -1);
|
||||
assert.notEqual(bootstrapIndex, -1);
|
||||
assert.ok(latestStateIndex < bootstrapIndex);
|
||||
});
|
||||
|
||||
test('sidepanel signup method UI syncs shared step definitions with the selected signup method', () => {
|
||||
const source = extractFunction('updateSignupMethodUI');
|
||||
assert.match(source, /syncStepDefinitionsForMode\(/);
|
||||
@@ -124,8 +133,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 +174,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'),
|
||||
@@ -210,7 +275,7 @@ return {
|
||||
assert.deepEqual(api.getStepIds(), [13]);
|
||||
assert.deepEqual(api.calls[0], {
|
||||
type: 'getSteps',
|
||||
options: { plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', signupMethod: 'email' },
|
||||
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', signupMethod: 'email' },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -16,6 +16,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
|
||||
assert.equal(Array.isArray(steps), true);
|
||||
assert.equal(steps.length, 10);
|
||||
assert.equal(steps.every((step) => step.flowId === 'openai'), true);
|
||||
assert.deepStrictEqual(
|
||||
steps.map((step) => step.order),
|
||||
steps.map((step) => step.order).slice().sort((left, right) => left - right)
|
||||
@@ -68,6 +69,11 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
assert.equal(api.getPlusPaymentStepTitle({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), '');
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 13);
|
||||
assert.equal(api.hasFlow('openai'), true);
|
||||
assert.equal(api.hasFlow('site-a'), false);
|
||||
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai']);
|
||||
assert.deepStrictEqual(api.getSteps({ activeFlowId: 'site-a' }), []);
|
||||
assert.equal(api.getStepById(2, { activeFlowId: 'site-a' }), null);
|
||||
assert.equal(plusSteps[5].title, '创建 Plus Checkout');
|
||||
assert.equal(plusSteps[7].title, 'PayPal 登录与授权');
|
||||
|
||||
|
||||
@@ -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 () => {},
|
||||
|
||||
+16
-8
@@ -128,7 +128,7 @@
|
||||
- 内容脚本通过 `log(message, level, { step, stepKey })` 上报结构化日志,`reportComplete` / `reportError` 的步骤号只走消息字段和日志元数据。
|
||||
- [sidepanel/sidepanel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.js) 只读取 `entry.step` 渲染步骤标签;日志正文只作为正文展示,不参与步骤号判断。
|
||||
- Plus 模式后半段复用普通执行器时,必须先解析当前可见步骤:`oauth-login=10`、`fetch-login-code=11`、`confirm-oauth=12`、`platform-verify=13`,再把该步骤号传入日志、完成信号和内容脚本 payload。
|
||||
- 平台验证链路中,CPA / SUB2API / Codex2API 都按当前 `platform-verify` 可见步骤上报;SUB2API 内容脚本请求额外携带 `visibleStep`,避免 Plus 第 13 步落回普通 Step 10。
|
||||
- 平台验证链路中,CPA / SUB2API / Codex2API 都按当前 `platform-verify` 可见步骤上报;SUB2API 和 Codex2API 的协议式分支由后台直接完成 callback 交换与账号创建,不再依赖后台页面内容脚本修正步骤号。
|
||||
|
||||
## 4. 状态与存储链路
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
- 当前手机号验证激活记录 `currentPhoneActivation`
|
||||
- 可复用的手机号验证激活记录 `reusablePhoneActivation`
|
||||
- localhost 回调地址
|
||||
- SUB2API OAuth 运行态:`sub2apiSessionId / sub2apiOAuthState / sub2apiGroupId / sub2apiGroupIds / sub2apiDraftName / sub2apiProxyId`,用于把步骤 7 生成的会话、目标分组与代理选择传给步骤 10 / 13 的后台直连接口。
|
||||
- 自动运行轮次信息
|
||||
- 当前自动运行 session 标识 `autoRunSessionId`
|
||||
- IP 代理运行态:`ipProxyApiPool / ipProxyAccountPool / ipProxyCurrent / ipProxyApplied*`,用于记录当前代理池、当前代理节点、PAC 接管结果、出口检测结果和诊断信息
|
||||
@@ -441,7 +442,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
||||
|
||||
1. 按当前来源刷新 OAuth 地址:
|
||||
- CPA:打开管理页并读取 OAuth 地址
|
||||
- SUB2API:打开后台并生成 OAuth 地址
|
||||
- SUB2API:后台直连 SUB2API 管理接口,登录后解析 openai 分组、可选代理,并调用 `/api/v1/admin/openai/generate-auth-url`
|
||||
- Codex2API:直接调用后台协议 `/api/admin/oauth/generate-auth-url`
|
||||
2. 打开最新 OAuth 链接
|
||||
3. 根据统一账号标识选择登录身份:
|
||||
@@ -478,7 +479,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
||||
- 打开邮箱页或 API 轮询入口
|
||||
- 轮询登录验证码并回填
|
||||
4. 当前是真实 `phone-verification` 页:
|
||||
- 才复用手机号验证码共享流程,重新激活同一号码并轮询短信验证码
|
||||
- 才复用 OpenAI 专属手机号验证码流程,重新激活同一号码并轮询短信验证码
|
||||
- 在当前 `phone-verification` 页回填短信验证码
|
||||
5. 如果登录验证码提交后页面进入 `add-phone / 手机号页`,则邮箱注册模式下交给后续 OAuth 后置手机号验证链路;手机号注册模式不把 `signupPhone*` 身份误当成官方 add-phone 订单
|
||||
6. 如遇邮箱轮询类失败或显式的 `STEP8_RESTART_STEP7` 恢复错误,则按有限次数回到 Step 7 重试;若仍停在验证码页、手机验证码页或 add-email 页,则优先在当前链路重试
|
||||
@@ -503,8 +504,8 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
||||
流程:
|
||||
|
||||
1. 监听 localhost callback
|
||||
2. 准备 OAuth 同意页;如果页面已进入 `add-phone / phone-verification`,先切入手机号验证共享流程
|
||||
3. 手机号验证共享流程会按当前 sidepanel 中保存的接码平台 `phoneSmsProvider` 选择 HeroSMS 或 5sim,并使用对应平台的 API Key、国家/地区、价格上限申请或复用号码
|
||||
2. 准备 OAuth 同意页;如果页面已进入 `add-phone / phone-verification`,先切入 OpenAI 专属手机号验证流程
|
||||
3. OpenAI 手机号验证流程会按当前 sidepanel 中保存的接码平台 `phoneSmsProvider` 选择 HeroSMS / 5sim / NexSMS,并使用对应平台的 API Key、国家/地区、价格上限申请或复用号码
|
||||
4. 后台提交号码后,会按 `currentPhoneActivation.provider` 分发后续查码、完成、取消、ban、reuse:HeroSMS 走原 `getStatus / setStatus` 逻辑,5sim 走 `/v1/user/check|finish|cancel|ban|reuse`
|
||||
5. 验证码被拒绝或长时间收不到短信时,会按平台决定重发、换号、ban/取消当前激活,必要时把自动流拉回 Step 7
|
||||
6. 手机号验证完成后,如果认证页偶发进入资料页,内容脚本会复用 Step 5 的姓名生日填写逻辑补齐资料,再继续等待 OAuth 同意页
|
||||
@@ -514,7 +515,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
||||
|
||||
补充:
|
||||
|
||||
- HeroSMS 和 5sim 都归入接码平台适配层;默认平台是 HeroSMS,5sim 产品码固定为 `openai`,operator 默认 `any`。
|
||||
- HeroSMS / 5sim / NexSMS 都归入 OpenAI 接码平台适配层;默认平台是 HeroSMS,5sim 产品码固定为 `openai`,operator 默认 `any`。该适配层暂不进入多注册 flow 的 core 通用服务,后续只有第二个真实 flow 也需要接码时再按实际复用点抽象。
|
||||
- 号码当前最多复用 3 次成功注册;超过上限后会清空可复用激活记录,下次重新申请新号码。
|
||||
- 5sim 的国家/地区使用字符串 slug(当前开放 `indonesia / thailand / vietnam`),HeroSMS 仍使用数字国家 ID(当前开放印度尼西亚/泰国/越南)。
|
||||
- 如果同一个号码在重发短信后仍收不到验证码,后台会按当前平台取消或 ban 激活并换号,而不是把当前号码无限重试下去。
|
||||
@@ -530,8 +531,8 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
|
||||
|
||||
1. 校验 localhost callback 是否有效
|
||||
2. 判断当前来源是 CPA、SUB2API 还是 Codex2API
|
||||
3. CPA / SUB2API 打开相应后台;Codex2API 直接走协议分支,不打开后台页面
|
||||
4. 提交回调地址或等价的授权码交换请求
|
||||
3. CPA 打开相应后台;SUB2API / Codex2API 直接走协议分支,不打开后台页面
|
||||
4. 提交回调地址或等价的授权码交换请求;SUB2API 会调用 `/api/v1/admin/openai/exchange-code` 交换授权码,再调用 `/api/v1/admin/accounts` 创建账号
|
||||
5. 仅当出现精确成功徽标,且该徽标不是红色/错误态、页面上也没有同时可见的失败提示时,才判定成功
|
||||
6. 识别 `认证失败:*`、`认证失败: timeout of 30000ms exceeded`、`回调 URL 提交失败: oauth flow is not pending` 等失败提示并立即报错
|
||||
7. 完成平台侧验证
|
||||
@@ -545,6 +546,13 @@ Codex2API 补充:
|
||||
- 步骤 10 直接调用 `POST /api/admin/oauth/exchange-code`,用 callback 中的 `code / state` 完成账号创建
|
||||
- Codex2API 这条来源不新增 panel content script,也不依赖“添加账号 -> OAuth 授权 -> 生成授权链接”页面按钮 DOM
|
||||
|
||||
SUB2API 补充:
|
||||
|
||||
- 步骤 7 直接调用 `POST /api/v1/auth/login` 登录管理接口,再调用 `/api/v1/admin/groups/all` 校验目标 openai 分组;如果配置了默认代理,会读取 `/api/v1/admin/proxies/all?with_count=true` 并按名称或 ID 选择代理。
|
||||
- 步骤 7 调用 `POST /api/v1/admin/openai/generate-auth-url` 获取 `auth_url / session_id / state`,并把目标分组 ID、代理 ID、草稿账号名写入运行态。
|
||||
- 步骤 10 / Plus 第 13 步直接用 localhost callback 中的 `code / state` 调用 `POST /api/v1/admin/openai/exchange-code`,再用返回的 OpenAI token 信息调用 `POST /api/v1/admin/accounts` 创建 SUB2API 账号。
|
||||
- SUB2API 主链路不再打开后台页面,也不再依赖 `content/sub2api-panel.js` 的 DOM 自动化;该内容脚本仅保留为旧后台页面路径的兼容能力。
|
||||
|
||||
贡献模式补充:
|
||||
|
||||
- 贡献模式下,步骤 10 不再打开 CPA 管理页
|
||||
|
||||
+16
-9
@@ -58,8 +58,9 @@
|
||||
- `background/registration-email-state.js`:注册邮箱运行态共享模块,负责维护 `registrationEmailState = { current, previous, source, updatedAt }`,并提供“当前邮箱清空时保留上一比较基线”“Duck 生成前解析比较基线”“Step 8 `add-email` 写入邮箱时按需保留手机号身份”的统一工具。
|
||||
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API / Codex2API 地址归一化、来源标签页家族判断与步骤跳转相关判断。
|
||||
- `background/paypal-account-store.js`:PayPal 账号池持久化模块,负责保存账号列表、切换当前账号,并把当前选中账号同步回兼容字段 `paypalEmail / paypalPassword`。
|
||||
- `background/phone-verification-flow.js`:手机号验证码共享流程模块,负责复用 HeroSMS / 5sim / NexSMS 的取号、复用、轮询、重发、换号与收尾能力;既承接 OAuth 后置 `add-phone / phone-verification` 页面,也承接手机号注册 Step 4 和 Step 8 中真实出现的 `phone-verification` 页面,并保持 `signupPhone*` 运行态与 `currentPhoneActivation` 隔离;后置 add-phone 成功后会把已绑定手机号写入运行态 `phoneNumber`,供账号记录并入同一轮;提交后置手机号验证码时会一并透传可复用的资料页 payload,便于认证页偶发跳回资料页时继续补齐;该流程由调用方传入当前可见步骤号,手机号日志不会再固定显示普通模式 Step 9。
|
||||
- `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。
|
||||
- `background/phone-verification-flow.js`:OpenAI 专属手机号验证码流程模块,负责复用 HeroSMS / 5sim / NexSMS 的取号、复用、轮询、重发、换号与收尾能力;既承接 OpenAI OAuth 后置 `add-phone / phone-verification` 页面,也承接 OpenAI 手机号注册 Step 4 和 Step 8 中真实出现的 `phone-verification` 页面,并保持 `signupPhone*` 运行态与 `currentPhoneActivation` 隔离;后续多注册 flow 架构中,该模块应收拢到 `flows/openai` 边界内,不作为 core 通用接码服务提前抽象;后置 add-phone 成功后会把已绑定手机号写入运行态 `phoneNumber`,供账号记录并入同一轮;提交后置手机号验证码时会一并透传可复用的资料页 payload,便于认证页偶发跳回资料页时继续补齐;该流程由调用方传入当前可见步骤号,手机号日志不会再固定显示普通模式 Step 9。
|
||||
- `background/panel-bridge.js`:来源桥接层;CPA 继续封装页面打开、脚本注入和通信,SUB2API / Codex2API 则通过后台协议生成 OAuth 地址。
|
||||
- `background/sub2api-api.js`:SUB2API 管理接口直连模块,负责后台登录 SUB2API、解析 openai 分组、按配置选择代理、生成 OpenAI OAuth 地址、交换 localhost callback 授权码并创建 SUB2API 账号;主链路不再依赖 SUB2API 后台页面 DOM 自动化。
|
||||
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、Step 2 打开入口页后的加载完成与额外 3 秒稳定等待、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号;若邮箱生成器在生成阶段已经写回运行态,这里不会再重复覆盖 `registrationEmailState`,但仍会在 Step 8 `add-email` 的手机号身份场景下通过共享持久化按需保留手机号身份。
|
||||
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
|
||||
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置、Cloud Mail API 轮询以及 2925 长轮询参数;日志步骤号使用 `completionStep`,因此 Plus 登录验证码会显示可见第 11 步而不是内部复用的 Step 8;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给 OAuth 确认步骤,而不是误判为普通失败;若登录验证码提交后通信中断但认证页已进入 OAuth 同意页或手机号验证页,会按真实页面状态继续后续流程;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
|
||||
@@ -69,7 +70,7 @@
|
||||
- `background/steps/wait-registration-success.js`:步骤 6 实现,负责注册资料提交后等待 20 秒,让注册成功状态和页面跳转稳定;默认不清理 cookies,只有侧栏第六步 `清 Cookies` 开关开启时才会在等待结束后清理 ChatGPT / OpenAI 相关 cookies。
|
||||
- `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成;等待 callback 期间会动态检查 OAuth 总预算开关,关闭后仅保留本地回调等待超时。
|
||||
- `background/steps/create-plus-checkout.js`:Plus 模式第 6 步实现,负责在已登录 ChatGPT 页面创建 PayPal / GoPay checkout session,或在 GPC 模式下读取 accessToken 并通过 `https://gpc.qlhazycoder.top/api/gp/tasks` 创建队列任务,记录 checkout / GPC task 运行态。
|
||||
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责按真实认证页状态分发登录后续流程:邮箱验证码页走邮箱轮询、验证码回填与回退控制;真实 `phone-verification` 页才复用手机号验证码共享流程;手机号注册登录后若进入 `add-email`,会先按共享邮箱状态持久化规则生成/解析邮箱并提交绑定,在不丢失当前手机号身份的前提下再进入邮箱验证码轮询;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录;命中 `email_in_use` 时会仅清空当前邮箱并保留上一轮比较基线,再在当前 Step 8 内重开 `add-email` 获取新邮箱,`max_check_attempts` 也只重启当前步骤恢复链。
|
||||
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责按真实认证页状态分发登录后续流程:邮箱验证码页走邮箱轮询、验证码回填与回退控制;真实 `phone-verification` 页才复用 OpenAI 专属手机号验证码流程;手机号注册登录后若进入 `add-email`,会先按共享邮箱状态持久化规则生成/解析邮箱并提交绑定,在不丢失当前手机号身份的前提下再进入邮箱验证码轮询;邮箱分支对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱,当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录;命中 `email_in_use` 时会仅清空当前邮箱并保留上一轮比较基线,再在当前 Step 8 内重开 `add-email` 获取新邮箱,`max_check_attempts` 也只重启当前步骤恢复链。
|
||||
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;邮箱注册继续走邮箱验证码,手机号注册改走短信验证码;当 provider 为 2925 时,会在邮箱轮询前先确保当前 2925 账号已自动登录。
|
||||
- `background/steps/fill-plus-checkout.js`:Plus 模式第 7 步实现,负责驱动 PayPal / GoPay checkout 页面选择付款方式、填写账单地址、提交订阅并等待跳转;GPC 模式则轮询队列任务,按远端 `api_waiting_for` 提交 OTP / PIN,并在任务失败、过期或取消时清理运行态。
|
||||
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交;当前已改为身份中立,手机号注册遇到无密码页时会按页面状态自动跳过。
|
||||
@@ -77,7 +78,7 @@
|
||||
- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;当前会根据 `accountIdentifierType / accountIdentifier` 选择邮箱或手机号登录,手机号账号会先探测并切换手机号登录入口;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。
|
||||
- `background/steps/open-chatgpt.js`:步骤 1 实现,负责打开 ChatGPT 官网并确认入口就绪。
|
||||
- `background/steps/paypal-approve.js`:Plus 模式第 8 步实现,负责驱动 PayPal 登录、关闭可见通行密钥提示、点击“同意并继续”,并等待授权流程离开 PayPal 页面。
|
||||
- `background/steps/platform-verify.js`:平台回调验证实现,普通模式为步骤 10,Plus 模式为可见步骤 13;负责 CPA / SUB2API 回调验证,以及 Codex2API 的协议式 callback code/state 交换,所有平台验证日志和完成信号都按当前可见步骤上报。
|
||||
- `background/steps/platform-verify.js`:平台回调验证实现,普通模式为步骤 10,Plus 模式为可见步骤 13;负责 CPA 页面回调验证、SUB2API 管理接口 callback code/state 交换与账号创建,以及 Codex2API 的协议式 callback code/state 交换,所有平台验证日志和完成信号都按当前可见步骤上报。
|
||||
- `background/steps/plus-return-confirm.js`:Plus 模式第 9 步实现,负责等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒再完成。
|
||||
- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
|
||||
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责按 `signupMethod` 在邮箱注册与手机号注册之间分发;切回已有注册页标签时会先等待加载完成并额外稳定 3 秒,再检查注册入口状态;邮箱分支会先点击官网“免费注册”再提交邮箱并清理旧手机号注册运行态,手机号分支会先点击官网“免费注册”并切到手机号注册入口,确认手机号输入页就绪后优先复用已有 signup activation,其次使用手动运行态手机号,最后才申请接码平台号码并提交,最后根据落地页判断是否跳过后续密码步骤。
|
||||
@@ -98,7 +99,7 @@
|
||||
- `content/plus-checkout.js`:ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。
|
||||
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
|
||||
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、官网注册入口点击前固定等待 3 秒、入口点击失败后最多 5 次重试、手机号国家下拉框同步校验、手机号填写与提交;Step 3 收尾会识别手机号注册密码页的 `Incorrect phone number or password` 并上报当前轮重开信号;Step 7 / 8 登录链路会额外识别手机号输入页、手机号登录入口、`phone-verification` 与真实 `add-email` 页面,避免把手机验证码页或添加邮箱页误判成普通邮箱登录入口;Step 9 后置手机号验证完成后若偶发进入资料页,会复用 Step 5 的姓名生日填写逻辑再继续等待 OAuth 同意页。
|
||||
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;平台验证请求会读取 `visibleStep`,普通模式承接步骤 10,Plus 模式承接步骤 13。
|
||||
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,保留后台页面内生成 OAuth 地址和提交 localhost 回调的兼容能力;当前主流程已迁移到 `background/sub2api-api.js` 的管理接口直连路径。
|
||||
- `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。
|
||||
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10 与 Plus 可见步骤 13。
|
||||
|
||||
@@ -112,14 +113,20 @@
|
||||
|
||||
- `docs/仓库协作者AI分析PR与合并标准流程.md`:仓库协作者进行 AI 分析 PR 与合并时的流程说明。
|
||||
- `docs/ip-proxy-module.md`:IP Proxy 模块说明,记录模块定位、后台/侧栏代码结构、当前功能范围、711Proxy 参数联动、操作方式、发布策略与已知限制。
|
||||
- `docs/local-customizations-maintenance.md`:记录本地相对上游长期保留的定制能力、关键代码位置,以及后续同步上游时的检查点。
|
||||
- `docs/公告书写标准.md`:公告类文档的书写标准与维护约束。
|
||||
- `docs/使用教程.md`:旧版完整使用教程,保留尚未完全拆分的 HeroSMS、IP 代理、GoPay 等补充章节。
|
||||
- `docs/使用教程/使用教程.md`:分部分使用教程总索引。
|
||||
- `docs/使用教程/使用教程书写模板.md`:后续维护分部分教程时使用的书写与拆分模板。
|
||||
- `docs/使用教程/分部分/*.md`:按主题拆分后的使用教程正文。
|
||||
- `docs/错误重试分层策略.md`:记录错误分类、恢复边界与重试策略分层,避免不同模块各自定义失败语义。
|
||||
- `docs/多注册流程架构边界.md`:后续从 OpenAI 单注册流程升级到多注册 flow 时的架构边界说明,明确 core 通用能力、flow 私有能力,以及手机接码暂归 OpenAI flow、不提前抽成通用服务的决策。
|
||||
- `docs/多注册流程状态迁移设计.md`:多注册 flow 重构时的状态迁移设计,明确共享运行态、服务运行态、flow 私有状态与旧 step 兼容层。
|
||||
- `docs/多注册流程来源与驱动注册设计.md`:多注册 flow 下 source registry、driver registry、内容脚本注入与 localhost callback 清理的设计说明。
|
||||
- `docs/多注册流程邮件分层设计.md`:多注册 flow 下邮件 provider driver 与 flow mail rules 的分层设计说明。
|
||||
- `docs/多注册流程侧边栏能力矩阵.md`:多注册 flow 下 sidepanel 的 flow capability、panel capability 与 runtime lock 设计说明。
|
||||
- `docs/images/交流群.jpg`:README 中展示的官网 / QQ 交流群入口图片。
|
||||
- `docs/refactor/2026-04-16-architecture-refactor-plan.md`:本轮重构阶段的结构设计与迁移计划记录。
|
||||
- `docs/superpowers/plans/2026-04-10-hotmail-oauth-mail-pool.md`:Hotmail OAuth 邮箱池实现计划文档。
|
||||
- `docs/superpowers/specs/2026-04-10-hotmail-oauth-design.md`:Hotmail OAuth + Graph Mail 的设计规格文档。
|
||||
- `docs/images/微信.png`:README 与文档中使用的微信相关展示图片。
|
||||
|
||||
## `icons/`
|
||||
|
||||
@@ -217,7 +224,7 @@
|
||||
- `tests/background-verification-flow-module.test.js`:测试验证码流程模块已接入且导出工厂。
|
||||
- `tests/phone-auth-country-match.test.js`:测试认证页手机号国家选择在页面本地化时,仍能用 HeroSMS 的英文国家名匹配对应国家选项。
|
||||
- `tests/phone-country-utils.test.js`:测试手机号国家/区号共享工具的区号提取、最长前缀解析、国家别名匹配,以及 manifest 与后台动态注入顺序。
|
||||
- `tests/phone-verification-flow.test.js`:测试手机号验证共享流程对 HeroSMS 的取号、复用、重发、换号与 Step 7 重开错误分支。
|
||||
- `tests/phone-verification-flow.test.js`:测试 OpenAI 专属手机号验证流程对 HeroSMS / 5sim / NexSMS 的取号、复用、重发、换号与 Step 7 重开错误分支。
|
||||
- `tests/paypal-approve-detection.test.js`:测试 Plus 第 8 步后台执行器对 PayPal 标签页发现、分离式账号/密码页识别、联合登录页识别,以及登录后直接离开 PayPal 页的分支判断。
|
||||
- `tests/paypal-flow-content.test.js`:测试 PayPal 内容脚本对可见邮箱/密码输入框的识别,并覆盖邮箱页即使已预填相同账号也会先清空再重填后继续下一步。
|
||||
- `tests/gpc-sms-helper-script.test.js`:测试 GPC macOS 本地短信 helper 在非 macOS 环境下会提示平台与 iPhone 短信转发要求。
|
||||
|
||||
Reference in New Issue
Block a user