Refactor multi-flow mail capability boundaries
This commit is contained in:
+123
-5
@@ -61,21 +61,30 @@ importScripts(
|
|||||||
'content/activation-utils.js'
|
'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?.({
|
const PLUS_PAYPAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||||
|
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||||
plusModeEnabled: true,
|
plusModeEnabled: true,
|
||||||
plusPaymentMethod: 'paypal',
|
plusPaymentMethod: 'paypal',
|
||||||
}) || NORMAL_STEP_DEFINITIONS;
|
}) || NORMAL_STEP_DEFINITIONS;
|
||||||
const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||||
|
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||||
plusModeEnabled: true,
|
plusModeEnabled: true,
|
||||||
plusPaymentMethod: 'gopay',
|
plusPaymentMethod: 'gopay',
|
||||||
}) || PLUS_PAYPAL_STEP_DEFINITIONS;
|
}) || PLUS_PAYPAL_STEP_DEFINITIONS;
|
||||||
const PLUS_GPC_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
const PLUS_GPC_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||||
|
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||||
plusModeEnabled: true,
|
plusModeEnabled: true,
|
||||||
plusPaymentMethod: 'gpc-helper',
|
plusPaymentMethod: 'gpc-helper',
|
||||||
}) || PLUS_GOPAY_STEP_DEFINITIONS;
|
}) || PLUS_GOPAY_STEP_DEFINITIONS;
|
||||||
const PLUS_STEP_DEFINITIONS = PLUS_PAYPAL_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,
|
...NORMAL_STEP_DEFINITIONS,
|
||||||
...PLUS_PAYPAL_STEP_DEFINITIONS,
|
...PLUS_PAYPAL_STEP_DEFINITIONS,
|
||||||
...PLUS_GOPAY_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))
|
.map((definition) => Number(definition?.id))
|
||||||
.filter(Number.isFinite)))
|
.filter(Number.isFinite)))
|
||||||
.sort((left, right) => left - right);
|
.sort((left, right) => left - right);
|
||||||
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
|
|
||||||
const DEFAULT_STEP_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending']));
|
const DEFAULT_STEP_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending']));
|
||||||
const NORMAL_STEP_IDS = NORMAL_STEP_DEFINITIONS
|
const NORMAL_STEP_IDS = NORMAL_STEP_DEFINITIONS
|
||||||
.map((definition) => Number(definition?.id))
|
.map((definition) => Number(definition?.id))
|
||||||
@@ -597,11 +605,21 @@ function getSignupMethodForStepDefinitions(state = {}) {
|
|||||||
function getStepDefinitionsForState(state = {}) {
|
function getStepDefinitionsForState(state = {}) {
|
||||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||||
if (rootScope.MultiPageStepDefinitions?.getSteps) {
|
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),
|
plusModeEnabled: isPlusModeState(state),
|
||||||
plusPaymentMethod: normalizePlusPaymentMethod(state?.plusPaymentMethod),
|
plusPaymentMethod: normalizePlusPaymentMethod(state?.plusPaymentMethod),
|
||||||
signupMethod: getSignupMethodForStepDefinitions(state),
|
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)) {
|
if (!isPlusModeState(state)) {
|
||||||
return NORMAL_STEP_DEFINITIONS;
|
return NORMAL_STEP_DEFINITIONS;
|
||||||
@@ -633,10 +651,19 @@ function getStepIdsForState(state = {}) {
|
|||||||
|
|
||||||
function getLastStepIdForState(state = {}) {
|
function getLastStepIdForState(state = {}) {
|
||||||
const ids = getStepIdsForState(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 = {}) {
|
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;
|
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 = {}) {
|
function canUsePhoneSignup(state = {}) {
|
||||||
const capabilityState = typeof resolveCurrentFlowCapabilities === 'function'
|
const capabilityState = typeof resolveCurrentFlowCapabilities === 'function'
|
||||||
? resolveCurrentFlowCapabilities(state)
|
? resolveCurrentFlowCapabilities(state)
|
||||||
@@ -3029,6 +3088,30 @@ async function importSettingsBundle(configBundle) {
|
|||||||
fillDefaults: true,
|
fillDefaults: true,
|
||||||
requireKnownKeys: 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);
|
await setPersistentSettings(importedSettings);
|
||||||
|
|
||||||
@@ -4151,6 +4234,8 @@ async function requestHotmailLocalCode(account, pollPayload = {}) {
|
|||||||
top: 5,
|
top: 5,
|
||||||
senderFilters: pollPayload.senderFilters || [],
|
senderFilters: pollPayload.senderFilters || [],
|
||||||
subjectFilters: pollPayload.subjectFilters || [],
|
subjectFilters: pollPayload.subjectFilters || [],
|
||||||
|
requiredKeywords: pollPayload.requiredKeywords || [],
|
||||||
|
codePatterns: pollPayload.codePatterns || [],
|
||||||
excludeCodes: pollPayload.excludeCodes || [],
|
excludeCodes: pollPayload.excludeCodes || [],
|
||||||
filterAfterTimestamp: Number(pollPayload.filterAfterTimestamp || 0) || 0,
|
filterAfterTimestamp: Number(pollPayload.filterAfterTimestamp || 0) || 0,
|
||||||
}),
|
}),
|
||||||
@@ -4427,6 +4512,8 @@ async function pollHotmailVerificationCode(step, state, pollPayload = {}) {
|
|||||||
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
||||||
senderFilters: pollPayload.senderFilters || [],
|
senderFilters: pollPayload.senderFilters || [],
|
||||||
subjectFilters: pollPayload.subjectFilters || [],
|
subjectFilters: pollPayload.subjectFilters || [],
|
||||||
|
requiredKeywords: pollPayload.requiredKeywords || [],
|
||||||
|
codePatterns: pollPayload.codePatterns || [],
|
||||||
excludeCodes: pollPayload.excludeCodes || [],
|
excludeCodes: pollPayload.excludeCodes || [],
|
||||||
});
|
});
|
||||||
const match = matchResult.match;
|
const match = matchResult.match;
|
||||||
@@ -5609,6 +5696,8 @@ async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload
|
|||||||
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
||||||
senderFilters: pollPayload.senderFilters || [],
|
senderFilters: pollPayload.senderFilters || [],
|
||||||
subjectFilters: pollPayload.subjectFilters || [],
|
subjectFilters: pollPayload.subjectFilters || [],
|
||||||
|
requiredKeywords: pollPayload.requiredKeywords || [],
|
||||||
|
codePatterns: pollPayload.codePatterns || [],
|
||||||
excludeCodes: pollPayload.excludeCodes || [],
|
excludeCodes: pollPayload.excludeCodes || [],
|
||||||
});
|
});
|
||||||
const match = matchResult.match;
|
const match = matchResult.match;
|
||||||
@@ -8680,6 +8769,30 @@ async function launchAutoRunTimerPlan(trigger = 'alarm', options = {}) {
|
|||||||
return false;
|
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();
|
await clearAutoRunTimerAlarm();
|
||||||
if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) {
|
if (plan.autoRunSessionId && !isCurrentAutoRunSessionId(plan.autoRunSessionId)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -11814,6 +11927,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
|||||||
normalizeSignupMethod,
|
normalizeSignupMethod,
|
||||||
canUsePhoneSignup,
|
canUsePhoneSignup,
|
||||||
resolveSignupMethod,
|
resolveSignupMethod,
|
||||||
|
validateAutoRunStart: validateAutoRunStartState,
|
||||||
getTabId,
|
getTabId,
|
||||||
getStopRequested: () => stopRequested,
|
getStopRequested: () => stopRequested,
|
||||||
handleCloudflareSecurityBlocked,
|
handleCloudflareSecurityBlocked,
|
||||||
@@ -11902,6 +12016,10 @@ const plusGoPayStepRegistry = buildStepRegistry(PLUS_GOPAY_STEP_DEFINITIONS);
|
|||||||
const plusGpcStepRegistry = buildStepRegistry(PLUS_GPC_STEP_DEFINITIONS);
|
const plusGpcStepRegistry = buildStepRegistry(PLUS_GPC_STEP_DEFINITIONS);
|
||||||
|
|
||||||
function getStepRegistryForState(state = {}) {
|
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)) {
|
if (!isPlusModeState(state)) {
|
||||||
return normalStepRegistry;
|
return normalStepRegistry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,44 @@
|
|||||||
}
|
}
|
||||||
return method === 'phone' && canUsePhoneSignup(state) ? 'phone' : 'email';
|
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,
|
getTabId,
|
||||||
getStopRequested,
|
getStopRequested,
|
||||||
handleAutoRunLoopUnhandledError,
|
handleAutoRunLoopUnhandledError,
|
||||||
@@ -908,6 +946,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
|
const autoRunStartValidation = validateAutoRunStart(state, { state });
|
||||||
|
if (autoRunStartValidation?.ok === false) {
|
||||||
|
throw new Error(autoRunStartValidation.errors?.[0]?.message || '当前设置不支持启动自动流程。');
|
||||||
|
}
|
||||||
if (getPendingAutoRunTimerPlan(state)) {
|
if (getPendingAutoRunTimerPlan(state)) {
|
||||||
throw new Error('已有自动运行倒计时计划,请先取消或立即开始。');
|
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);
|
const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1);
|
||||||
return await scheduleAutoRun(totalRuns, {
|
return await scheduleAutoRun(totalRuns, {
|
||||||
delayMinutes: message.payload?.delayMinutes,
|
delayMinutes: message.payload?.delayMinutes,
|
||||||
@@ -1006,6 +1053,16 @@
|
|||||||
const currentState = await getState();
|
const currentState = await getState();
|
||||||
const updates = buildPersistentSettingsPayload(message.payload || {});
|
const updates = buildPersistentSettingsPayload(message.payload || {});
|
||||||
const sessionUpdates = buildLuckmailSessionSettingsPayload(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 = {
|
const nextSignupState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
...updates,
|
...updates,
|
||||||
@@ -1017,6 +1074,7 @@
|
|||||||
|| Object.prototype.hasOwnProperty.call(updates, 'signupMethod')
|
|| Object.prototype.hasOwnProperty.call(updates, 'signupMethod')
|
||||||
|| Object.prototype.hasOwnProperty.call(updates, 'panelMode')
|
|| Object.prototype.hasOwnProperty.call(updates, 'panelMode')
|
||||||
|| Object.prototype.hasOwnProperty.call(updates, 'activeFlowId')
|
|| Object.prototype.hasOwnProperty.call(updates, 'activeFlowId')
|
||||||
|
|| Object.prototype.hasOwnProperty.call(updates, 'contributionMode')
|
||||||
) {
|
) {
|
||||||
updates.signupMethod = resolveSignupMethod(nextSignupState);
|
updates.signupMethod = resolveSignupMethod(nextSignupState);
|
||||||
}
|
}
|
||||||
@@ -1117,7 +1175,12 @@
|
|||||||
);
|
);
|
||||||
await addLog(`Plus 支付方式已切换为 ${selectedPlusPaymentMethod},已更新对应的 Plus 步骤。`, 'info');
|
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': {
|
case 'REFRESH_GPC_CARD_BALANCE': {
|
||||||
|
|||||||
@@ -412,27 +412,22 @@
|
|||||||
if (typeof externalBuildVerificationPollPayload === 'function') {
|
if (typeof externalBuildVerificationPollPayload === 'function') {
|
||||||
return externalBuildVerificationPollPayload(step, state, overrides);
|
return externalBuildVerificationPollPayload(step, state, overrides);
|
||||||
}
|
}
|
||||||
|
const normalizedStep = Number(step) === 4 ? 4 : 8;
|
||||||
const is2925Provider = state?.mailProvider === '2925';
|
const is2925Provider = state?.mailProvider === '2925';
|
||||||
const mail2925MatchTargetEmail = is2925Provider
|
const mail2925MatchTargetEmail = is2925Provider
|
||||||
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive';
|
&& 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 {
|
return {
|
||||||
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(8, state),
|
flowId: String(state?.activeFlowId || '').trim(),
|
||||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
step: normalizedStep,
|
||||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(normalizedStep, state),
|
||||||
targetEmail: String(state?.step8VerificationTargetEmail || '').trim() || state.email,
|
senderFilters: [],
|
||||||
|
subjectFilters: [],
|
||||||
|
requiredKeywords: [],
|
||||||
|
codePatterns: [],
|
||||||
|
targetEmail: normalizedStep === 4
|
||||||
|
? state.email
|
||||||
|
: (String(state?.step8VerificationTargetEmail || '').trim() || state.email),
|
||||||
|
targetEmailHints: [],
|
||||||
mail2925MatchTargetEmail,
|
mail2925MatchTargetEmail,
|
||||||
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
|
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
|
||||||
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
|
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
|
||||||
|
|||||||
+47
-8
@@ -98,6 +98,39 @@ function getTargetEmailMatchState(text, targetEmail) {
|
|||||||
return { matches: false, hasExplicitEmail: false };
|
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 = {
|
const MONTH_INDEX_MAP = {
|
||||||
jan: 0,
|
jan: 0,
|
||||||
feb: 1,
|
feb: 1,
|
||||||
@@ -165,16 +198,17 @@ function parseGmailTimestampText(rawText) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractVerificationCode(text) {
|
function extractVerificationCode(text, options = {}) {
|
||||||
const normalized = String(text || '');
|
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);
|
const cnMatch = normalized.match(/(?:验证码|代码)[^0-9]{0,16}(\d{6})/i);
|
||||||
if (cnMatch) return cnMatch[1];
|
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);
|
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 (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);
|
|
||||||
if (enMatch) return enMatch[1];
|
if (enMatch) return enMatch[1];
|
||||||
|
|
||||||
const plainMatch = normalized.match(/\b(\d{6})\b/);
|
const plainMatch = normalized.match(/\b(\d{6})\b/);
|
||||||
@@ -357,7 +391,7 @@ function collectThreadRows() {
|
|||||||
if (
|
if (
|
||||||
row.matches('tr.zA')
|
row.matches('tr.zA')
|
||||||
|| row.querySelector('.bog, .y6, .y2, .afn, [data-thread-id], [data-legacy-thread-id], [data-legacy-last-message-id]')
|
|| 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);
|
rows.push(row);
|
||||||
}
|
}
|
||||||
@@ -545,6 +579,7 @@ async function openRowAndGetMessageText(row) {
|
|||||||
|
|
||||||
async function handlePollEmail(step, payload) {
|
async function handlePollEmail(step, payload) {
|
||||||
const {
|
const {
|
||||||
|
codePatterns = [],
|
||||||
senderFilters = [],
|
senderFilters = [],
|
||||||
subjectFilters = [],
|
subjectFilters = [],
|
||||||
maxAttempts = 5,
|
maxAttempts = 5,
|
||||||
@@ -618,7 +653,9 @@ async function handlePollEmail(step, payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previewTargetState = getTargetEmailMatchState(preview.combinedText, targetEmail);
|
const previewTargetState = getTargetEmailMatchState(preview.combinedText, targetEmail);
|
||||||
const previewCode = extractVerificationCode(preview.combinedText);
|
const previewCode = extractVerificationCode(preview.combinedText, {
|
||||||
|
codePatterns,
|
||||||
|
});
|
||||||
if (previewCode) {
|
if (previewCode) {
|
||||||
if (excludedCodeSet.has(previewCode)) {
|
if (excludedCodeSet.has(previewCode)) {
|
||||||
log(`步骤 ${step}:跳过排除的验证码:${previewCode}`, 'info');
|
log(`步骤 ${step}:跳过排除的验证码:${previewCode}`, 'info');
|
||||||
@@ -644,7 +681,9 @@ async function handlePollEmail(step, payload) {
|
|||||||
|
|
||||||
const openedText = await openRowAndGetMessageText(row);
|
const openedText = await openRowAndGetMessageText(row);
|
||||||
const openedTargetState = getTargetEmailMatchState(openedText, targetEmail);
|
const openedTargetState = getTargetEmailMatchState(openedText, targetEmail);
|
||||||
const bodyCode = extractVerificationCode(openedText);
|
const bodyCode = extractVerificationCode(openedText, {
|
||||||
|
codePatterns,
|
||||||
|
});
|
||||||
if (!bodyCode) {
|
if (!bodyCode) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-6
@@ -43,6 +43,39 @@ if (shouldHandlePollEmailInCurrentFrame) {
|
|||||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
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) {
|
function isVisibleElement(node) {
|
||||||
return Boolean(node instanceof HTMLElement)
|
return Boolean(node instanceof HTMLElement)
|
||||||
&& (Boolean(node.offsetParent) || getComputedStyle(node).position === 'fixed');
|
&& (Boolean(node.offsetParent) || getComputedStyle(node).position === 'fixed');
|
||||||
@@ -82,12 +115,15 @@ if (shouldHandlePollEmailInCurrentFrame) {
|
|||||||
].join('::')).slice(0, 240);
|
].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})/);
|
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||||
if (matchCn) return matchCn[1];
|
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);
|
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
if (matchLoginCode) return matchLoginCode[1];
|
||||||
|
|
||||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||||
if (matchEn) return matchEn[1] || matchEn[2];
|
if (matchEn) return matchEn[1] || matchEn[2];
|
||||||
@@ -276,7 +312,14 @@ if (shouldHandlePollEmailInCurrentFrame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handlePollEmail(step, payload) {
|
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 excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||||
const FALLBACK_AFTER = 3;
|
const FALLBACK_AFTER = 3;
|
||||||
const pollSessionKey = normalizePollSessionKey(payload);
|
const pollSessionKey = normalizePollSessionKey(payload);
|
||||||
@@ -327,7 +370,7 @@ if (shouldHandlePollEmailInCurrentFrame) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let code = extractVerificationCode(meta.combinedText);
|
let code = extractVerificationCode(meta.combinedText, { codePatterns });
|
||||||
let opened = null;
|
let opened = null;
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
@@ -339,7 +382,7 @@ if (shouldHandlePollEmailInCurrentFrame) {
|
|||||||
if (!openedSenderMatch && !openedSubjectMatch && !senderMatch && !subjectMatch) {
|
if (!openedSenderMatch && !openedSubjectMatch && !senderMatch && !subjectMatch) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
code = extractVerificationCode(opened.combinedText);
|
code = extractVerificationCode(opened.combinedText, { codePatterns });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
|
|||||||
@@ -60,12 +60,55 @@ function normalizeText(value) {
|
|||||||
return (value || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
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})/);
|
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||||
if (matchCn) return matchCn[1];
|
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);
|
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
if (matchLoginCode) return matchLoginCode[1];
|
||||||
|
|
||||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||||
if (matchEn) return matchEn[1] || matchEn[2];
|
if (matchEn) return matchEn[1] || matchEn[2];
|
||||||
@@ -76,7 +119,7 @@ function extractVerificationCode(text) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rowMatchesFilters(mail, senderFilters, subjectFilters, targetEmail) {
|
function rowMatchesFilters(mail, senderFilters, subjectFilters, targetEmail, options = {}) {
|
||||||
const sender = normalizeText(mail.sender);
|
const sender = normalizeText(mail.sender);
|
||||||
const subject = normalizeText(mail.subject);
|
const subject = normalizeText(mail.subject);
|
||||||
const mailbox = normalizeText(mail.mailbox);
|
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 subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()) || combined.includes(f.toLowerCase()));
|
||||||
const mailboxMatch = Boolean(targetLocal) && mailbox.includes(targetLocal);
|
const mailboxMatch = Boolean(targetLocal) && mailbox.includes(targetLocal);
|
||||||
const forwardedDuck = /duckduckgo|forward(?:ed)?\s*by/i.test(mail.combinedText);
|
const forwardedDuck = /duckduckgo|forward(?:ed)?\s*by/i.test(mail.combinedText);
|
||||||
const code = extractVerificationCode(mail.combinedText);
|
const code = extractVerificationCode(mail.combinedText, {
|
||||||
const keywordMatch = /openai|chatgpt|verify|verification|confirm|log-?in|验证码|代码/.test(combined);
|
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 (mailboxMatch) return { matched: true, mailboxMatch, code };
|
||||||
if (senderMatch || subjectMatch) return { matched: true, mailboxMatch: false, code };
|
if (senderMatch || subjectMatch) return { matched: true, mailboxMatch: false, code };
|
||||||
@@ -170,6 +217,8 @@ async function deleteCurrentMailboxMessage(step) {
|
|||||||
|
|
||||||
async function handleMailboxPollEmail(step, payload) {
|
async function handleMailboxPollEmail(step, payload) {
|
||||||
const {
|
const {
|
||||||
|
codePatterns = [],
|
||||||
|
requiredKeywords = [],
|
||||||
senderFilters = [],
|
senderFilters = [],
|
||||||
subjectFilters = [],
|
subjectFilters = [],
|
||||||
maxAttempts = 20,
|
maxAttempts = 20,
|
||||||
@@ -208,14 +257,19 @@ async function handleMailboxPollEmail(step, payload) {
|
|||||||
if (seenMailIds.has(mail.mailId)) continue;
|
if (seenMailIds.has(mail.mailId)) continue;
|
||||||
if (!useFallback && existingMailIds.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;
|
if (!match.matched) continue;
|
||||||
|
|
||||||
candidates.push({ ...mail, code: match.code });
|
candidates.push({ ...mail, code: match.code });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const mail of candidates) {
|
for (const mail of candidates) {
|
||||||
const code = mail.code || extractVerificationCode(mail.combinedText);
|
const code = mail.code || extractVerificationCode(mail.combinedText, {
|
||||||
|
codePatterns,
|
||||||
|
});
|
||||||
if (!code) continue;
|
if (!code) continue;
|
||||||
if (excludedCodeSet.has(code)) {
|
if (excludedCodeSet.has(code)) {
|
||||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
||||||
|
|||||||
+60
-12
@@ -73,6 +73,39 @@ function normalizeText(value) {
|
|||||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
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) {
|
function getNetEaseMailLabel(hostname) {
|
||||||
const currentHostname = String(
|
const currentHostname = String(
|
||||||
hostname || (typeof location !== 'undefined' ? location.hostname : '') || ''
|
hostname || (typeof location !== 'undefined' ? location.hostname : '') || ''
|
||||||
@@ -129,7 +162,7 @@ function isLikelyMailItemNode(node) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return /发件人|验证码|verification|chatgpt|openai|code|log-?in/i.test(summaryText);
|
return /发件人|验证码|verification|code|log-?in/i.test(summaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findMailItems() {
|
function findMailItems() {
|
||||||
@@ -406,7 +439,7 @@ function selectOpenedMailTextCandidate(item, candidates = [], options = {}) {
|
|||||||
if (sender && lower.includes(sender)) {
|
if (sender && lower.includes(sender)) {
|
||||||
return true;
|
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] || '';
|
}) || source[0] || '';
|
||||||
|
|
||||||
const filteredCandidates = candidates.filter((candidate) => !excludedSet.has(normalizeText(candidate)));
|
const filteredCandidates = candidates.filter((candidate) => !excludedSet.has(normalizeText(candidate)));
|
||||||
@@ -443,9 +476,11 @@ async function returnToInbox() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openMailAndGetMessageText(item) {
|
async function openMailAndGetMessageText(item, options = {}) {
|
||||||
const beforeCandidates = collectOpenedMailTextCandidates();
|
const beforeCandidates = collectOpenedMailTextCandidates();
|
||||||
const beforeText = selectOpenedMailTextCandidate(item, beforeCandidates);
|
const beforeText = selectOpenedMailTextCandidate(item, beforeCandidates, {
|
||||||
|
codePatterns: options.codePatterns,
|
||||||
|
});
|
||||||
if (typeof simulateClick === 'function') {
|
if (typeof simulateClick === 'function') {
|
||||||
simulateClick(item);
|
simulateClick(item);
|
||||||
} else {
|
} else {
|
||||||
@@ -456,6 +491,7 @@ async function openMailAndGetMessageText(item) {
|
|||||||
for (let i = 0; i < 24; i += 1) {
|
for (let i = 0; i < 24; i += 1) {
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
const candidate = readOpenedMailText(item, {
|
const candidate = readOpenedMailText(item, {
|
||||||
|
codePatterns: options.codePatterns,
|
||||||
excludedTexts: beforeCandidates,
|
excludedTexts: beforeCandidates,
|
||||||
allowExcludedFallback: false,
|
allowExcludedFallback: false,
|
||||||
});
|
});
|
||||||
@@ -463,7 +499,7 @@ async function openMailAndGetMessageText(item) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
openedText = candidate;
|
openedText = candidate;
|
||||||
if (extractVerificationCode(candidate)) {
|
if (extractVerificationCode(candidate, { codePatterns: options.codePatterns })) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (candidate !== beforeText && candidate.length > beforeText.length + 24) {
|
if (candidate !== beforeText && candidate.length > beforeText.length + 24) {
|
||||||
@@ -488,7 +524,15 @@ function scheduleEmailCleanup(item, step) {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function handlePollEmail(step, payload) {
|
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 excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||||
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
|
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
|
||||||
const mailLabel = getNetEaseMailLabel();
|
const mailLabel = getNetEaseMailLabel();
|
||||||
@@ -581,12 +625,12 @@ async function handlePollEmail(step, payload) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (senderMatch || subjectMatch) {
|
if (senderMatch || subjectMatch) {
|
||||||
let code = extractVerificationCode(combinedText);
|
let code = extractVerificationCode(combinedText, { codePatterns });
|
||||||
let codeSource = '邮件列表';
|
let codeSource = '邮件列表';
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
const openedText = await openMailAndGetMessageText(item);
|
const openedText = await openMailAndGetMessageText(item, { codePatterns });
|
||||||
code = extractVerificationCode(openedText);
|
code = extractVerificationCode(openedText, { codePatterns });
|
||||||
if (code) {
|
if (code) {
|
||||||
codeSource = '邮件正文';
|
codeSource = '邮件正文';
|
||||||
}
|
}
|
||||||
@@ -716,12 +760,16 @@ async function refreshInbox() {
|
|||||||
// Verification Code Extraction
|
// 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})/);
|
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||||
if (matchCn) return matchCn[1];
|
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);
|
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
if (matchLoginCode) return matchLoginCode[1];
|
||||||
|
|
||||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||||
if (matchEn) return matchEn[1] || matchEn[2];
|
if (matchEn) return matchEn[1] || matchEn[2];
|
||||||
|
|||||||
+90
-21
@@ -670,7 +670,56 @@ function matchesMailFilters(text, senderFilters, subjectFilters) {
|
|||||||
return senderMatch || subjectMatch;
|
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 normalized = String(text || '');
|
||||||
const patterns = [
|
const patterns = [
|
||||||
/your\s+(?:temporary\s+)?chatgpt\s+(?:(?:log-?in|login)\s+)?code\s+is[\s\S]{0,80}?(\d{6})/i,
|
/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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractVerificationCode(text, strictChatGPTCodeOnly = false) {
|
function extractVerificationCode(text, options = {}) {
|
||||||
const strictCode = extractStrictChatGPTVerificationCode(text);
|
const legacyStrictMode = typeof options === 'boolean' ? options : false;
|
||||||
if (strictChatGPTCodeOnly) {
|
const strictMode = legacyStrictMode || Boolean(options?.strictMode);
|
||||||
return strictCode;
|
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;
|
if (strictCode) return strictCode;
|
||||||
|
|
||||||
const normalized = String(text || '');
|
const normalized = String(text || '');
|
||||||
@@ -754,11 +808,8 @@ function extractVerificationCode(text, strictChatGPTCodeOnly = false) {
|
|||||||
const matchCn = normalized.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
const matchCn = normalized.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||||
if (matchCn) return matchCn[1];
|
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);
|
const matchLoginCode = normalized.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
if (matchLoginCode) return matchLoginCode[1];
|
||||||
|
|
||||||
const matchChatGPT = normalized.match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i);
|
|
||||||
if (matchChatGPT) return matchChatGPT[1];
|
|
||||||
|
|
||||||
const matchEn = normalized.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
const matchEn = normalized.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||||
if (matchEn) return matchEn[1] || matchEn[2];
|
if (matchEn) return matchEn[1] || matchEn[2];
|
||||||
@@ -771,9 +822,9 @@ function extractEmails(text = '') {
|
|||||||
return [...new Set(matches.map((item) => item.toLowerCase()))];
|
return [...new Set(matches.map((item) => item.toLowerCase()))];
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractForwardedTargetEmails(text = '') {
|
function extractForwardedTargetEmails(text = '', targetEmailHints = []) {
|
||||||
const normalizedText = String(text || '').toLowerCase();
|
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
|
const decoded = matches
|
||||||
.map((candidate) => {
|
.map((candidate) => {
|
||||||
const match = String(candidate || '').match(/bounce\+[a-z0-9._%+-]*-([a-z0-9._%+-]+)=([a-z0-9.-]+\.[a-z]{2,})@/i);
|
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()}`;
|
return `${match[1].toLowerCase()}@${match[2].toLowerCase()}`;
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.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) {
|
function emailMatchesTarget(candidate, targetEmail) {
|
||||||
@@ -796,19 +859,20 @@ function emailMatchesTarget(candidate, targetEmail) {
|
|||||||
return normalizedCandidate === normalizedTarget;
|
return normalizedCandidate === normalizedTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTargetEmailMatchState(text, targetEmail) {
|
function getTargetEmailMatchState(text, targetEmail, options = {}) {
|
||||||
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
|
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
|
||||||
if (!normalizedTarget) {
|
if (!normalizedTarget) {
|
||||||
return { matches: true, hasExplicitEmail: false };
|
return { matches: true, hasExplicitEmail: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedText = String(text || '').toLowerCase();
|
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 };
|
return { matches: true, hasExplicitEmail: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractedEmails = extractEmails(normalizedText);
|
const extractedEmails = extractEmails(normalizedText);
|
||||||
const forwardedTargetEmails = extractForwardedTargetEmails(normalizedText);
|
const forwardedTargetEmails = extractForwardedTargetEmails(normalizedText, targetEmailHints);
|
||||||
if (!extractedEmails.length) {
|
if (!extractedEmails.length) {
|
||||||
return forwardedTargetEmails.length
|
return forwardedTargetEmails.length
|
||||||
? {
|
? {
|
||||||
@@ -1175,14 +1239,15 @@ async function ensureMail2925Session(payload = {}) {
|
|||||||
async function handlePollEmail(step, payload) {
|
async function handlePollEmail(step, payload) {
|
||||||
await ensureSeenCodesSession(step, payload);
|
await ensureSeenCodesSession(step, payload);
|
||||||
const {
|
const {
|
||||||
|
codePatterns = [],
|
||||||
senderFilters,
|
senderFilters,
|
||||||
subjectFilters,
|
subjectFilters,
|
||||||
maxAttempts,
|
maxAttempts,
|
||||||
intervalMs,
|
intervalMs,
|
||||||
filterAfterTimestamp = 0,
|
filterAfterTimestamp = 0,
|
||||||
excludeCodes = [],
|
excludeCodes = [],
|
||||||
strictChatGPTCodeOnly = false,
|
|
||||||
targetEmail = '',
|
targetEmail = '',
|
||||||
|
targetEmailHints = [],
|
||||||
mail2925MatchTargetEmail = false,
|
mail2925MatchTargetEmail = false,
|
||||||
} = payload || {};
|
} = payload || {};
|
||||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||||
@@ -1249,21 +1314,25 @@ async function handlePollEmail(step, payload) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const previewTargetState = mail2925MatchTargetEmail
|
const previewTargetState = mail2925MatchTargetEmail
|
||||||
? getTargetEmailMatchState(previewText, targetEmail)
|
? getTargetEmailMatchState(previewText, targetEmail, { targetEmailHints })
|
||||||
: { matches: true, hasExplicitEmail: false };
|
: { matches: true, hasExplicitEmail: false };
|
||||||
if (mail2925MatchTargetEmail && previewTargetState.hasExplicitEmail && !previewTargetState.matches) {
|
if (mail2925MatchTargetEmail && previewTargetState.hasExplicitEmail && !previewTargetState.matches) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previewCode = extractVerificationCode(previewText, strictChatGPTCodeOnly);
|
const previewCode = extractVerificationCode(previewText, {
|
||||||
|
codePatterns,
|
||||||
|
});
|
||||||
const openedText = await openMailAndDeleteAfterRead(item, step);
|
const openedText = await openMailAndDeleteAfterRead(item, step);
|
||||||
const openedTargetState = mail2925MatchTargetEmail
|
const openedTargetState = mail2925MatchTargetEmail
|
||||||
? getTargetEmailMatchState(openedText, targetEmail)
|
? getTargetEmailMatchState(openedText, targetEmail, { targetEmailHints })
|
||||||
: { matches: true, hasExplicitEmail: false };
|
: { matches: true, hasExplicitEmail: false };
|
||||||
if (mail2925MatchTargetEmail && openedTargetState.hasExplicitEmail && !openedTargetState.matches) {
|
if (mail2925MatchTargetEmail && openedTargetState.hasExplicitEmail && !openedTargetState.matches) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const bodyCode = extractVerificationCode(openedText, strictChatGPTCodeOnly);
|
const bodyCode = extractVerificationCode(openedText, {
|
||||||
|
codePatterns,
|
||||||
|
});
|
||||||
const candidateCode = bodyCode || previewCode;
|
const candidateCode = bodyCode || previewCode;
|
||||||
|
|
||||||
if (!candidateCode) {
|
if (!candidateCode) {
|
||||||
|
|||||||
+50
-5
@@ -50,12 +50,52 @@ function getCurrentMailIds() {
|
|||||||
return ids;
|
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
|
// Email Polling
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function handlePollEmail(step, payload) {
|
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 excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||||
|
|
||||||
log(`步骤 ${step}:开始轮询邮箱(最多 ${maxAttempts} 次,每 ${intervalMs / 1000} 秒一次)`);
|
log(`步骤 ${step}:开始轮询邮箱(最多 ${maxAttempts} 次,每 ${intervalMs / 1000} 秒一次)`);
|
||||||
@@ -103,7 +143,9 @@ async function handlePollEmail(step, payload) {
|
|||||||
const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()));
|
const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()));
|
||||||
|
|
||||||
if (senderMatch || subjectMatch) {
|
if (senderMatch || subjectMatch) {
|
||||||
const code = extractVerificationCode(subject + ' ' + digest);
|
const code = extractVerificationCode(subject + ' ' + digest, {
|
||||||
|
codePatterns,
|
||||||
|
});
|
||||||
if (code) {
|
if (code) {
|
||||||
if (excludedCodeSet.has(code)) {
|
if (excludedCodeSet.has(code)) {
|
||||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
||||||
@@ -169,13 +211,16 @@ async function refreshInbox() {
|
|||||||
// Verification Code Extraction
|
// 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"
|
// Pattern 1: Chinese format "代码为 370794" or "验证码...370794"
|
||||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||||
if (matchCn) return matchCn[1];
|
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);
|
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
if (matchLoginCode) return matchLoginCode[1];
|
||||||
|
|
||||||
// Pattern 2: English format "code is 370794" or "code: 370794"
|
// 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);
|
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||||
|
|||||||
+90
-26
@@ -1,6 +1,7 @@
|
|||||||
(function attachStepDefinitions(root, factory) {
|
(function attachStepDefinitions(root, factory) {
|
||||||
root.MultiPageStepDefinitions = factory();
|
root.MultiPageStepDefinitions = factory();
|
||||||
})(typeof self !== 'undefined' ? self : globalThis, function createStepDefinitionsModule() {
|
})(typeof self !== 'undefined' ? self : globalThis, function createStepDefinitionsModule() {
|
||||||
|
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
|
||||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||||
@@ -88,11 +89,20 @@
|
|||||||
: SIGNUP_METHOD_EMAIL;
|
: 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 = {}) {
|
function getResolvedSignupMethod(options = {}) {
|
||||||
return normalizeSignupMethod(options?.resolvedSignupMethod || options?.signupMethod);
|
return normalizeSignupMethod(options?.resolvedSignupMethod || options?.signupMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModeStepDefinitions(options = {}) {
|
function getOpenAiModeStepDefinitions(options = {}) {
|
||||||
if (!isPlusModeEnabled(options)) {
|
if (!isPlusModeEnabled(options)) {
|
||||||
return NORMAL_STEP_DEFINITIONS;
|
return NORMAL_STEP_DEFINITIONS;
|
||||||
}
|
}
|
||||||
@@ -103,20 +113,20 @@
|
|||||||
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS;
|
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlusPaymentStepTitle(options = {}) {
|
function getOpenAiPlusPaymentStepTitle(options = {}) {
|
||||||
if (!isPlusModeEnabled(options)) {
|
if (!isPlusModeEnabled(options)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const paymentStep = getModeStepDefinitions({
|
const paymentStep = getOpenAiModeStepDefinitions({
|
||||||
...options,
|
...options,
|
||||||
plusModeEnabled: true,
|
plusModeEnabled: true,
|
||||||
}).find((step) => step.key === PLUS_PAYMENT_STEP_KEY);
|
}).find((step) => step.key === PLUS_PAYMENT_STEP_KEY);
|
||||||
return paymentStep?.title || '';
|
return paymentStep?.title || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResolvedStepTitle(step = {}, options = {}) {
|
function getOpenAiResolvedStepTitle(step = {}, options = {}) {
|
||||||
if (isPlusModeEnabled(options) && step.key === PLUS_PAYMENT_STEP_KEY) {
|
if (isPlusModeEnabled(options) && step.key === PLUS_PAYMENT_STEP_KEY) {
|
||||||
return getPlusPaymentStepTitle(options) || step.title;
|
return getOpenAiPlusPaymentStepTitle(options) || step.title;
|
||||||
}
|
}
|
||||||
const signupMethod = getResolvedSignupMethod(options);
|
const signupMethod = getResolvedSignupMethod(options);
|
||||||
if (signupMethod === SIGNUP_METHOD_PHONE && PHONE_SIGNUP_TITLE_OVERRIDES[step.key]) {
|
if (signupMethod === SIGNUP_METHOD_PHONE && PHONE_SIGNUP_TITLE_OVERRIDES[step.key]) {
|
||||||
@@ -125,37 +135,83 @@
|
|||||||
return step.title;
|
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) => ({
|
return steps.map((step) => ({
|
||||||
...step,
|
...step,
|
||||||
title: getResolvedStepTitle(step, options),
|
flowId,
|
||||||
|
title: builder?.resolveStepTitle ? builder.resolveStepTitle(step, options) : step.title,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSteps(options = {}) {
|
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() {
|
function getAllSteps(options = {}) {
|
||||||
const keyed = new Map();
|
const { flowId, builder } = getFlowDefinitionBuilder(options);
|
||||||
for (const step of [
|
if (!builder?.getAllSteps) {
|
||||||
...NORMAL_STEP_DEFINITIONS,
|
return [];
|
||||||
...PLUS_PAYPAL_STEP_DEFINITIONS,
|
|
||||||
...PLUS_GOPAY_STEP_DEFINITIONS,
|
|
||||||
...PLUS_GPC_STEP_DEFINITIONS,
|
|
||||||
]) {
|
|
||||||
keyed.set(`${step.id}:${step.key}`, step);
|
|
||||||
}
|
}
|
||||||
return cloneSteps(Array.from(keyed.values()).sort((left, right) => {
|
return cloneSteps(builder.getAllSteps(options), options, flowId);
|
||||||
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;
|
function getPlusPaymentStepTitle(options = {}) {
|
||||||
return left.id - right.id;
|
const { builder } = getFlowDefinitionBuilder(options);
|
||||||
}));
|
if (!builder?.getPlusPaymentStepTitle) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return builder.getPlusPaymentStepTitle(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStepIds(options = {}) {
|
function getStepIds(options = {}) {
|
||||||
return getModeStepDefinitions(options)
|
return getSteps(options)
|
||||||
.map((step) => Number(step.id))
|
.map((step) => Number(step.id))
|
||||||
.filter(Number.isFinite)
|
.filter(Number.isFinite)
|
||||||
.sort((left, right) => left - right);
|
.sort((left, right) => left - right);
|
||||||
@@ -168,11 +224,16 @@
|
|||||||
|
|
||||||
function getStepById(id, options = {}) {
|
function getStepById(id, options = {}) {
|
||||||
const numericId = Number(id);
|
const numericId = Number(id);
|
||||||
const match = getModeStepDefinitions(options).find((step) => step.id === numericId);
|
const { flowId, builder } = getFlowDefinitionBuilder(options);
|
||||||
return match ? cloneSteps([match], options)[0] : null;
|
if (!builder?.getModeStepDefinitions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = builder.getModeStepDefinitions(options).find((step) => step.id === numericId);
|
||||||
|
return match ? cloneSteps([match], options, flowId)[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
DEFAULT_ACTIVE_FLOW_ID,
|
||||||
STEP_DEFINITIONS: NORMAL_STEP_DEFINITIONS,
|
STEP_DEFINITIONS: NORMAL_STEP_DEFINITIONS,
|
||||||
NORMAL_STEP_DEFINITIONS,
|
NORMAL_STEP_DEFINITIONS,
|
||||||
PLUS_STEP_DEFINITIONS: PLUS_PAYPAL_STEP_DEFINITIONS,
|
PLUS_STEP_DEFINITIONS: PLUS_PAYPAL_STEP_DEFINITIONS,
|
||||||
@@ -184,10 +245,13 @@
|
|||||||
getAllSteps,
|
getAllSteps,
|
||||||
getLastStepId,
|
getLastStepId,
|
||||||
getPlusPaymentStepTitle,
|
getPlusPaymentStepTitle,
|
||||||
|
getRegisteredFlowIds,
|
||||||
getStepById,
|
getStepById,
|
||||||
getStepIds,
|
getStepIds,
|
||||||
getSteps,
|
getSteps,
|
||||||
|
hasFlow,
|
||||||
isPlusModeEnabled,
|
isPlusModeEnabled,
|
||||||
|
normalizeActiveFlowId,
|
||||||
normalizePlusPaymentMethod,
|
normalizePlusPaymentMethod,
|
||||||
normalizeSignupMethod,
|
normalizeSignupMethod,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,42 @@
|
|||||||
})(typeof self !== 'undefined' ? self : globalThis, function createOpenAiMailRulesModule() {
|
})(typeof self !== 'undefined' ? self : globalThis, function createOpenAiMailRulesModule() {
|
||||||
const SIGNUP_CODE_RULE_ID = 'openai-signup-code';
|
const SIGNUP_CODE_RULE_ID = 'openai-signup-code';
|
||||||
const LOGIN_CODE_RULE_ID = 'openai-login-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 = {}) {
|
function createOpenAiMailRules(deps = {}) {
|
||||||
const {
|
const {
|
||||||
@@ -24,24 +60,30 @@
|
|||||||
const normalizedStep = Number(step) === 4 ? 4 : 8;
|
const normalizedStep = Number(step) === 4 ? 4 : 8;
|
||||||
const mail2925Provider = isMail2925Provider(state);
|
const mail2925Provider = isMail2925Provider(state);
|
||||||
const signupStep = normalizedStep === 4;
|
const signupStep = normalizedStep === 4;
|
||||||
|
const targetEmail = signupStep
|
||||||
|
? state?.email
|
||||||
|
: (String(state?.step8VerificationTargetEmail || '').trim() || state?.email);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
flowId: 'openai',
|
flowId: 'openai',
|
||||||
ruleId: signupStep ? SIGNUP_CODE_RULE_ID : LOGIN_CODE_RULE_ID,
|
ruleId: signupStep ? SIGNUP_CODE_RULE_ID : LOGIN_CODE_RULE_ID,
|
||||||
step: normalizedStep,
|
step: normalizedStep,
|
||||||
artifactType: 'code',
|
artifactType: 'code',
|
||||||
|
codePatterns: OPENAI_CODE_PATTERNS,
|
||||||
filterAfterTimestamp: mail2925Provider
|
filterAfterTimestamp: mail2925Provider
|
||||||
? 0
|
? 0
|
||||||
: getHotmailVerificationRequestTimestamp(normalizedStep, state),
|
: getHotmailVerificationRequestTimestamp(normalizedStep, state),
|
||||||
|
requiredKeywords: signupStep
|
||||||
|
? OPENAI_REQUIRED_KEYWORDS
|
||||||
|
: [...OPENAI_REQUIRED_KEYWORDS, 'login'],
|
||||||
senderFilters: signupStep
|
senderFilters: signupStep
|
||||||
? ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward']
|
? ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward']
|
||||||
: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||||
subjectFilters: signupStep
|
subjectFilters: signupStep
|
||||||
? ['verify', 'verification', 'code', '验证码', 'confirm']
|
? ['verify', 'verification', 'code', '验证码', 'confirm']
|
||||||
: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
||||||
targetEmail: signupStep
|
targetEmail,
|
||||||
? state?.email
|
targetEmailHints: buildTargetEmailHints(targetEmail),
|
||||||
: (String(state?.step8VerificationTargetEmail || '').trim() || state?.email),
|
|
||||||
mail2925MatchTargetEmail: shouldMatchMail2925TargetEmail(state),
|
mail2925MatchTargetEmail: shouldMatchMail2925TargetEmail(state),
|
||||||
maxAttempts: mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
|
maxAttempts: mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
|
||||||
intervalMs: mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
|
intervalMs: mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
|
||||||
|
|||||||
+60
-13
@@ -32,13 +32,49 @@
|
|||||||
: HOTMAIL_SERVICE_MODE_LOCAL;
|
: 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 source = String(text || '');
|
||||||
|
const matchedByRule = extractCodeByRulePatterns(source, options?.codePatterns);
|
||||||
|
if (matchedByRule) return matchedByRule;
|
||||||
|
|
||||||
const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i);
|
const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i);
|
||||||
if (matchCn) return matchCn[1];
|
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);
|
const matchLoginCode = source.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
|
||||||
if (matchOpenAiLogin) return matchOpenAiLogin[1];
|
if (matchLoginCode) return matchLoginCode[1];
|
||||||
|
|
||||||
const matchEn = source.match(/code(?:\s+is|[\s:])+(\d{6})/i);
|
const matchEn = source.match(/code(?:\s+is|[\s:])+(\d{6})/i);
|
||||||
if (matchEn) return matchEn[1];
|
if (matchEn) return matchEn[1];
|
||||||
@@ -47,7 +83,7 @@
|
|||||||
return matchStandalone ? matchStandalone[1] : null;
|
return matchStandalone ? matchStandalone[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractVerificationCodeFromMessage(message = {}) {
|
function extractVerificationCodeFromMessage(message = {}, options = {}) {
|
||||||
const sender = firstNonEmptyString([
|
const sender = firstNonEmptyString([
|
||||||
message?.from?.emailAddress?.address,
|
message?.from?.emailAddress?.address,
|
||||||
message?.sender,
|
message?.sender,
|
||||||
@@ -55,7 +91,9 @@
|
|||||||
]);
|
]);
|
||||||
const subject = firstNonEmptyString([message?.subject]);
|
const subject = firstNonEmptyString([message?.subject]);
|
||||||
const preview = firstNonEmptyString([message?.bodyPreview, message?.preview, message?.text]);
|
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) {
|
function getLatestHotmailMessage(messages) {
|
||||||
@@ -138,6 +176,10 @@
|
|||||||
function messageMatchesFilters(message, filters = {}) {
|
function messageMatchesFilters(message, filters = {}) {
|
||||||
const senderFilters = (filters.senderFilters || []).map(normalizeText).filter(Boolean);
|
const senderFilters = (filters.senderFilters || []).map(normalizeText).filter(Boolean);
|
||||||
const subjectFilters = (filters.subjectFilters || []).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 afterTimestamp = normalizeTimestamp(filters.afterTimestamp);
|
||||||
const receivedAt = normalizeTimestamp(message?.receivedDateTime);
|
const receivedAt = normalizeTimestamp(message?.receivedDateTime);
|
||||||
if (afterTimestamp && receivedAt && receivedAt < afterTimestamp) {
|
if (afterTimestamp && receivedAt && receivedAt < afterTimestamp) {
|
||||||
@@ -148,20 +190,25 @@
|
|||||||
const subject = normalizeText(message?.subject);
|
const subject = normalizeText(message?.subject);
|
||||||
const preview = String(message?.bodyPreview || '');
|
const preview = String(message?.bodyPreview || '');
|
||||||
const combinedText = [subject, sender, preview].filter(Boolean).join(' ');
|
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));
|
const excludedCodes = new Set((filters.excludeCodes || []).filter(Boolean));
|
||||||
if (code && excludedCodes.has(code)) {
|
if (code && excludedCodes.has(code)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const senderMatch = senderFilters.length === 0
|
const senderMatch = hasSenderFilters
|
||||||
? true
|
? senderFilters.some((item) => sender.includes(item) || normalizeText(preview).includes(item))
|
||||||
: senderFilters.some((item) => sender.includes(item) || normalizeText(preview).includes(item));
|
: false;
|
||||||
const subjectMatch = subjectFilters.length === 0
|
const subjectMatch = hasSubjectFilters
|
||||||
? true
|
? subjectFilters.some((item) => subject.includes(item) || normalizeText(preview).includes(item))
|
||||||
: 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+64
-25
@@ -1,8 +1,4 @@
|
|||||||
(function attachMicrosoftEmailHelpers(globalScope) {
|
(function attachMicrosoftEmailHelpers(globalScope) {
|
||||||
const OPENAI_SENDER_PATTERNS = [
|
|
||||||
/openai\.com/i,
|
|
||||||
/auth0\.openai\.com/i,
|
|
||||||
];
|
|
||||||
const CODE_PATTERN = /\b(\d{6})\b/;
|
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_SCOPES = 'offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/User.Read';
|
||||||
const GRAPH_DEFAULT_SCOPE = 'https://graph.microsoft.com/.default';
|
const GRAPH_DEFAULT_SCOPE = 'https://graph.microsoft.com/.default';
|
||||||
@@ -179,6 +175,57 @@
|
|||||||
return String(value || '').trim().toLowerCase();
|
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) {
|
function getMessageSender(message) {
|
||||||
return String(
|
return String(
|
||||||
message?.from?.emailAddress?.address
|
message?.from?.emailAddress?.address
|
||||||
@@ -203,22 +250,13 @@
|
|||||||
.join('\n');
|
.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 = {}) {
|
function extractVerificationCodeFromMessages(messages, options = {}) {
|
||||||
const filterAfterTimestamp = Number(options.filterAfterTimestamp || 0) || 0;
|
const filterAfterTimestamp = Number(options.filterAfterTimestamp || 0) || 0;
|
||||||
const senderFilters = (options.senderFilters || []).map(normalizeFilterValue).filter(Boolean);
|
const senderFilters = (options.senderFilters || []).map(normalizeFilterValue).filter(Boolean);
|
||||||
const subjectFilters = (options.subjectFilters || []).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 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 : [])
|
const sortedMessages = (Array.isArray(messages) ? messages : [])
|
||||||
.map((raw) => normalizeMessage(raw, raw?.mailbox))
|
.map((raw) => normalizeMessage(raw, raw?.mailbox))
|
||||||
@@ -234,23 +272,23 @@
|
|||||||
const subject = normalizeFilterValue(message?.subject);
|
const subject = normalizeFilterValue(message?.subject);
|
||||||
const preview = normalizeFilterValue(message?.bodyPreview);
|
const preview = normalizeFilterValue(message?.bodyPreview);
|
||||||
const searchText = normalizeFilterValue(getMessageSearchText(message));
|
const searchText = normalizeFilterValue(getMessageSearchText(message));
|
||||||
const codeMatch = getMessageSearchText(message).match(CODE_PATTERN);
|
const code = extractVerificationCode(getMessageSearchText(message), {
|
||||||
const code = codeMatch?.[1] || '';
|
codePatterns: options.codePatterns,
|
||||||
|
});
|
||||||
if (!code || excludedCodes.has(code)) {
|
if (!code || excludedCodes.has(code)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasExplicitFilters && !isOpenAiMessage(message)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const senderMatched = senderFilters.length === 0
|
const senderMatched = senderFilters.length === 0
|
||||||
? true
|
? false
|
||||||
: senderFilters.some((filter) => sender.includes(filter) || preview.includes(filter) || searchText.includes(filter));
|
: senderFilters.some((filter) => sender.includes(filter) || preview.includes(filter) || searchText.includes(filter));
|
||||||
const subjectMatched = subjectFilters.length === 0
|
const subjectMatched = subjectFilters.length === 0
|
||||||
? true
|
? false
|
||||||
: subjectFilters.some((filter) => subject.includes(filter) || preview.includes(filter) || searchText.includes(filter));
|
: 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +439,8 @@
|
|||||||
filterAfterTimestamp,
|
filterAfterTimestamp,
|
||||||
senderFilters,
|
senderFilters,
|
||||||
subjectFilters,
|
subjectFilters,
|
||||||
|
requiredKeywords: options.requiredKeywords,
|
||||||
|
codePatterns: options.codePatterns,
|
||||||
excludeCodes,
|
excludeCodes,
|
||||||
});
|
});
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -437,7 +477,6 @@
|
|||||||
fetchOutlookMessages,
|
fetchOutlookMessages,
|
||||||
getMessageSender,
|
getMessageSender,
|
||||||
getMessageTimestamp,
|
getMessageTimestamp,
|
||||||
isOpenAiMessage,
|
|
||||||
normalizeMailboxId,
|
normalizeMailboxId,
|
||||||
normalizeMailboxLabel,
|
normalizeMailboxLabel,
|
||||||
normalizeMessage,
|
normalizeMessage,
|
||||||
|
|||||||
+38
-44
@@ -122,12 +122,6 @@ def compact_text(value, limit=400):
|
|||||||
def log_info(message):
|
def log_info(message):
|
||||||
print(f"[HotmailHelper] {message}", flush=True)
|
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):
|
def get_message_body_content(message):
|
||||||
body = message.get("body") or {}
|
body = message.get("body") or {}
|
||||||
if not isinstance(body, dict):
|
if not isinstance(body, dict):
|
||||||
@@ -135,36 +129,6 @@ def get_message_body_content(message):
|
|||||||
return str(body.get("content") or "").strip()
|
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():
|
def get_proxy_debug_context():
|
||||||
names = ["all_proxy", "http_proxy", "https_proxy", "ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY"]
|
names = ["all_proxy", "http_proxy", "https_proxy", "ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY"]
|
||||||
parts = []
|
parts = []
|
||||||
@@ -725,7 +689,6 @@ def collect_messages(email_addr, client_id, refresh_token, mailboxes, top):
|
|||||||
try:
|
try:
|
||||||
log_info(f"message collection start transport={transport_name}")
|
log_info(f"message collection start transport={transport_name}")
|
||||||
result = collector(email_addr, client_id, refresh_token, mailboxes, top)
|
result = collector(email_addr, client_id, refresh_token, mailboxes, top)
|
||||||
log_openai_messages(result.get("messages") or [], transport=transport_name)
|
|
||||||
log_info(
|
log_info(
|
||||||
f"message collection success transport={transport_name} "
|
f"message collection success transport={transport_name} "
|
||||||
f"tokenEndpoint={result['token_payload'].get('token_endpoint', '')}"
|
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)}")
|
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 "")
|
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 = [
|
patterns = [
|
||||||
r"(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})",
|
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"code(?:\s+is|[\s:])+(\d{6})",
|
||||||
r"\b(\d{6})\b",
|
r"\b(\d{6})\b",
|
||||||
]
|
]
|
||||||
@@ -753,9 +743,10 @@ def extract_code(text):
|
|||||||
return ""
|
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()]
|
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()]
|
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()}
|
excluded = {str(item).strip() for item in exclude_codes or [] if str(item).strip()}
|
||||||
|
|
||||||
def match_message(message, apply_time_filter):
|
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", ""))
|
subject = str(message.get("subject", ""))
|
||||||
preview = str(message.get("bodyPreview", ""))
|
preview = str(message.get("bodyPreview", ""))
|
||||||
combined = " ".join([sender, subject.lower(), preview.lower()])
|
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:
|
if not code:
|
||||||
body_content = get_message_body_content(message)
|
body_content = get_message_body_content(message)
|
||||||
if body_content:
|
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:
|
if not code or code in excluded:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
sender_ok = not sender_keywords or any(keyword in combined for keyword in sender_keywords)
|
sender_ok = bool(sender_keywords) and any(keyword in combined for keyword in sender_keywords)
|
||||||
subject_ok = not subject_keywords or any(keyword in combined for keyword in subject_keywords)
|
subject_ok = bool(subject_keywords) and any(keyword in combined for keyword in subject_keywords)
|
||||||
if not sender_ok and not subject_ok:
|
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 None
|
||||||
|
|
||||||
return {"code": code, "message": message}
|
return {"code": code, "message": message}
|
||||||
@@ -862,6 +854,8 @@ class HotmailHelperHandler(BaseHTTPRequestHandler):
|
|||||||
payload.get("subjectFilters") or [],
|
payload.get("subjectFilters") or [],
|
||||||
payload.get("excludeCodes") or [],
|
payload.get("excludeCodes") or [],
|
||||||
int(payload.get("filterAfterTimestamp") or 0),
|
int(payload.get("filterAfterTimestamp") or 0),
|
||||||
|
payload.get("requiredKeywords") or [],
|
||||||
|
payload.get("codePatterns") or [],
|
||||||
)
|
)
|
||||||
json_response(self, 200, {
|
json_response(self, 200, {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
|
|||||||
@@ -38,6 +38,14 @@
|
|||||||
supportsPhoneSignup: true,
|
supportsPhoneSignup: true,
|
||||||
requiresPhoneSignupWarning: false,
|
requiresPhoneSignupWarning: false,
|
||||||
});
|
});
|
||||||
|
const MODE_SWITCH_RELEVANT_KEYS = Object.freeze([
|
||||||
|
'activeFlowId',
|
||||||
|
'contributionMode',
|
||||||
|
'panelMode',
|
||||||
|
'phoneVerificationEnabled',
|
||||||
|
'plusModeEnabled',
|
||||||
|
'signupMethod',
|
||||||
|
]);
|
||||||
|
|
||||||
const PANEL_CAPABILITIES = Object.freeze({
|
const PANEL_CAPABILITIES = Object.freeze({
|
||||||
cpa: Object.freeze({
|
cpa: Object.freeze({
|
||||||
@@ -95,6 +103,17 @@
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPanelModeLabel(panelMode = '') {
|
||||||
|
const normalized = normalizePanelMode(panelMode);
|
||||||
|
if (normalized === 'sub2api') {
|
||||||
|
return 'SUB2API';
|
||||||
|
}
|
||||||
|
if (normalized === 'codex2api') {
|
||||||
|
return 'Codex2API';
|
||||||
|
}
|
||||||
|
return 'CPA';
|
||||||
|
}
|
||||||
|
|
||||||
function createFlowCapabilityRegistry(deps = {}) {
|
function createFlowCapabilityRegistry(deps = {}) {
|
||||||
const {
|
const {
|
||||||
defaultFlowCapabilities = DEFAULT_FLOW_CAPABILITIES,
|
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 = {}) {
|
function resolveSidepanelCapabilities(options = {}) {
|
||||||
const state = options?.state || {};
|
const state = options?.state || {};
|
||||||
const activeFlowId = normalizeFlowId(
|
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 = {}) {
|
function canUsePhoneSignup(state = {}) {
|
||||||
return resolveSidepanelCapabilities({ state }).canUsePhoneSignup;
|
return resolveSidepanelCapabilities({ state }).canUsePhoneSignup;
|
||||||
}
|
}
|
||||||
@@ -222,6 +415,8 @@
|
|||||||
normalizeSignupMethod,
|
normalizeSignupMethod,
|
||||||
resolveSidepanelCapabilities,
|
resolveSidepanelCapabilities,
|
||||||
resolveSignupMethod,
|
resolveSignupMethod,
|
||||||
|
validateAutoRunStart,
|
||||||
|
validateModeSwitch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -789,6 +789,7 @@ function initPhoneVerificationSectionExpandedState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
|
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 defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
|
||||||
const rawPaymentMethod = typeof options === 'string'
|
const rawPaymentMethod = typeof options === 'string'
|
||||||
? options
|
? options
|
||||||
@@ -796,7 +797,11 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
|
|||||||
const rawSignupMethod = typeof options === 'string'
|
const rawSignupMethod = typeof options === 'string'
|
||||||
? currentSignupMethod
|
? currentSignupMethod
|
||||||
: (options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
|
: (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?.({
|
return (window.MultiPageStepDefinitions?.getSteps?.({
|
||||||
|
activeFlowId: String(activeFlowId || '').trim().toLowerCase() || defaultFlowId,
|
||||||
plusModeEnabled,
|
plusModeEnabled,
|
||||||
plusPaymentMethod: normalizePlusPaymentMethod(rawPaymentMethod),
|
plusPaymentMethod: normalizePlusPaymentMethod(rawPaymentMethod),
|
||||||
signupMethod: normalizeSignupMethod(rawSignupMethod),
|
signupMethod: normalizeSignupMethod(rawSignupMethod),
|
||||||
@@ -830,6 +835,7 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
|
|||||||
currentPlusPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod);
|
currentPlusPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod);
|
||||||
currentSignupMethod = normalizeSignupMethod(rawSignupMethod);
|
currentSignupMethod = normalizeSignupMethod(rawSignupMethod);
|
||||||
stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled, {
|
stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled, {
|
||||||
|
activeFlowId: options?.activeFlowId,
|
||||||
plusPaymentMethod: currentPlusPaymentMethod,
|
plusPaymentMethod: currentPlusPaymentMethod,
|
||||||
signupMethod: currentSignupMethod,
|
signupMethod: currentSignupMethod,
|
||||||
});
|
});
|
||||||
@@ -8524,6 +8530,7 @@ function renderStepsList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOrOptions = {}, maybeOptions = {}) {
|
function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOrOptions = {}, maybeOptions = {}) {
|
||||||
|
const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID !== 'undefined' ? DEFAULT_ACTIVE_FLOW_ID : 'openai';
|
||||||
const nextPlusModeEnabled = Boolean(plusModeEnabled);
|
const nextPlusModeEnabled = Boolean(plusModeEnabled);
|
||||||
const options = typeof plusPaymentMethodOrOptions === 'string'
|
const options = typeof plusPaymentMethodOrOptions === 'string'
|
||||||
? maybeOptions
|
? maybeOptions
|
||||||
@@ -8533,9 +8540,15 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
|
|||||||
: (options.plusPaymentMethod || getSelectedPlusPaymentMethod(latestState));
|
: (options.plusPaymentMethod || getSelectedPlusPaymentMethod(latestState));
|
||||||
const nextSignupMethod = normalizeSignupMethod(options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
|
const nextSignupMethod = normalizeSignupMethod(options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
|
||||||
const nextPaymentMethod = normalizePlusPaymentMethod(rawPaymentMethod);
|
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 rootScope = typeof window !== 'undefined' ? window : globalThis;
|
||||||
const currentPaymentStep = stepDefinitions.find((step) => step.key === 'paypal-approve');
|
const currentPaymentStep = stepDefinitions.find((step) => step.key === 'paypal-approve');
|
||||||
const nextPaymentTitle = rootScope.MultiPageStepDefinitions?.getPlusPaymentStepTitle?.({
|
const nextPaymentTitle = rootScope.MultiPageStepDefinitions?.getPlusPaymentStepTitle?.({
|
||||||
|
activeFlowId: nextActiveFlowId,
|
||||||
plusModeEnabled: nextPlusModeEnabled,
|
plusModeEnabled: nextPlusModeEnabled,
|
||||||
plusPaymentMethod: nextPaymentMethod,
|
plusPaymentMethod: nextPaymentMethod,
|
||||||
signupMethod: nextSignupMethod,
|
signupMethod: nextSignupMethod,
|
||||||
@@ -8551,6 +8564,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethodOr
|
|||||||
}
|
}
|
||||||
|
|
||||||
rebuildStepDefinitionState(nextPlusModeEnabled, {
|
rebuildStepDefinitionState(nextPlusModeEnabled, {
|
||||||
|
activeFlowId: nextActiveFlowId,
|
||||||
plusPaymentMethod: nextPaymentMethod,
|
plusPaymentMethod: nextPaymentMethod,
|
||||||
signupMethod: nextSignupMethod,
|
signupMethod: nextSignupMethod,
|
||||||
});
|
});
|
||||||
@@ -11834,6 +11848,37 @@ async function startAutoRunFromCurrentSettings() {
|
|||||||
if (typeof persistCurrentSettingsForAction === 'function') {
|
if (typeof persistCurrentSettingsForAction === 'function') {
|
||||||
await persistCurrentSettingsForAction();
|
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())) {
|
if (!(await ensureGpcApiKeyReadyForStart())) {
|
||||||
clearPendingAutoRunStartRunCount();
|
clearPendingAutoRunStartRunCount();
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -145,3 +145,109 @@ return {
|
|||||||
assert.equal(snapshot.autoRunTotalRuns, 1);
|
assert.equal(snapshot.autoRunTotalRuns, 1);
|
||||||
assert.equal(snapshot.autoRunAttemptRun, 0);
|
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', () => {
|
test('account run history snapshot sync is disabled in contribution mode', () => {
|
||||||
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
|
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
|
||||||
const globalScope = {};
|
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',
|
ruleId: 'openai-signup-code',
|
||||||
step: 4,
|
step: 4,
|
||||||
artifactType: 'code',
|
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,
|
filterAfterTimestamp: 0,
|
||||||
|
requiredKeywords: ['openai', 'chatgpt', 'verify', 'verification', 'confirm', '验证码', '代码'],
|
||||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
||||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
|
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
|
||||||
targetEmail: 'user@example.com',
|
targetEmail: 'user@example.com',
|
||||||
|
targetEmailHints: ['user@example.com', 'user=example.com'],
|
||||||
mail2925MatchTargetEmail: true,
|
mail2925MatchTargetEmail: true,
|
||||||
maxAttempts: 15,
|
maxAttempts: 15,
|
||||||
intervalMs: 15000,
|
intervalMs: 15000,
|
||||||
@@ -64,10 +80,26 @@ test('mail rule registry exposes canonical OpenAI verification poll payloads', (
|
|||||||
ruleId: 'openai-login-code',
|
ruleId: 'openai-login-code',
|
||||||
step: 8,
|
step: 8,
|
||||||
artifactType: 'code',
|
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,
|
filterAfterTimestamp: 456,
|
||||||
|
requiredKeywords: ['openai', 'chatgpt', 'verify', 'verification', 'confirm', '验证码', '代码', 'login'],
|
||||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||||
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
|
||||||
targetEmail: 'login@example.com',
|
targetEmail: 'login@example.com',
|
||||||
|
targetEmailHints: ['login@example.com', 'login=example.com'],
|
||||||
mail2925MatchTargetEmail: false,
|
mail2925MatchTargetEmail: false,
|
||||||
maxAttempts: 5,
|
maxAttempts: 5,
|
||||||
intervalMs: 3000,
|
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.panelMode, 'cpa');
|
||||||
assert.equal(state.signupMethod, 'email');
|
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(), [{
|
assert.deepEqual(api.getCaptured(), [{
|
||||||
|
activeFlowId: 'openai',
|
||||||
plusModeEnabled: true,
|
plusModeEnabled: true,
|
||||||
plusPaymentMethod: 'gopay',
|
plusPaymentMethod: 'gopay',
|
||||||
signupMethod: 'phone',
|
signupMethod: 'phone',
|
||||||
|
|||||||
@@ -70,3 +70,91 @@ test('flow capability registry defaults unknown flows to minimal non-phone capab
|
|||||||
assert.equal(capabilityState.panelMode, 'codex2api');
|
assert.equal(capabilityState.panelMode, 'codex2api');
|
||||||
assert.deepEqual(capabilityState.supportedPanelModes, []);
|
assert.deepEqual(capabilityState.supportedPanelModes, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('flow capability registry exposes shared auto-run validation for phone locks and panel support', () => {
|
||||||
|
const api = loadApi();
|
||||||
|
const registry = api.createFlowCapabilityRegistry({
|
||||||
|
flowCapabilities: {
|
||||||
|
openai: api.FLOW_CAPABILITIES.openai,
|
||||||
|
'site-a': {
|
||||||
|
...api.DEFAULT_FLOW_CAPABILITIES,
|
||||||
|
supportsPlatformBinding: ['cpa'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const plusLockedResult = registry.validateAutoRunStart({
|
||||||
|
state: {
|
||||||
|
activeFlowId: 'openai',
|
||||||
|
panelMode: 'cpa',
|
||||||
|
signupMethod: 'phone',
|
||||||
|
phoneVerificationEnabled: true,
|
||||||
|
plusModeEnabled: true,
|
||||||
|
contributionMode: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(plusLockedResult.ok, false);
|
||||||
|
assert.equal(plusLockedResult.errors[0].code, 'phone_signup_plus_mode_locked');
|
||||||
|
|
||||||
|
const unsupportedPanelResult = registry.validateAutoRunStart({
|
||||||
|
state: {
|
||||||
|
activeFlowId: 'site-a',
|
||||||
|
panelMode: 'sub2api',
|
||||||
|
signupMethod: 'email',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(unsupportedPanelResult.ok, false);
|
||||||
|
assert.equal(unsupportedPanelResult.errors[0].code, 'panel_mode_unsupported');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('flow capability registry normalizes unsupported mode switches back to the effective capability set', () => {
|
||||||
|
const api = loadApi();
|
||||||
|
const registry = api.createFlowCapabilityRegistry({
|
||||||
|
flowCapabilities: {
|
||||||
|
openai: api.FLOW_CAPABILITIES.openai,
|
||||||
|
'site-a': {
|
||||||
|
...api.DEFAULT_FLOW_CAPABILITIES,
|
||||||
|
supportsPlatformBinding: ['cpa'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const validation = registry.validateModeSwitch({
|
||||||
|
state: {
|
||||||
|
activeFlowId: 'site-a',
|
||||||
|
panelMode: 'sub2api',
|
||||||
|
signupMethod: 'phone',
|
||||||
|
phoneVerificationEnabled: true,
|
||||||
|
plusModeEnabled: true,
|
||||||
|
contributionMode: true,
|
||||||
|
},
|
||||||
|
changedKeys: [
|
||||||
|
'panelMode',
|
||||||
|
'signupMethod',
|
||||||
|
'phoneVerificationEnabled',
|
||||||
|
'plusModeEnabled',
|
||||||
|
'contributionMode',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(validation.ok, false);
|
||||||
|
assert.deepEqual(validation.normalizedUpdates, {
|
||||||
|
panelMode: 'cpa',
|
||||||
|
signupMethod: 'email',
|
||||||
|
phoneVerificationEnabled: false,
|
||||||
|
plusModeEnabled: false,
|
||||||
|
contributionMode: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
validation.errors.map((entry) => entry.code),
|
||||||
|
[
|
||||||
|
'panel_mode_unsupported',
|
||||||
|
'plus_mode_unsupported',
|
||||||
|
'contribution_mode_unsupported',
|
||||||
|
'phone_verification_unsupported',
|
||||||
|
'phone_signup_flow_unsupported',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -185,6 +185,15 @@ test('extractVerificationCode returns first six-digit code from multilingual mai
|
|||||||
assert.equal(extractVerificationCode('No code here'), null);
|
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', () => {
|
test('extractVerificationCodeFromMessage reads code from the latest message subject or preview', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
extractVerificationCodeFromMessage({
|
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', () => {
|
test('getHotmailListToggleLabel reflects expanded state and account count', () => {
|
||||||
assert.equal(getHotmailListToggleLabel(false, 0), '展开列表');
|
assert.equal(getHotmailListToggleLabel(false, 0), '展开列表');
|
||||||
assert.equal(getHotmailListToggleLabel(false, 7), '展开列表(7)');
|
assert.equal(getHotmailListToggleLabel(false, 7), '展开列表(7)');
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ function extractFunction(name) {
|
|||||||
test('readOpenedMailBody falls back to thread detail pane and extracts verification code', () => {
|
test('readOpenedMailBody falls back to thread detail pane and extracts verification code', () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
extractFunction('normalizeText'),
|
extractFunction('normalizeText'),
|
||||||
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
extractFunction('extractVerificationCode'),
|
extractFunction('extractVerificationCode'),
|
||||||
extractFunction('getOpenedMailBodyRoot'),
|
extractFunction('getOpenedMailBodyRoot'),
|
||||||
extractFunction('readOpenedMailBody'),
|
extractFunction('readOpenedMailBody'),
|
||||||
@@ -83,6 +85,8 @@ return { readOpenedMailBody, extractVerificationCode };
|
|||||||
|
|
||||||
test('extractVerificationCode matches the new suspicious log-in mail body', () => {
|
test('extractVerificationCode matches the new suspicious log-in mail body', () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
extractFunction('extractVerificationCode'),
|
extractFunction('extractVerificationCode'),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
@@ -95,6 +99,27 @@ return { extractVerificationCode };
|
|||||||
assert.equal(api.extractVerificationCode(bodyText), '982219');
|
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', () => {
|
test('readOpenedMailBody ignores conversation list rows when no detail pane is open', () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
extractFunction('normalizeText'),
|
extractFunction('normalizeText'),
|
||||||
@@ -199,6 +224,8 @@ test('icloud poll session baseline is reused across calls and enables fallback a
|
|||||||
extractFunction('normalizeText'),
|
extractFunction('normalizeText'),
|
||||||
extractFunction('getThreadItemMetadata'),
|
extractFunction('getThreadItemMetadata'),
|
||||||
extractFunction('buildItemSignature'),
|
extractFunction('buildItemSignature'),
|
||||||
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
extractFunction('extractVerificationCode'),
|
extractFunction('extractVerificationCode'),
|
||||||
extractFunction('normalizePollSessionKey'),
|
extractFunction('normalizePollSessionKey'),
|
||||||
extractFunction('getOrCreatePollSessionBaseline'),
|
extractFunction('getOrCreatePollSessionBaseline'),
|
||||||
@@ -300,6 +327,8 @@ test('icloud step8 polling finds a visible first-row code immediately', async ()
|
|||||||
extractFunction('normalizeText'),
|
extractFunction('normalizeText'),
|
||||||
extractFunction('getThreadItemMetadata'),
|
extractFunction('getThreadItemMetadata'),
|
||||||
extractFunction('buildItemSignature'),
|
extractFunction('buildItemSignature'),
|
||||||
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
extractFunction('extractVerificationCode'),
|
extractFunction('extractVerificationCode'),
|
||||||
extractFunction('normalizePollSessionKey'),
|
extractFunction('normalizePollSessionKey'),
|
||||||
extractFunction('getOrCreatePollSessionBaseline'),
|
extractFunction('getOrCreatePollSessionBaseline'),
|
||||||
@@ -378,6 +407,8 @@ test('icloud step8 visible first-row code still respects excluded codes', async
|
|||||||
extractFunction('normalizeText'),
|
extractFunction('normalizeText'),
|
||||||
extractFunction('getThreadItemMetadata'),
|
extractFunction('getThreadItemMetadata'),
|
||||||
extractFunction('buildItemSignature'),
|
extractFunction('buildItemSignature'),
|
||||||
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
extractFunction('extractVerificationCode'),
|
extractFunction('extractVerificationCode'),
|
||||||
extractFunction('normalizePollSessionKey'),
|
extractFunction('normalizePollSessionKey'),
|
||||||
extractFunction('getOrCreatePollSessionBaseline'),
|
extractFunction('getOrCreatePollSessionBaseline'),
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ test('readOpenedMailText prefers opened body content that contains the verificat
|
|||||||
extractFunction('collectOpenedMailTextCandidates'),
|
extractFunction('collectOpenedMailTextCandidates'),
|
||||||
extractFunction('selectOpenedMailTextCandidate'),
|
extractFunction('selectOpenedMailTextCandidate'),
|
||||||
extractFunction('readOpenedMailText'),
|
extractFunction('readOpenedMailText'),
|
||||||
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
extractFunction('extractVerificationCode'),
|
extractFunction('extractVerificationCode'),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
@@ -216,6 +218,8 @@ test('openMailAndGetMessageText reads opened body text and returns to inbox', as
|
|||||||
extractFunction('readOpenedMailText'),
|
extractFunction('readOpenedMailText'),
|
||||||
extractFunction('returnToInbox'),
|
extractFunction('returnToInbox'),
|
||||||
extractFunction('openMailAndGetMessageText'),
|
extractFunction('openMailAndGetMessageText'),
|
||||||
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
extractFunction('extractVerificationCode'),
|
extractFunction('extractVerificationCode'),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
@@ -289,6 +293,8 @@ test('openMailAndGetMessageText ignores stale pre-open text that contains an old
|
|||||||
extractFunction('readOpenedMailText'),
|
extractFunction('readOpenedMailText'),
|
||||||
extractFunction('returnToInbox'),
|
extractFunction('returnToInbox'),
|
||||||
extractFunction('openMailAndGetMessageText'),
|
extractFunction('openMailAndGetMessageText'),
|
||||||
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
extractFunction('extractVerificationCode'),
|
extractFunction('extractVerificationCode'),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
@@ -359,6 +365,8 @@ return { openMailAndGetMessageText, mailItem };
|
|||||||
|
|
||||||
test('extractVerificationCode matches the new suspicious log-in mail body', () => {
|
test('extractVerificationCode matches the new suspicious log-in mail body', () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
extractFunction('extractVerificationCode'),
|
extractFunction('extractVerificationCode'),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
@@ -371,6 +379,27 @@ return { extractVerificationCode };
|
|||||||
assert.equal(api.extractVerificationCode(bodyText), '982219');
|
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 () => {
|
test('handlePollEmail ignores same-minute old snapshot mail before fallback', async () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
extractFunction('normalizeText'),
|
extractFunction('normalizeText'),
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ return {
|
|||||||
test('handlePollEmail skips explicit mismatched target emails when receive-mode matching is enabled', async () => {
|
test('handlePollEmail skips explicit mismatched target emails when receive-mode matching is enabled', async () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
extractFunction('extractEmails'),
|
extractFunction('extractEmails'),
|
||||||
|
extractFunction('normalizeTargetEmailHints'),
|
||||||
extractFunction('extractForwardedTargetEmails'),
|
extractFunction('extractForwardedTargetEmails'),
|
||||||
extractFunction('emailMatchesTarget'),
|
extractFunction('emailMatchesTarget'),
|
||||||
extractFunction('getTargetEmailMatchState'),
|
extractFunction('getTargetEmailMatchState'),
|
||||||
@@ -407,6 +408,34 @@ return {
|
|||||||
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-2']);
|
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 () => {
|
test('handlePollEmail only accepts 2925 mails inside the fixed lookback window', async () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
extractFunction('normalizeMinuteTimestamp'),
|
extractFunction('normalizeMinuteTimestamp'),
|
||||||
@@ -549,7 +578,9 @@ return {
|
|||||||
|
|
||||||
test('extractVerificationCode strict mode matches the new suspicious log-in mail body', () => {
|
test('extractVerificationCode strict mode matches the new suspicious log-in mail body', () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
extractFunction('extractStrictChatGPTVerificationCode'),
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
|
extractFunction('extractLegacyStrictVerificationCode'),
|
||||||
extractFunction('isLikelyCompactTimeValue'),
|
extractFunction('isLikelyCompactTimeValue'),
|
||||||
extractFunction('isLikelyHeaderTimestampCode'),
|
extractFunction('isLikelyHeaderTimestampCode'),
|
||||||
extractFunction('findSafeStandaloneSixDigitCode'),
|
extractFunction('findSafeStandaloneSixDigitCode'),
|
||||||
@@ -566,9 +597,36 @@ return { extractVerificationCode };
|
|||||||
assert.equal(api.extractVerificationCode(bodyText, false), '982219');
|
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', () => {
|
test('extractVerificationCode ignores compact header time before fallback code', () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
extractFunction('extractStrictChatGPTVerificationCode'),
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
|
extractFunction('extractLegacyStrictVerificationCode'),
|
||||||
extractFunction('isLikelyCompactTimeValue'),
|
extractFunction('isLikelyCompactTimeValue'),
|
||||||
extractFunction('isLikelyHeaderTimestampCode'),
|
extractFunction('isLikelyHeaderTimestampCode'),
|
||||||
extractFunction('findSafeStandaloneSixDigitCode'),
|
extractFunction('findSafeStandaloneSixDigitCode'),
|
||||||
|
|||||||
@@ -64,6 +64,36 @@ test('extractVerificationCodeFromMessages 支持显式过滤条件并跳过排
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('extractVerificationCodeFromMessages supports keyword hints and runtime code patterns without OpenAI-specific fallback', () => {
|
||||||
|
const result = extractVerificationCodeFromMessages([
|
||||||
|
{
|
||||||
|
From: { EmailAddress: { Address: 'alerts@example.com' } },
|
||||||
|
Subject: 'Security center',
|
||||||
|
BodyPreview: 'Use pin A-551199 to continue',
|
||||||
|
ReceivedDateTime: '2026-04-14T10:07:00.000Z',
|
||||||
|
Id: 'mail-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: { EmailAddress: { Address: 'news@example.com' } },
|
||||||
|
Subject: 'Newsletter',
|
||||||
|
BodyPreview: 'pin A-000000',
|
||||||
|
ReceivedDateTime: '2026-04-14T10:06:00.000Z',
|
||||||
|
Id: 'mail-2',
|
||||||
|
},
|
||||||
|
], {
|
||||||
|
filterAfterTimestamp: 0,
|
||||||
|
senderFilters: [],
|
||||||
|
subjectFilters: [],
|
||||||
|
requiredKeywords: ['security'],
|
||||||
|
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
|
||||||
|
excludeCodes: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.code, '551199');
|
||||||
|
assert.equal(result.messageId, 'mail-1');
|
||||||
|
assert.equal(result.sender, 'alerts@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
test('normalizeMailboxId 将 Junk 归一为微软邮箱夹 ID', () => {
|
test('normalizeMailboxId 将 Junk 归一为微软邮箱夹 ID', () => {
|
||||||
assert.equal(normalizeMailboxId('INBOX'), 'inbox');
|
assert.equal(normalizeMailboxId('INBOX'), 'inbox');
|
||||||
assert.equal(normalizeMailboxId('junk'), 'junkemail');
|
assert.equal(normalizeMailboxId('junk'), 'junkemail');
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const source = fs.readFileSync('content/qq-mail.js', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
|
const start = markers
|
||||||
|
.map((marker) => source.indexOf(marker))
|
||||||
|
.find((index) => index >= 0);
|
||||||
|
if (start < 0) {
|
||||||
|
throw new Error(`missing function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parenDepth = 0;
|
||||||
|
let signatureEnded = false;
|
||||||
|
let braceStart = -1;
|
||||||
|
for (let i = start; i < source.length; i += 1) {
|
||||||
|
const ch = source[i];
|
||||||
|
if (ch === '(') {
|
||||||
|
parenDepth += 1;
|
||||||
|
} else if (ch === ')') {
|
||||||
|
parenDepth -= 1;
|
||||||
|
if (parenDepth === 0) {
|
||||||
|
signatureEnded = true;
|
||||||
|
}
|
||||||
|
} else if (ch === '{' && signatureEnded) {
|
||||||
|
braceStart = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (braceStart < 0) {
|
||||||
|
throw new Error(`missing body for function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let end = braceStart;
|
||||||
|
for (; end < source.length; end += 1) {
|
||||||
|
const ch = source[end];
|
||||||
|
if (ch === '{') depth += 1;
|
||||||
|
if (ch === '}') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
end += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return source.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('qq extractVerificationCode supports runtime mail rule patterns', () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
|
extractFunction('extractVerificationCode'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
${bundle}
|
||||||
|
return { extractVerificationCode };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
api.extractVerificationCode('Mailbox notice: use pin A-441122 to continue.', {
|
||||||
|
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
|
||||||
|
}),
|
||||||
|
'441122'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('qq handlePollEmail forwards runtime code patterns to new-mail matching', async () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('getCurrentMailIds'),
|
||||||
|
extractFunction('normalizeRulePatternList'),
|
||||||
|
extractFunction('extractCodeByRulePatterns'),
|
||||||
|
extractFunction('extractVerificationCode'),
|
||||||
|
extractFunction('handlePollEmail'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
let currentItems = [];
|
||||||
|
let refreshCount = 0;
|
||||||
|
|
||||||
|
function createMailItem(mailId, sender, subject, digest) {
|
||||||
|
return {
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'data-mailid') return mailId;
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
querySelector(selector) {
|
||||||
|
if (selector === '.cmp-account-nick') return { textContent: sender };
|
||||||
|
if (selector === '.mail-subject') return { textContent: subject };
|
||||||
|
if (selector === '.mail-digest') return { textContent: digest };
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = {
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (selector === '.mail-list-page-item[data-mailid]') {
|
||||||
|
return currentItems;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function waitForElement() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
async function refreshInbox() {
|
||||||
|
refreshCount += 1;
|
||||||
|
if (refreshCount >= 1) {
|
||||||
|
currentItems = [
|
||||||
|
createMailItem('mail-1', 'alerts@example.com', 'Security center', 'Use pin A-551188 to continue'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function sleep() {}
|
||||||
|
function log() {}
|
||||||
|
|
||||||
|
${bundle}
|
||||||
|
|
||||||
|
return { handlePollEmail };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const result = await api.handlePollEmail(4, {
|
||||||
|
senderFilters: ['alerts'],
|
||||||
|
subjectFilters: ['security'],
|
||||||
|
maxAttempts: 2,
|
||||||
|
intervalMs: 1,
|
||||||
|
codePatterns: [{ source: 'pin\\s+A-(\\d{6})', flags: 'i' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.code, '551188');
|
||||||
|
});
|
||||||
@@ -196,6 +196,107 @@ test('startAutoRunFromCurrentSettings freezes run count before async settings sy
|
|||||||
assert.equal(events[3].message.payload.totalRuns, 20);
|
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 () => {
|
test('persistCurrentSettingsForAction forces a silent save even when settings are not marked dirty', async () => {
|
||||||
const bundle = [
|
const bundle = [
|
||||||
extractFunction('waitForSettingsSaveIdle'),
|
extractFunction('waitForSettingsSaveIdle'),
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ return {
|
|||||||
assert.deepEqual(api.getStepIds(), [7]);
|
assert.deepEqual(api.getStepIds(), [7]);
|
||||||
assert.deepEqual(api.calls[0], {
|
assert.deepEqual(api.calls[0], {
|
||||||
type: 'getSteps',
|
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] });
|
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [7] });
|
||||||
});
|
});
|
||||||
@@ -266,7 +266,7 @@ return {
|
|||||||
assert.deepEqual(api.getStepIds(), [13]);
|
assert.deepEqual(api.getStepIds(), [13]);
|
||||||
assert.deepEqual(api.calls[0], {
|
assert.deepEqual(api.calls[0], {
|
||||||
type: 'getSteps',
|
type: 'getSteps',
|
||||||
options: { plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', signupMethod: 'email' },
|
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', signupMethod: 'email' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
|||||||
|
|
||||||
assert.equal(Array.isArray(steps), true);
|
assert.equal(Array.isArray(steps), true);
|
||||||
assert.equal(steps.length, 10);
|
assert.equal(steps.length, 10);
|
||||||
|
assert.equal(steps.every((step) => step.flowId === 'openai'), true);
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
steps.map((step) => step.order),
|
steps.map((step) => step.order),
|
||||||
steps.map((step) => step.order).slice().sort((left, right) => left - right)
|
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.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.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.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[5].title, '创建 Plus Checkout');
|
||||||
assert.equal(plusSteps[7].title, 'PayPal 登录与授权');
|
assert.equal(plusSteps[7].title, 'PayPal 登录与授权');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user