Refactor multi-flow mail capability boundaries

This commit is contained in:
QLHazyCoder
2026-05-13 06:35:43 +08:00
parent a85ed0ce89
commit d93cdfff9e
33 changed files with 2035 additions and 198 deletions
+123 -5
View File
@@ -61,21 +61,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,
@@ -85,7 +94,6 @@ const STEP_IDS = Array.from(new Set(ALL_STEP_DEFINITIONS
.map((definition) => Number(definition?.id))
.filter(Number.isFinite)))
.sort((left, right) => left - right);
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const DEFAULT_STEP_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending']));
const NORMAL_STEP_IDS = NORMAL_STEP_DEFINITIONS
.map((definition) => Number(definition?.id))
@@ -597,11 +605,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;
@@ -633,10 +651,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;
}
@@ -1374,6 +1401,38 @@ function resolveCurrentFlowCapabilities(state = {}, options = {}) {
});
}
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)
@@ -3029,6 +3088,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);
@@ -4151,6 +4234,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,
}),
@@ -4427,6 +4512,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;
@@ -5609,6 +5696,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;
@@ -8680,6 +8769,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;
@@ -11814,6 +11927,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
normalizeSignupMethod,
canUsePhoneSignup,
resolveSignupMethod,
validateAutoRunStart: validateAutoRunStartState,
getTabId,
getStopRequested: () => stopRequested,
handleCloudflareSecurityBlocked,
@@ -11902,6 +12016,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;
}
+64 -1
View File
@@ -74,6 +74,44 @@
}
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,
@@ -908,6 +946,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('已有自动运行倒计时计划,请先取消或立即开始。');
}
@@ -935,6 +977,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,
@@ -1006,6 +1053,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,
@@ -1017,6 +1074,7 @@
|| 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);
}
@@ -1117,7 +1175,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': {
+12 -17
View File
@@ -412,27 +412,22 @@
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
View File
@@ -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
View File
@@ -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) {
+62 -8
View File
@@ -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
View File
@@ -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
View File
@@ -670,7 +670,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,
@@ -742,11 +791,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 || '');
@@ -754,11 +808,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];
@@ -771,9 +822,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);
@@ -783,7 +834,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) {
@@ -796,19 +859,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
? {
@@ -1175,14 +1239,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));
@@ -1249,21 +1314,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
View File
@@ -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);
+90 -26
View File
@@ -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,
};
+45 -3
View File
@@ -3,6 +3,42 @@
})(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 {
@@ -24,24 +60,30 @@
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: signupStep
? state?.email
: (String(state?.step8VerificationTargetEmail || '').trim() || state?.email),
targetEmail,
targetEmailHints: buildTargetEmailHints(targetEmail),
mail2925MatchTargetEmail: shouldMatchMail2925TargetEmail(state),
maxAttempts: mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
intervalMs: mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
+60 -13
View File
@@ -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;
}
+64 -25
View File
@@ -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
View File
@@ -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,
+195
View File
@@ -38,6 +38,14 @@
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({
@@ -95,6 +103,17 @@
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,
@@ -122,6 +141,21 @@
};
}
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(
@@ -202,6 +236,165 @@
};
}
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;
}
@@ -222,6 +415,8 @@
normalizeSignupMethod,
resolveSidepanelCapabilities,
resolveSignupMethod,
validateAutoRunStart,
validateModeSwitch,
};
}
+45
View File
@@ -789,6 +789,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
@@ -796,7 +797,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),
@@ -830,6 +835,7 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
currentPlusPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod);
currentSignupMethod = normalizeSignupMethod(rawSignupMethod);
stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled, {
activeFlowId: options?.activeFlowId,
plusPaymentMethod: currentPlusPaymentMethod,
signupMethod: currentSignupMethod,
});
@@ -8524,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
@@ -8533,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,
@@ -8551,6 +8564,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
}
rebuildStepDefinitionState(nextPlusModeEnabled, {
activeFlowId: nextActiveFlowId,
plusPaymentMethod: nextPaymentMethod,
signupMethod: nextSignupMethod,
});
@@ -11834,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;
+106
View File
@@ -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);
});
@@ -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'
);
});
@@ -42,10 +42,26 @@ test('mail rule registry exposes canonical OpenAI verification poll payloads', (
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,
@@ -64,10 +80,26 @@ test('mail rule registry exposes canonical OpenAI verification poll payloads', (
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,
@@ -198,3 +198,67 @@ test('SAVE_SETTING re-resolves signup method when panel mode changes', async ()
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');
});
@@ -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');
});
@@ -177,6 +177,7 @@ return {
});
assert.deepEqual(api.getCaptured(), [{
activeFlowId: 'openai',
plusModeEnabled: true,
plusPaymentMethod: 'gopay',
signupMethod: 'phone',
+88
View File
@@ -70,3 +70,91 @@ test('flow capability registry defaults unknown flows to minimal non-phone capab
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',
]
);
});
+33
View File
@@ -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');
+31
View File
@@ -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'),
+29
View File
@@ -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'),
+60 -2
View File
@@ -319,6 +319,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'),
@@ -407,6 +408,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'),
@@ -549,7 +578,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'),
@@ -566,9 +597,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'),
+30
View File
@@ -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');
+139
View File
@@ -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'),
+2 -2
View File
@@ -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] });
});
@@ -266,7 +266,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' },
});
});
+6
View File
@@ -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 登录与授权');