Refactor workflow auto-run to node graph
This commit is contained in:
+835
-411
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,8 @@
|
||||
buildLocalHelperEndpoint,
|
||||
chrome,
|
||||
getErrorMessage,
|
||||
getNodeIdByStepForState = null,
|
||||
getNodeTitleForState = null,
|
||||
getState,
|
||||
normalizeAccountRunHistoryHelperBaseUrl,
|
||||
} = deps;
|
||||
@@ -30,18 +32,27 @@
|
||||
if (normalized === 'success') {
|
||||
return 'success';
|
||||
}
|
||||
if (normalized === 'running' || /_running$/.test(normalized)) {
|
||||
if (normalized === 'running' || /_running$/.test(normalized) || /^node:[^:]+:running$/.test(normalized)) {
|
||||
return 'running';
|
||||
}
|
||||
if (normalized === 'failed' || /_failed$/.test(normalized)) {
|
||||
if (normalized === 'failed' || /_failed$/.test(normalized) || /^node:[^:]+:failed$/.test(normalized)) {
|
||||
return 'failed';
|
||||
}
|
||||
if (normalized === 'stopped' || /_stopped$/.test(normalized)) {
|
||||
if (normalized === 'stopped' || /_stopped$/.test(normalized) || /^node:[^:]+:stopped$/.test(normalized)) {
|
||||
return 'stopped';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeNodeId(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function extractRecordNodeFromStatus(status = '') {
|
||||
const match = String(status || '').trim().match(/^node:([^:]+):(?:running|failed|stopped)$/i);
|
||||
return match ? normalizeNodeId(match[1]) : '';
|
||||
}
|
||||
|
||||
function extractRecordStepFromStatus(status = '') {
|
||||
const normalizedStatus = String(status || '').trim().toLowerCase();
|
||||
const statusMatch = normalizedStatus.match(/^step(\d+)_(?:failed|stopped)$/);
|
||||
@@ -88,6 +99,11 @@
|
||||
return Number.isInteger(step) && step > 0 ? step : null;
|
||||
}
|
||||
|
||||
function parseFailureLabelNode(label = '') {
|
||||
const match = String(label || '').trim().match(/^节点\s*([A-Za-z0-9_.:-]+)\s*(?:失败|停止)$/);
|
||||
return match ? normalizeNodeId(match[1]) : '';
|
||||
}
|
||||
|
||||
function shouldIgnorePersistedFailedStepCandidate(candidate, failureDetail = '') {
|
||||
if (!Number.isInteger(candidate) || candidate <= 0) {
|
||||
return true;
|
||||
@@ -128,7 +144,22 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveFailureLabel(finalStatus, rawFailureLabel = '', computedFailureLabel = '', failedStep = null) {
|
||||
function resolveNormalizedFailedNodeId(record = {}, status = '', failedStep = null) {
|
||||
const explicitNodeId = normalizeNodeId(record.failedNodeId || record.failedNode || record.nodeId);
|
||||
if (explicitNodeId) {
|
||||
return explicitNodeId;
|
||||
}
|
||||
const statusNodeId = extractRecordNodeFromStatus(status || record.finalStatus || record.status || '');
|
||||
if (statusNodeId) {
|
||||
return statusNodeId;
|
||||
}
|
||||
if (Number.isInteger(failedStep) && failedStep > 0 && typeof getNodeIdByStepForState === 'function') {
|
||||
return normalizeNodeId(getNodeIdByStepForState(failedStep, record));
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function resolveFailureLabel(finalStatus, rawFailureLabel = '', computedFailureLabel = '', failedNodeId = '', failedStep = null) {
|
||||
const rawLabel = String(rawFailureLabel || '').trim();
|
||||
if (finalStatus === 'stopped') {
|
||||
return computedFailureLabel;
|
||||
@@ -137,6 +168,11 @@
|
||||
return rawLabel || computedFailureLabel;
|
||||
}
|
||||
|
||||
const rawNodeId = parseFailureLabelNode(rawLabel);
|
||||
if (rawNodeId) {
|
||||
return rawNodeId === failedNodeId ? rawLabel : computedFailureLabel;
|
||||
}
|
||||
|
||||
const rawStep = parseFailureLabelStep(rawLabel);
|
||||
if (Number.isInteger(rawStep) && rawStep > 0) {
|
||||
return rawStep === failedStep ? rawLabel : computedFailureLabel;
|
||||
@@ -156,7 +192,21 @@
|
||||
|| /进入了手机号页面/.test(text);
|
||||
}
|
||||
|
||||
function buildFailureLabel(finalStatus, failedStep, failureDetail = '') {
|
||||
function getNodeDisplayName(nodeId, state = {}) {
|
||||
const normalizedNodeId = normalizeNodeId(nodeId);
|
||||
if (!normalizedNodeId) {
|
||||
return '';
|
||||
}
|
||||
if (typeof getNodeTitleForState === 'function') {
|
||||
const title = String(getNodeTitleForState(normalizedNodeId, state) || '').trim();
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
return normalizedNodeId;
|
||||
}
|
||||
|
||||
function buildFailureLabel(finalStatus, failedNodeId = '', failedStep = null, failureDetail = '', state = {}) {
|
||||
if (finalStatus === 'success') {
|
||||
return '流程完成';
|
||||
}
|
||||
@@ -164,6 +214,9 @@
|
||||
return '正在运行';
|
||||
}
|
||||
if (finalStatus === 'stopped') {
|
||||
if (failedNodeId) {
|
||||
return `节点 ${getNodeDisplayName(failedNodeId, state)} 停止`;
|
||||
}
|
||||
if (Number.isInteger(failedStep) && failedStep > 0) {
|
||||
return `步骤 ${failedStep} 停止`;
|
||||
}
|
||||
@@ -175,6 +228,9 @@
|
||||
if (isPhoneVerificationFailure(failureDetail)) {
|
||||
return '出现手机号验证';
|
||||
}
|
||||
if (failedNodeId) {
|
||||
return `节点 ${getNodeDisplayName(failedNodeId, state)} 失败`;
|
||||
}
|
||||
if (Number.isInteger(failedStep) && failedStep > 0) {
|
||||
return `步骤 ${failedStep} 失败`;
|
||||
}
|
||||
@@ -341,6 +397,9 @@
|
||||
const failedStep = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? resolveNormalizedFailedStep(record, failureDetail)
|
||||
: null;
|
||||
const failedNodeId = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? resolveNormalizedFailedNodeId(record, record.finalStatus || record.status || '', failedStep)
|
||||
: '';
|
||||
const autoRunContext = normalizeAutoRunContext(record.autoRunContext);
|
||||
const retryCount = normalizeRetryCount(
|
||||
record.retryCount !== undefined
|
||||
@@ -348,11 +407,15 @@
|
||||
: ((autoRunContext?.attemptRun || 0) > 1 ? autoRunContext.attemptRun - 1 : 0)
|
||||
);
|
||||
const source = normalizeSource(record.source || (autoRunContext ? 'auto' : 'manual'));
|
||||
const computedFailureLabel = buildFailureLabel(finalStatus, failedStep, failureDetail);
|
||||
const computedFailureLabel = buildFailureLabel(finalStatus, failedNodeId, failedStep, failureDetail, record);
|
||||
const rawFailureLabel = String(record.failureLabel || '').trim();
|
||||
const flowId = String(record.flowId || record.activeFlowId || '').trim();
|
||||
const runId = String(record.runId || record.activeRunId || '').trim();
|
||||
|
||||
return {
|
||||
recordId: String(record.recordId || '').trim() || buildRecordId(accountIdentifier, accountIdentifierType),
|
||||
flowId,
|
||||
runId,
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
email,
|
||||
@@ -361,8 +424,9 @@
|
||||
finalStatus,
|
||||
finishedAt,
|
||||
retryCount,
|
||||
failureLabel: resolveFailureLabel(finalStatus, rawFailureLabel, computedFailureLabel, failedStep),
|
||||
failureLabel: resolveFailureLabel(finalStatus, rawFailureLabel, computedFailureLabel, failedNodeId, failedStep),
|
||||
failureDetail,
|
||||
failedNodeId,
|
||||
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
|
||||
source,
|
||||
autoRunContext: source === 'auto' ? autoRunContext : null,
|
||||
@@ -422,13 +486,31 @@
|
||||
const failedStep = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? extractRecordStep(status, failureDetail)
|
||||
: null;
|
||||
const statusNodeId = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? extractRecordNodeFromStatus(status)
|
||||
: '';
|
||||
const stateNodeId = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||
? normalizeNodeId(state.currentNodeId)
|
||||
: '';
|
||||
const failedNodeIdFromStep = !statusNodeId
|
||||
&& !stateNodeId
|
||||
&& Number.isInteger(failedStep)
|
||||
&& failedStep > 0
|
||||
&& typeof getNodeIdByStepForState === 'function'
|
||||
? normalizeNodeId(getNodeIdByStepForState(failedStep, state))
|
||||
: '';
|
||||
const failedNodeId = statusNodeId || stateNodeId || failedNodeIdFromStep;
|
||||
const source = Boolean(state.autoRunning) ? 'auto' : 'manual';
|
||||
const autoRunContext = source === 'auto' ? buildAutoRunContextFromState(state) : null;
|
||||
const retryCount = source === 'auto' ? getRetryCountFromState(state) : 0;
|
||||
const finishedAt = new Date().toISOString();
|
||||
const flowId = String(state.flowId || state.activeFlowId || '').trim();
|
||||
const runId = String(state.runId || state.activeRunId || '').trim();
|
||||
|
||||
return {
|
||||
recordId: buildRecordId(accountIdentifier, accountIdentifierType),
|
||||
flowId,
|
||||
runId,
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
email,
|
||||
@@ -437,9 +519,10 @@
|
||||
finalStatus,
|
||||
finishedAt,
|
||||
retryCount,
|
||||
failureLabel: buildFailureLabel(finalStatus, failedStep, failureDetail),
|
||||
failureLabel: buildFailureLabel(finalStatus, failedNodeId, failedStep, failureDetail, state),
|
||||
failureDetail,
|
||||
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
|
||||
failedNodeId,
|
||||
failedStep: statusNodeId ? null : (Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null),
|
||||
source,
|
||||
autoRunContext,
|
||||
plusModeEnabled: Boolean(state.plusModeEnabled),
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
ensureHotmailMailboxReadyForAutoRunRound,
|
||||
getAutoRunStatusPayload,
|
||||
getErrorMessage,
|
||||
getFirstUnfinishedStep,
|
||||
getFirstUnfinishedNodeId,
|
||||
getPendingAutoRunTimerPlan,
|
||||
getRunningSteps,
|
||||
getRunningNodeIds,
|
||||
getState,
|
||||
hasSavedProgress,
|
||||
hasSavedNodeProgress,
|
||||
isAddPhoneAuthFailure,
|
||||
isGpcTaskEndedFailure,
|
||||
isPhoneSmsPlatformRateLimitFailure,
|
||||
@@ -34,14 +34,49 @@
|
||||
normalizeAutoRunFallbackThreadIntervalMinutes,
|
||||
persistAutoRunTimerPlan,
|
||||
resetState,
|
||||
runAutoSequenceFromStep,
|
||||
runAutoSequenceFromNode,
|
||||
runtime,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
throwIfAutoRunSessionStopped,
|
||||
waitForRunningStepsToFinish,
|
||||
waitForRunningNodesToFinish,
|
||||
} = deps;
|
||||
|
||||
function getRunningWorkflowNodes(state = {}) {
|
||||
if (typeof getRunningNodeIds === 'function') {
|
||||
return getRunningNodeIds(state.nodeStatuses || {}, state);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getFirstUnfinishedWorkflowNode(state = {}) {
|
||||
if (typeof getFirstUnfinishedNodeId === 'function') {
|
||||
return getFirstUnfinishedNodeId(state.nodeStatuses || {}, state);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasSavedWorkflowProgress(state = {}) {
|
||||
if (typeof hasSavedNodeProgress === 'function') {
|
||||
return hasSavedNodeProgress(state.nodeStatuses || {}, state);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function waitForRunningWorkflowNodesToFinish(payload = {}) {
|
||||
if (typeof waitForRunningNodesToFinish === 'function') {
|
||||
return waitForRunningNodesToFinish(payload);
|
||||
}
|
||||
return getState();
|
||||
}
|
||||
|
||||
async function runAutoSequenceFromWorkflowNode(startNodeId, context = {}) {
|
||||
if (typeof runAutoSequenceFromNode === 'function') {
|
||||
return runAutoSequenceFromNode(startNodeId, context);
|
||||
}
|
||||
throw new Error('自动运行节点执行器未接入。');
|
||||
}
|
||||
|
||||
function createAutoRunRoundSummary(round) {
|
||||
return {
|
||||
round,
|
||||
@@ -85,90 +120,83 @@
|
||||
return Math.max(0, Number(summary?.attempts || 0) - 1);
|
||||
}
|
||||
|
||||
function normalizeRecordStep(value) {
|
||||
const step = Math.floor(Number(value) || 0);
|
||||
return step > 0 ? step : null;
|
||||
function normalizeRecordNode(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function extractStepFromRecordStatus(status = '') {
|
||||
const match = String(status || '').trim().toLowerCase().match(/^step(\d+)_(?:failed|stopped)$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return normalizeRecordStep(match[1]);
|
||||
function extractNodeFromRecordStatus(status = '') {
|
||||
const match = String(status || '').trim().match(/^node:([^:]+):(failed|stopped)$/i);
|
||||
return match ? normalizeRecordNode(match[1]) : '';
|
||||
}
|
||||
|
||||
function getKnownStepIdsFromState(state = {}) {
|
||||
function getKnownNodeIdsFromState(state = {}) {
|
||||
const ids = new Set();
|
||||
for (const key of Object.keys(state?.stepStatuses || {})) {
|
||||
const step = normalizeRecordStep(key);
|
||||
if (step) {
|
||||
ids.add(step);
|
||||
for (const key of Object.keys(state?.nodeStatuses || {})) {
|
||||
const nodeId = normalizeRecordNode(key);
|
||||
if (nodeId) {
|
||||
ids.add(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
const currentStep = normalizeRecordStep(state?.currentStep);
|
||||
if (currentStep) {
|
||||
ids.add(currentStep);
|
||||
const currentNodeId = normalizeRecordNode(state?.currentNodeId);
|
||||
if (currentNodeId) {
|
||||
ids.add(currentNodeId);
|
||||
}
|
||||
|
||||
return Array.from(ids).sort((left, right) => left - right);
|
||||
return Array.from(ids);
|
||||
}
|
||||
|
||||
function inferRecordStepFromState(state = {}, preferredStatuses = []) {
|
||||
const statuses = state?.stepStatuses || {};
|
||||
function inferRecordNodeFromState(state = {}, preferredStatuses = []) {
|
||||
const statuses = state?.nodeStatuses || {};
|
||||
const preferredStatusSet = new Set(preferredStatuses.map((item) => String(item || '').trim()).filter(Boolean));
|
||||
const stepIds = getKnownStepIdsFromState(state);
|
||||
const currentStep = normalizeRecordStep(state?.currentStep);
|
||||
const nodeIds = getKnownNodeIdsFromState(state);
|
||||
const currentNodeId = normalizeRecordNode(state?.currentNodeId);
|
||||
|
||||
if (currentStep && preferredStatusSet.has(String(statuses[currentStep] || '').trim())) {
|
||||
return currentStep;
|
||||
if (currentNodeId && preferredStatusSet.has(String(statuses[currentNodeId] || '').trim())) {
|
||||
return currentNodeId;
|
||||
}
|
||||
|
||||
const matchingSteps = stepIds
|
||||
.filter((step) => preferredStatusSet.has(String(statuses[step] || '').trim()))
|
||||
.sort((left, right) => right - left);
|
||||
if (matchingSteps.length) {
|
||||
return matchingSteps[0];
|
||||
const matchingNodes = nodeIds.filter((nodeId) => preferredStatusSet.has(String(statuses[nodeId] || '').trim()));
|
||||
if (matchingNodes.length) {
|
||||
return matchingNodes[matchingNodes.length - 1];
|
||||
}
|
||||
|
||||
if (currentStep) {
|
||||
const currentStatus = String(statuses[currentStep] || '').trim();
|
||||
if (currentNodeId) {
|
||||
const currentStatus = String(statuses[currentNodeId] || '').trim();
|
||||
if (!['', 'pending', 'completed', 'manual_completed', 'skipped'].includes(currentStatus)) {
|
||||
return currentStep;
|
||||
return currentNodeId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return '';
|
||||
}
|
||||
|
||||
function inferRecordStepFromError(errorLike = null) {
|
||||
function inferRecordNodeFromError(errorLike = null, state = {}) {
|
||||
if (!errorLike || typeof errorLike !== 'object') {
|
||||
return null;
|
||||
return '';
|
||||
}
|
||||
|
||||
return normalizeRecordStep(errorLike.failedStep)
|
||||
|| normalizeRecordStep(errorLike.step)
|
||||
|| normalizeRecordStep(errorLike.currentStep);
|
||||
return normalizeRecordNode(errorLike.failedNodeId)
|
||||
|| normalizeRecordNode(errorLike.nodeId)
|
||||
|| normalizeRecordNode(errorLike.currentNodeId);
|
||||
}
|
||||
|
||||
function resolveAutoRunAccountRecordStatus(status, state = {}, errorLike = null) {
|
||||
const normalizedStatus = String(status || '').trim().toLowerCase();
|
||||
const explicitStep = extractStepFromRecordStatus(normalizedStatus);
|
||||
if (explicitStep) {
|
||||
return normalizedStatus;
|
||||
const explicitNode = extractNodeFromRecordStatus(status);
|
||||
if (explicitNode) {
|
||||
return `node:${explicitNode}:${normalizedStatus.endsWith(':stopped') ? 'stopped' : 'failed'}`;
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'failed') {
|
||||
const failedStep = inferRecordStepFromError(errorLike)
|
||||
|| inferRecordStepFromState(state, ['failed', 'running']);
|
||||
return failedStep ? `step${failedStep}_failed` : status;
|
||||
const failedNode = inferRecordNodeFromError(errorLike, state)
|
||||
|| inferRecordNodeFromState(state, ['failed', 'running']);
|
||||
return failedNode ? `node:${failedNode}:failed` : status;
|
||||
}
|
||||
|
||||
if (normalizedStatus === 'stopped') {
|
||||
const stoppedStep = inferRecordStepFromError(errorLike)
|
||||
|| inferRecordStepFromState(state, ['stopped', 'running']);
|
||||
return stoppedStep ? `step${stoppedStep}_stopped` : status;
|
||||
const stoppedNode = inferRecordNodeFromError(errorLike, state)
|
||||
|| inferRecordNodeFromState(state, ['stopped', 'running']);
|
||||
return stoppedNode ? `node:${stoppedNode}:stopped` : status;
|
||||
}
|
||||
|
||||
return status;
|
||||
@@ -439,7 +467,7 @@
|
||||
|
||||
let successfulRuns = roundSummaries.filter((item) => item.status === 'success').length;
|
||||
const initialState = await getState();
|
||||
const initialPhase = continueCurrentOnFirstAttempt && getRunningSteps(initialState.stepStatuses, initialState).length
|
||||
const initialPhase = continueCurrentOnFirstAttempt && getRunningWorkflowNodes(initialState).length
|
||||
? 'waiting_step'
|
||||
: 'running';
|
||||
const showResumePosition = continueCurrentOnFirstAttempt || resumeCurrentRun > 1 || resumeAttemptRun > 1;
|
||||
@@ -474,24 +502,25 @@
|
||||
autoRunAttemptRun: attemptRun,
|
||||
});
|
||||
roundSummary.attempts = attemptRun;
|
||||
let startStep = 1;
|
||||
const defaultStartNodeId = typeof runAutoSequenceFromNode === 'function' ? 'open-chatgpt' : 1;
|
||||
let startNodeId = defaultStartNodeId;
|
||||
let useExistingProgress = false;
|
||||
|
||||
if (reuseExistingProgress) {
|
||||
let currentState = await getState();
|
||||
if (getRunningSteps(currentState.stepStatuses, currentState).length) {
|
||||
currentState = await waitForRunningStepsToFinish({
|
||||
if (getRunningWorkflowNodes(currentState).length) {
|
||||
currentState = await waitForRunningWorkflowNodesToFinish({
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
});
|
||||
}
|
||||
const resumeStep = getFirstUnfinishedStep(currentState.stepStatuses, currentState);
|
||||
if (resumeStep && hasSavedProgress(currentState.stepStatuses, currentState)) {
|
||||
startStep = resumeStep;
|
||||
const resumeNodeId = getFirstUnfinishedWorkflowNode(currentState);
|
||||
if (resumeNodeId && hasSavedWorkflowProgress(currentState)) {
|
||||
startNodeId = resumeNodeId;
|
||||
useExistingProgress = true;
|
||||
} else if (hasSavedProgress(currentState.stepStatuses, currentState)) {
|
||||
await addLog('检测到当前流程已处理完成,本轮将改为从步骤 1 重新开始。', 'info');
|
||||
} else if (hasSavedWorkflowProgress(currentState)) {
|
||||
await addLog('检测到当前流程已处理完成,本轮将改为从首个节点重新开始。', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,7 +600,7 @@
|
||||
sessionId,
|
||||
});
|
||||
|
||||
if (!useExistingProgress && startStep === 1 && typeof ensureHotmailMailboxReadyForAutoRunRound === 'function') {
|
||||
if (!useExistingProgress && startNodeId === defaultStartNodeId && typeof ensureHotmailMailboxReadyForAutoRunRound === 'function') {
|
||||
await ensureHotmailMailboxReadyForAutoRunRound({
|
||||
targetRun,
|
||||
totalRuns,
|
||||
@@ -580,7 +609,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
await runAutoSequenceFromStep(startStep, {
|
||||
await runAutoSequenceFromWorkflowNode(startNodeId, {
|
||||
targetRun,
|
||||
totalRuns,
|
||||
attemptRuns: attemptRun,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const {
|
||||
chrome,
|
||||
DEFAULT_STATE,
|
||||
getStepIdByNodeIdForState,
|
||||
getState,
|
||||
isRecoverableStep9AuthFailure,
|
||||
LOG_PREFIX,
|
||||
@@ -51,12 +52,14 @@
|
||||
const normalizedOptions = options && typeof options === 'object' ? options : {};
|
||||
const step = normalizeLogStep(normalizedOptions.step);
|
||||
const stepKey = String(normalizedOptions.stepKey || '').trim();
|
||||
const nodeId = String(normalizedOptions.nodeId || normalizedOptions.nodeKey || stepKey || '').trim();
|
||||
return {
|
||||
message: String(message || ''),
|
||||
level,
|
||||
timestamp: Date.now(),
|
||||
step,
|
||||
stepKey,
|
||||
nodeId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,14 +73,21 @@
|
||||
chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => { });
|
||||
}
|
||||
|
||||
async function setStepStatus(step, status) {
|
||||
async function setNodeStatus(nodeId, status) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
if (!normalizedNodeId) {
|
||||
throw new Error('setNodeStatus 缺少 nodeId。');
|
||||
}
|
||||
const state = await getState();
|
||||
const statuses = { ...state.stepStatuses };
|
||||
statuses[step] = status;
|
||||
await setState({ stepStatuses: statuses, currentStep: step });
|
||||
const nodeStatuses = { ...(state.nodeStatuses || {}) };
|
||||
nodeStatuses[normalizedNodeId] = status;
|
||||
await setState({
|
||||
nodeStatuses,
|
||||
currentNodeId: normalizedNodeId,
|
||||
});
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'STEP_STATUS_CHANGED',
|
||||
payload: { step, status },
|
||||
type: 'NODE_STATUS_CHANGED',
|
||||
payload: { nodeId: normalizedNodeId, status },
|
||||
}).catch(() => { });
|
||||
}
|
||||
|
||||
@@ -149,29 +159,50 @@
|
||||
}
|
||||
|
||||
function getFirstUnfinishedStep(statuses = {}) {
|
||||
const stepIds = Object.keys(DEFAULT_STATE.stepStatuses || {})
|
||||
.map((step) => Number(step))
|
||||
.filter(Number.isFinite)
|
||||
.sort((left, right) => left - right);
|
||||
for (const step of stepIds) {
|
||||
if (!isStepDoneStatus(statuses[step] || 'pending')) {
|
||||
return step;
|
||||
const nodeStatuses = statuses && typeof statuses === 'object' ? statuses : {};
|
||||
const nodeIds = Object.keys(DEFAULT_STATE.nodeStatuses || {});
|
||||
for (const nodeId of nodeIds) {
|
||||
if (!isStepDoneStatus(nodeStatuses[nodeId] || 'pending')) {
|
||||
return typeof getStepIdByNodeIdForState === 'function'
|
||||
? getStepIdByNodeIdForState(nodeId, {})
|
||||
: null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasSavedProgress(statuses = {}) {
|
||||
return Object.values({ ...DEFAULT_STATE.stepStatuses, ...statuses }).some((status) => status !== 'pending');
|
||||
return Object.values({ ...DEFAULT_STATE.nodeStatuses, ...statuses }).some((status) => status !== 'pending');
|
||||
}
|
||||
|
||||
function getRunningSteps(statuses = {}) {
|
||||
return Object.entries({ ...DEFAULT_STATE.stepStatuses, ...statuses })
|
||||
return Object.entries({ ...DEFAULT_STATE.nodeStatuses, ...statuses })
|
||||
.filter(([, status]) => status === 'running')
|
||||
.map(([step]) => Number(step))
|
||||
.map(([nodeId]) => (typeof getStepIdByNodeIdForState === 'function' ? getStepIdByNodeIdForState(nodeId, {}) : null))
|
||||
.filter((step) => Number.isInteger(step) && step > 0)
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function getFirstUnfinishedNode(statuses = {}) {
|
||||
const nodeIds = Object.keys(DEFAULT_STATE.nodeStatuses || {});
|
||||
for (const nodeId of nodeIds) {
|
||||
if (!isStepDoneStatus(statuses[nodeId] || 'pending')) {
|
||||
return nodeId;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function hasSavedNodeProgress(statuses = {}) {
|
||||
return Object.values({ ...DEFAULT_STATE.nodeStatuses, ...statuses }).some((status) => status !== 'pending');
|
||||
}
|
||||
|
||||
function getRunningNodes(statuses = {}) {
|
||||
return Object.entries({ ...DEFAULT_STATE.nodeStatuses, ...statuses })
|
||||
.filter(([, status]) => status === 'running')
|
||||
.map(([nodeId]) => nodeId);
|
||||
}
|
||||
|
||||
function getAutoRunStatusPayload(phase, payload = {}) {
|
||||
return {
|
||||
autoRunning: phase === 'scheduled'
|
||||
@@ -195,12 +226,15 @@
|
||||
return {
|
||||
addLog,
|
||||
getAutoRunStatusPayload,
|
||||
getFirstUnfinishedNode,
|
||||
isAddPhoneAuthFailure,
|
||||
getErrorMessage,
|
||||
getFirstUnfinishedStep,
|
||||
getLoginAuthStateLabel,
|
||||
getRunningNodes,
|
||||
getRunningSteps,
|
||||
getSourceLabel,
|
||||
hasSavedNodeProgress,
|
||||
hasSavedProgress,
|
||||
isLegacyStep9RecoverableAuthError,
|
||||
isRestartCurrentAttemptError,
|
||||
@@ -208,7 +242,7 @@
|
||||
isStep9RecoverableAuthError,
|
||||
isStepDoneStatus,
|
||||
isVerificationMailPollingError,
|
||||
setStepStatus,
|
||||
setNodeStatus,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,21 @@
|
||||
return flowBuilder.getRuleDefinition(step, state);
|
||||
}
|
||||
|
||||
function getVerificationMailRuleForNode(nodeId, state = {}) {
|
||||
const flowId = resolveFlowId(state);
|
||||
const flowBuilder = getFlowBuilder(flowId);
|
||||
if (!flowBuilder) {
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件规则定义。`);
|
||||
}
|
||||
if (typeof flowBuilder.getRuleDefinitionForNode === 'function') {
|
||||
return flowBuilder.getRuleDefinitionForNode(nodeId, state);
|
||||
}
|
||||
if (typeof flowBuilder.getRuleDefinition === 'function') {
|
||||
return flowBuilder.getRuleDefinition({ nodeId }, state);
|
||||
}
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件规则定义。`);
|
||||
}
|
||||
|
||||
function buildVerificationPollPayload(step, state = {}, overrides = {}) {
|
||||
const flowId = resolveFlowId(state);
|
||||
const flowBuilder = getFlowBuilder(flowId);
|
||||
@@ -35,9 +50,26 @@
|
||||
return flowBuilder.buildVerificationPollPayload(step, state, overrides);
|
||||
}
|
||||
|
||||
function buildVerificationPollPayloadForNode(nodeId, state = {}, overrides = {}) {
|
||||
const flowId = resolveFlowId(state);
|
||||
const flowBuilder = getFlowBuilder(flowId);
|
||||
if (!flowBuilder) {
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件轮询规则构造器。`);
|
||||
}
|
||||
if (typeof flowBuilder.buildVerificationPollPayloadForNode === 'function') {
|
||||
return flowBuilder.buildVerificationPollPayloadForNode(nodeId, state, overrides);
|
||||
}
|
||||
if (typeof flowBuilder.buildVerificationPollPayload === 'function') {
|
||||
return flowBuilder.buildVerificationPollPayload({ nodeId }, state, overrides);
|
||||
}
|
||||
throw new Error(`未找到 flow=${flowId} 的邮件轮询规则构造器。`);
|
||||
}
|
||||
|
||||
return {
|
||||
buildVerificationPollPayload,
|
||||
buildVerificationPollPayloadForNode,
|
||||
getVerificationMailRule,
|
||||
getVerificationMailRuleForNode,
|
||||
resolveFlowId,
|
||||
};
|
||||
}
|
||||
|
||||
+212
-91
@@ -21,17 +21,17 @@
|
||||
clearStopRequest,
|
||||
closeLocalhostCallbackTabs,
|
||||
closeTabsByUrlPrefix,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
deleteHotmailAccount,
|
||||
deleteHotmailAccounts,
|
||||
deleteIcloudAlias,
|
||||
deleteUsedIcloudAliases,
|
||||
disableUsedLuckmailPurchases,
|
||||
doesStepUseCompletionSignal,
|
||||
doesNodeUseCompletionSignal,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureManualInteractionAllowed,
|
||||
executeStep,
|
||||
executeStepViaCompletionSignal,
|
||||
executeNode,
|
||||
executeNodeViaCompletionSignal,
|
||||
exportSettingsBundle,
|
||||
fetchGeneratedEmail,
|
||||
refreshGpcCardBalance,
|
||||
@@ -47,6 +47,9 @@
|
||||
getPendingAutoRunTimerPlan,
|
||||
getSourceLabel,
|
||||
getState,
|
||||
getNodeDefinitionForState,
|
||||
getNodeIdsForState,
|
||||
getStepIdByNodeIdForState,
|
||||
getStepDefinitionForState,
|
||||
getStepIdsForState,
|
||||
getLastStepIdForState,
|
||||
@@ -137,8 +140,8 @@
|
||||
normalizePayPalAccounts,
|
||||
normalizeRunCount,
|
||||
AUTO_RUN_TIMER_KIND_SCHEDULED_START,
|
||||
notifyStepComplete,
|
||||
notifyStepError,
|
||||
notifyNodeComplete,
|
||||
notifyNodeError,
|
||||
patchMail2925Account,
|
||||
patchHotmailAccount,
|
||||
pollContributionStatus,
|
||||
@@ -169,9 +172,9 @@
|
||||
setLuckmailPurchaseUsedState,
|
||||
setPersistentSettings,
|
||||
setState,
|
||||
setStepStatus,
|
||||
setNodeStatus,
|
||||
skipAutoRunCountdown,
|
||||
skipStep,
|
||||
skipNode,
|
||||
startContributionFlow,
|
||||
startAutoRunLoop,
|
||||
deleteMail2925Account,
|
||||
@@ -215,27 +218,92 @@
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_OPENAI_NODE_BY_STEP = Object.freeze({
|
||||
1: 'open-chatgpt',
|
||||
2: 'submit-signup-email',
|
||||
3: 'fill-password',
|
||||
4: 'fetch-signup-code',
|
||||
5: 'fill-profile',
|
||||
6: 'wait-registration-success',
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
9: 'confirm-oauth',
|
||||
10: 'platform-verify',
|
||||
11: 'fetch-login-code',
|
||||
12: 'confirm-oauth',
|
||||
13: 'platform-verify',
|
||||
});
|
||||
|
||||
function getStepKeyForState(step, state = {}) {
|
||||
if (typeof getStepDefinitionForState === 'function') {
|
||||
return String(getStepDefinitionForState(step, state)?.key || '').trim();
|
||||
}
|
||||
return '';
|
||||
return DEFAULT_OPENAI_NODE_BY_STEP[Number(step)] || '';
|
||||
}
|
||||
|
||||
function isStaleAutoRunStepMessage(step, state = {}) {
|
||||
function findStepByNodeId(nodeId, state = {}) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
if (normalizedNodeId && typeof getStepIdByNodeIdForState === 'function') {
|
||||
const step = getStepIdByNodeIdForState(normalizedNodeId, state);
|
||||
if (Number.isInteger(step) && step > 0) {
|
||||
return step;
|
||||
}
|
||||
}
|
||||
if (!normalizedNodeId || typeof getStepIdsForState !== 'function') {
|
||||
return 0;
|
||||
}
|
||||
for (const stepId of getStepIdsForState(state)) {
|
||||
if (getStepKeyForState(stepId, state) === normalizedNodeId) {
|
||||
return Number(stepId) || 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function normalizeNodeProtocolMessage(message = {}) {
|
||||
const type = String(message?.type || '').trim();
|
||||
const nodeProtocolTypes = new Set([
|
||||
'EXECUTE_NODE',
|
||||
'NODE_COMPLETE',
|
||||
'NODE_ERROR',
|
||||
'SKIP_NODE',
|
||||
]);
|
||||
if (!nodeProtocolTypes.has(type)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const nodeId = String(message?.payload?.nodeId || message?.nodeId || '').trim();
|
||||
if (!nodeId) {
|
||||
throw new Error(`${type} 缺少 nodeId。`);
|
||||
}
|
||||
const state = await getState();
|
||||
const step = findStepByNodeId(nodeId, state);
|
||||
if (!step) {
|
||||
throw new Error(`当前 flow 中未找到节点:${nodeId}`);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...(message.payload || {}),
|
||||
nodeId,
|
||||
step,
|
||||
};
|
||||
return { ...message, nodeId, step, payload };
|
||||
}
|
||||
|
||||
function isStaleAutoRunNodeMessage(nodeId, state = {}) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
if (!normalizedNodeId) {
|
||||
return false;
|
||||
}
|
||||
if (typeof isAutoRunLockedState !== 'function' || !isAutoRunLockedState(state)) {
|
||||
return false;
|
||||
}
|
||||
const normalizedStep = Number(step);
|
||||
if (!Number.isInteger(normalizedStep) || normalizedStep <= 0) {
|
||||
return false;
|
||||
}
|
||||
const currentStatus = String(state?.stepStatuses?.[normalizedStep] || '').trim();
|
||||
const currentStatus = String(state?.nodeStatuses?.[normalizedNodeId] || '').trim();
|
||||
if (currentStatus === 'running') {
|
||||
return false;
|
||||
}
|
||||
const currentStep = Number(state?.currentStep) || 0;
|
||||
if (currentStep > 0 && normalizedStep !== currentStep) {
|
||||
const currentNodeId = String(state?.currentNodeId || '').trim();
|
||||
if (currentNodeId && normalizedNodeId !== currentNodeId) {
|
||||
return true;
|
||||
}
|
||||
return ['completed', 'manual_completed', 'skipped', 'failed', 'stopped'].includes(currentStatus);
|
||||
@@ -383,24 +451,38 @@
|
||||
|| status === 'skipped';
|
||||
}
|
||||
|
||||
function findStepByKeyAfter(currentStep, targetKey, state = {}) {
|
||||
function findStepByKeyAfter(currentOrder, targetKey, state = {}) {
|
||||
const activeStepIds = typeof getStepIdsForState === 'function'
|
||||
? getStepIdsForState(state)
|
||||
: [];
|
||||
const candidates = activeStepIds.length ? activeStepIds : [Number(currentStep) + 1, 8];
|
||||
const candidates = activeStepIds.length ? activeStepIds : [Number(currentOrder) + 1, 8];
|
||||
return candidates.find((stepId) => {
|
||||
const numericStep = Number(stepId);
|
||||
if (!Number.isFinite(numericStep) || numericStep <= Number(currentStep)) {
|
||||
if (!Number.isFinite(numericStep) || numericStep <= Number(currentOrder)) {
|
||||
return false;
|
||||
}
|
||||
const stepKey = getStepKeyForState(numericStep, state);
|
||||
if (stepKey) {
|
||||
return stepKey === targetKey;
|
||||
}
|
||||
return targetKey === 'fetch-login-code' && Number(currentStep) === 7 && numericStep === 8;
|
||||
return targetKey === 'fetch-login-code' && Number(currentOrder) === 7 && numericStep === 8;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getNodeStatusByStep(step, state = {}) {
|
||||
const nodeId = getStepKeyForState(step, state);
|
||||
return nodeId ? (state.nodeStatuses?.[nodeId] || 'pending') : 'pending';
|
||||
}
|
||||
|
||||
async function setNodeStatusByStep(step, status, state = {}) {
|
||||
const nodeId = getStepKeyForState(step, state);
|
||||
if (!nodeId) {
|
||||
throw new Error(`未找到步骤 ${step} 对应节点。`);
|
||||
}
|
||||
await setNodeStatus(nodeId, status);
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
function normalizePlusPaymentMethodForDisplay(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'gpc-helper') {
|
||||
@@ -500,9 +582,9 @@
|
||||
const latestState = await getState();
|
||||
const loginCodeStep = findStepByKeyAfter(step, 'fetch-login-code', latestState);
|
||||
if (loginCodeStep) {
|
||||
const currentStatus = latestState.stepStatuses?.[loginCodeStep];
|
||||
const currentStatus = getNodeStatusByStep(loginCodeStep, latestState);
|
||||
if (!isStepProtectedFromAutoSkip(currentStatus)) {
|
||||
await setStepStatus(loginCodeStep, 'skipped');
|
||||
await setNodeStatusByStep(loginCodeStep, 'skipped', latestState);
|
||||
await addLog(`认证页已直接进入 OAuth 授权页,已自动跳过步骤 ${loginCodeStep} 的登录验证码。`, 'warn', {
|
||||
step,
|
||||
stepKey: 'oauth-login',
|
||||
@@ -571,21 +653,21 @@
|
||||
await syncStepAccountIdentityFromPayload(payload);
|
||||
if (payload.skipRegistrationFlow) {
|
||||
const latestState = await getState();
|
||||
for (const skipStep of [3, 4, 5]) {
|
||||
const status = latestState.stepStatuses?.[skipStep];
|
||||
for (const skippedStep of [3, 4, 5]) {
|
||||
const status = getNodeStatusByStep(skippedStep, latestState);
|
||||
if (status === 'running' || status === 'completed' || status === 'manual_completed') {
|
||||
continue;
|
||||
}
|
||||
await setStepStatus(skipStep, 'skipped');
|
||||
await setNodeStatusByStep(skippedStep, 'skipped', latestState);
|
||||
}
|
||||
await addLog('步骤 2:检测到当前已登录会话,已自动跳过步骤 3/4/5,流程将直接进入步骤 6。', 'warn');
|
||||
break;
|
||||
}
|
||||
if (payload.skippedPasswordStep) {
|
||||
const latestState = await getState();
|
||||
const step3Status = latestState.stepStatuses?.[3];
|
||||
const step3Status = getNodeStatusByStep(3, latestState);
|
||||
if (step3Status !== 'running' && step3Status !== 'completed' && step3Status !== 'manual_completed') {
|
||||
await setStepStatus(3, 'skipped');
|
||||
await setNodeStatusByStep(3, 'skipped', latestState);
|
||||
const identityLabel = payload.accountIdentifierType === 'phone' ? '手机号' : '邮箱';
|
||||
await addLog(`步骤 2:提交${identityLabel}后页面直接进入验证码页,已自动跳过步骤 3。`, 'warn');
|
||||
}
|
||||
@@ -598,9 +680,9 @@
|
||||
}
|
||||
if (payload.skipProfileStep) {
|
||||
const latestState = await getState();
|
||||
const step5Status = latestState.stepStatuses?.[5];
|
||||
const step5Status = getNodeStatusByStep(5, latestState);
|
||||
if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') {
|
||||
await setStepStatus(5, 'skipped');
|
||||
await setNodeStatusByStep(5, 'skipped', latestState);
|
||||
await addLog('步骤 3:页面已直接进入已登录态,已自动跳过步骤 5。', 'warn');
|
||||
}
|
||||
}
|
||||
@@ -621,9 +703,9 @@
|
||||
});
|
||||
if (payload.skipProfileStep) {
|
||||
const latestState = await getState();
|
||||
const step5Status = latestState.stepStatuses?.[5];
|
||||
const step5Status = getNodeStatusByStep(5, latestState);
|
||||
if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') {
|
||||
await setStepStatus(5, 'skipped');
|
||||
await setNodeStatusByStep(5, 'skipped', latestState);
|
||||
if (payload.skipProfileStepReason === 'combined_verification_profile') {
|
||||
await addLog('步骤 4:当前验证码页已内嵌完成注册资料提交,已自动跳过步骤 5。', 'warn');
|
||||
} else {
|
||||
@@ -664,7 +746,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMessage(message, sender) {
|
||||
async function handleMessage(rawMessage, sender) {
|
||||
const message = await normalizeNodeProtocolMessage(rawMessage);
|
||||
switch (message.type) {
|
||||
case 'CONTENT_SCRIPT_READY': {
|
||||
const tabId = sender.tab?.id;
|
||||
@@ -690,20 +773,30 @@
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'STEP_COMPLETE': {
|
||||
case 'NODE_COMPLETE': {
|
||||
const currentStateForNode = await getState();
|
||||
const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim();
|
||||
const resolvedStep = findStepByNodeId(nodeId, currentStateForNode);
|
||||
if (!nodeId || !resolvedStep) {
|
||||
throw new Error('NODE_COMPLETE 缺少 nodeId。');
|
||||
}
|
||||
const currentState = await getState();
|
||||
if (isStaleAutoRunStepMessage(message.step, currentState)) {
|
||||
await addLog(`自动运行:忽略过期的步骤 ${message.step} 完成消息,当前流程已在步骤 ${currentState.currentStep || '未知'}。`, 'warn', { step: message.step });
|
||||
if (isStaleAutoRunNodeMessage(nodeId, currentState)) {
|
||||
await addLog(
|
||||
`自动运行:忽略过期的节点 ${nodeId} 完成消息,当前流程已在节点 ${currentState.currentNodeId || '未知'}。`,
|
||||
'warn',
|
||||
{ nodeId }
|
||||
);
|
||||
return { ok: true, ignored: true };
|
||||
}
|
||||
if (getStopRequested()) {
|
||||
await setStepStatus(message.step, 'stopped');
|
||||
await appendManualAccountRunRecordIfNeeded(`step${message.step}_stopped`, null, '流程已被用户停止。');
|
||||
notifyStepError(message.step, '流程已被用户停止。');
|
||||
await setNodeStatus(nodeId, 'stopped');
|
||||
await appendManualAccountRunRecordIfNeeded(`node:${nodeId}:stopped`, null, '流程已被用户停止。');
|
||||
notifyNodeError(nodeId, '流程已被用户停止。');
|
||||
return { ok: true };
|
||||
}
|
||||
try {
|
||||
if (message.step === 3 && typeof finalizeStep3Completion === 'function') {
|
||||
if (nodeId === 'fill-password' && typeof finalizeStep3Completion === 'function') {
|
||||
await finalizeStep3Completion(message.payload || {});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -711,60 +804,74 @@
|
||||
const userMessage = typeof handleCloudflareSecurityBlocked === 'function'
|
||||
? await handleCloudflareSecurityBlocked(error)
|
||||
: (error?.message || String(error || ''));
|
||||
notifyStepError(message.step, '流程已被用户停止。');
|
||||
notifyNodeError(nodeId, '流程已被用户停止。');
|
||||
return { ok: true, error: userMessage };
|
||||
}
|
||||
const errorMessage = error?.message || String(error || '步骤 3 提交后确认失败');
|
||||
await setStepStatus(message.step, 'failed');
|
||||
await addLog(`失败:${errorMessage}`, 'error', { step: message.step });
|
||||
await appendManualAccountRunRecordIfNeeded(`step${message.step}_failed`, null, errorMessage);
|
||||
notifyStepError(message.step, errorMessage);
|
||||
await setNodeStatus(nodeId, 'failed');
|
||||
await addLog(`失败:${errorMessage}`, 'error', {
|
||||
nodeId,
|
||||
});
|
||||
await appendManualAccountRunRecordIfNeeded(`node:${nodeId}:failed`, null, errorMessage);
|
||||
notifyNodeError(nodeId, errorMessage);
|
||||
return { ok: true, error: errorMessage };
|
||||
}
|
||||
|
||||
const completionStateCandidate = await getState();
|
||||
const lastStepId = typeof getLastStepIdForState === 'function'
|
||||
? getLastStepIdForState(completionStateCandidate)
|
||||
: 10;
|
||||
const completionState = message.step === lastStepId ? completionStateCandidate : null;
|
||||
await setStepStatus(message.step, 'completed');
|
||||
await addLog('已完成', 'ok', { step: message.step });
|
||||
await handleStepData(message.step, message.payload);
|
||||
if (message.step === lastStepId && typeof appendAccountRunRecord === 'function') {
|
||||
const nodeIds = typeof getNodeIdsForState === 'function' ? getNodeIdsForState(completionStateCandidate) : [];
|
||||
const lastNodeId = nodeIds[nodeIds.length - 1] || '';
|
||||
const isFinalNode = nodeId === lastNodeId;
|
||||
const completionState = isFinalNode ? completionStateCandidate : null;
|
||||
await setNodeStatus(nodeId, 'completed');
|
||||
await addLog('已完成', 'ok', { nodeId });
|
||||
await handleStepData(resolvedStep, message.payload);
|
||||
if (isFinalNode && typeof appendAccountRunRecord === 'function') {
|
||||
await appendAccountRunRecord('success', completionState);
|
||||
}
|
||||
notifyStepComplete(message.step, message.payload);
|
||||
notifyNodeComplete(nodeId, message.payload);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'STEP_ERROR': {
|
||||
case 'NODE_ERROR': {
|
||||
const stateForNode = await getState();
|
||||
const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim();
|
||||
const resolvedStep = findStepByNodeId(nodeId, stateForNode);
|
||||
if (!nodeId || !resolvedStep) {
|
||||
throw new Error('NODE_ERROR 缺少 nodeId。');
|
||||
}
|
||||
const staleCheckState = await getState();
|
||||
if (isStaleAutoRunStepMessage(message.step, staleCheckState)) {
|
||||
await addLog(`自动运行:忽略过期的步骤 ${message.step} 失败消息,当前流程已在步骤 ${staleCheckState.currentStep || '未知'}。原始错误:${message.error || '未知错误'}`, 'warn', { step: message.step });
|
||||
if (isStaleAutoRunNodeMessage(nodeId, staleCheckState)) {
|
||||
await addLog(
|
||||
`自动运行:忽略过期的节点 ${nodeId} 失败消息,当前流程已在节点 ${staleCheckState.currentNodeId || '未知'}。原始错误:${message.error || '未知错误'}`,
|
||||
'warn',
|
||||
{ nodeId }
|
||||
);
|
||||
return { ok: true, ignored: true };
|
||||
}
|
||||
if (typeof isCloudflareSecurityBlockedError === 'function' && isCloudflareSecurityBlockedError(message.error)) {
|
||||
const userMessage = typeof handleCloudflareSecurityBlocked === 'function'
|
||||
? await handleCloudflareSecurityBlocked(message.error)
|
||||
: (typeof message.error === 'string' ? message.error : String(message.error || ''));
|
||||
notifyStepError(message.step, '流程已被用户停止。');
|
||||
notifyNodeError(nodeId, '流程已被用户停止。');
|
||||
return { ok: true, error: userMessage };
|
||||
}
|
||||
const currentState = await getState();
|
||||
const currentStepStatus = currentState?.stepStatuses?.[message.step] || '';
|
||||
const currentNodeStatus = currentState?.nodeStatuses?.[nodeId] || '';
|
||||
const isSignupPhonePasswordMismatch = /SIGNUP_PHONE_PASSWORD_MISMATCH::/i.test(String(message.error || ''));
|
||||
if (isStopError(message.error)) {
|
||||
await setStepStatus(message.step, 'stopped');
|
||||
await addLog('已被用户停止', 'warn', { step: message.step });
|
||||
await appendManualAccountRunRecordIfNeeded(`step${message.step}_stopped`, null, message.error);
|
||||
notifyStepError(message.step, message.error);
|
||||
await setNodeStatus(nodeId, 'stopped');
|
||||
await addLog('已被用户停止', 'warn', { nodeId });
|
||||
await appendManualAccountRunRecordIfNeeded(`node:${nodeId}:stopped`, null, message.error);
|
||||
notifyNodeError(nodeId, message.error);
|
||||
} else {
|
||||
if (!(isSignupPhonePasswordMismatch && currentStepStatus === 'failed')) {
|
||||
await setStepStatus(message.step, 'failed');
|
||||
await addLog(`失败:${message.error}`, 'error', { step: message.step });
|
||||
await appendManualAccountRunRecordIfNeeded(`step${message.step}_failed`, null, message.error);
|
||||
if (!(isSignupPhonePasswordMismatch && currentNodeStatus === 'failed')) {
|
||||
await setNodeStatus(nodeId, 'failed');
|
||||
await addLog(`失败:${message.error}`, 'error', {
|
||||
nodeId,
|
||||
});
|
||||
await appendManualAccountRunRecordIfNeeded(`node:${nodeId}:failed`, null, message.error);
|
||||
}
|
||||
notifyStepError(message.step, message.error);
|
||||
notifyNodeError(nodeId, message.error);
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -772,6 +879,7 @@
|
||||
case 'RESOLVE_PLUS_MANUAL_CONFIRMATION': {
|
||||
const currentState = await getState();
|
||||
const step = Number(message.payload?.step) || Number(currentState?.plusManualConfirmationStep) || 0;
|
||||
const confirmationNodeId = getStepKeyForState(step, currentState) || String(currentState?.currentNodeId || '').trim();
|
||||
const confirmed = Boolean(message.payload?.confirmed);
|
||||
const requestId = String(message.payload?.requestId || '').trim();
|
||||
const currentRequestId = String(currentState?.plusManualConfirmationRequestId || '').trim();
|
||||
@@ -818,7 +926,7 @@
|
||||
if (confirmed) {
|
||||
const methodLabel = method === 'gopay' ? 'GoPay' : '手动';
|
||||
await addLog(`步骤 ${step}:已确认${methodLabel}订阅完成,准备继续下一步。`, 'ok');
|
||||
await completeStepFromBackground(step, {
|
||||
await completeNodeFromBackground(confirmationNodeId, {
|
||||
plusManualConfirmationMethod: currentState?.plusManualConfirmationMethod || '',
|
||||
plusManualConfirmedAt: Date.now(),
|
||||
});
|
||||
@@ -828,10 +936,14 @@
|
||||
const cancelMessage = method === 'gopay'
|
||||
? '已取消 GoPay 订阅确认'
|
||||
: (isGpcOtp ? '已取消 GPC OTP 输入' : '已取消当前手动确认');
|
||||
await setStepStatus(step, 'failed');
|
||||
await setNodeStatus(confirmationNodeId, 'failed');
|
||||
await addLog(`步骤 ${step}:${cancelMessage}。`, 'warn');
|
||||
await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, null, cancelMessage);
|
||||
notifyStepError(step, cancelMessage);
|
||||
await appendManualAccountRunRecordIfNeeded(
|
||||
confirmationNodeId ? `node:${confirmationNodeId}:failed` : 'failed',
|
||||
null,
|
||||
cancelMessage
|
||||
);
|
||||
notifyNodeError(confirmationNodeId, cancelMessage);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -864,7 +976,7 @@
|
||||
case 'SET_CONTRIBUTION_MODE': {
|
||||
const enabled = Boolean(message.payload?.enabled);
|
||||
const state = await ensureManualInteractionAllowed(enabled ? '进入贡献模式' : '退出贡献模式');
|
||||
if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) {
|
||||
if (Object.values(state.nodeStatuses || {}).some((status) => status === 'running')) {
|
||||
throw new Error(enabled ? '当前有步骤正在执行,无法进入贡献模式。' : '当前有步骤正在执行,无法退出贡献模式。');
|
||||
}
|
||||
if (typeof setContributionMode !== 'function') {
|
||||
@@ -878,7 +990,7 @@
|
||||
|
||||
case 'START_CONTRIBUTION_FLOW': {
|
||||
const state = await ensureManualInteractionAllowed('开始贡献');
|
||||
if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) {
|
||||
if (Object.values(state.nodeStatuses || {}).some((status) => status === 'running')) {
|
||||
throw new Error('当前有步骤正在执行,无法开始贡献流程。');
|
||||
}
|
||||
if (typeof startContributionFlow !== 'function') {
|
||||
@@ -950,18 +1062,23 @@
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
|
||||
case 'EXECUTE_STEP': {
|
||||
case 'EXECUTE_NODE': {
|
||||
clearStopRequest();
|
||||
const requestState = await getState();
|
||||
const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim();
|
||||
const resolvedStep = findStepByNodeId(nodeId, requestState);
|
||||
if (!nodeId || !resolvedStep) {
|
||||
throw new Error('EXECUTE_NODE 缺少 nodeId。');
|
||||
}
|
||||
if (message.source === 'sidepanel') {
|
||||
await lockAutomationWindowFromMessage(message, sender);
|
||||
await ensureManualInteractionAllowed('手动执行步骤');
|
||||
}
|
||||
const step = message.payload.step;
|
||||
if (message.source === 'sidepanel') {
|
||||
await ensureManualStepPrerequisites(step);
|
||||
await ensureManualInteractionAllowed('手动执行节点');
|
||||
}
|
||||
if (message.source === 'sidepanel') {
|
||||
await invalidateDownstreamAfterStepRestart(step, { logLabel: `步骤 ${step} 重新执行` });
|
||||
await ensureManualStepPrerequisites(resolvedStep);
|
||||
}
|
||||
if (message.source === 'sidepanel') {
|
||||
await invalidateDownstreamAfterStepRestart(resolvedStep, { logLabel: `节点 ${nodeId} 重新执行` });
|
||||
}
|
||||
if (message.payload.email) {
|
||||
await setEmailState(message.payload.email);
|
||||
@@ -971,10 +1088,10 @@
|
||||
await setState({ emailPrefix: message.payload.emailPrefix });
|
||||
}
|
||||
const executionState = await getState();
|
||||
if (doesStepUseCompletionSignal(step, executionState)) {
|
||||
await executeStepViaCompletionSignal(step);
|
||||
if (doesNodeUseCompletionSignal(nodeId, executionState)) {
|
||||
await executeNodeViaCompletionSignal(nodeId);
|
||||
} else {
|
||||
await executeStep(step);
|
||||
await executeNode(nodeId);
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -1094,9 +1211,12 @@
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'SKIP_STEP': {
|
||||
const step = Number(message.payload?.step);
|
||||
return await skipStep(step);
|
||||
case 'SKIP_NODE': {
|
||||
const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim();
|
||||
if (!nodeId) {
|
||||
throw new Error('SKIP_NODE 缺少 nodeId。');
|
||||
}
|
||||
return await skipNode(nodeId);
|
||||
}
|
||||
|
||||
case 'SAVE_SETTING': {
|
||||
@@ -1156,10 +1276,11 @@
|
||||
}
|
||||
if (stepModeChanged && typeof getStepIdsForState === 'function') {
|
||||
const nextStateForSteps = { ...currentState, ...stateUpdates };
|
||||
stateUpdates.stepStatuses = Object.fromEntries(
|
||||
getStepIdsForState(nextStateForSteps).map((stepId) => [stepId, 'pending'])
|
||||
);
|
||||
stateUpdates.currentStep = 0;
|
||||
const nextNodeIds = typeof getNodeIdsForState === 'function'
|
||||
? getNodeIdsForState(nextStateForSteps)
|
||||
: getStepIdsForState(nextStateForSteps).map((stepId) => getStepKeyForState(stepId, nextStateForSteps)).filter(Boolean);
|
||||
stateUpdates.nodeStatuses = Object.fromEntries(nextNodeIds.map((nodeId) => [nodeId, 'pending']));
|
||||
stateUpdates.currentNodeId = '';
|
||||
}
|
||||
await setState(stateUpdates);
|
||||
const mergedState = await getState();
|
||||
|
||||
+52
-99
@@ -4,8 +4,7 @@
|
||||
function createRuntimeStateHelpers(deps = {}) {
|
||||
const {
|
||||
DEFAULT_ACTIVE_FLOW_ID = 'openai',
|
||||
defaultStepStatuses = {},
|
||||
getStepDefinitionForState = null,
|
||||
defaultNodeStatuses = {},
|
||||
} = deps;
|
||||
|
||||
const RUNTIME_SHARED_FIELDS = Object.freeze([
|
||||
@@ -150,11 +149,6 @@
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function normalizeStepNumber(value) {
|
||||
const step = Math.floor(Number(value) || 0);
|
||||
return step > 0 ? step : 0;
|
||||
}
|
||||
|
||||
function normalizeNodeStatus(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
@@ -163,73 +157,26 @@
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function buildDefaultStepStatuses() {
|
||||
function buildDefaultNodeStatuses() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(normalizePlainObject(defaultStepStatuses)).map(([key, value]) => [
|
||||
Object.entries(normalizePlainObject(defaultNodeStatuses)).map(([key, value]) => [
|
||||
String(key),
|
||||
normalizeNodeStatus(value),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeStepStatuses(value) {
|
||||
const base = buildDefaultStepStatuses();
|
||||
function normalizeNodeStatuses(value) {
|
||||
const base = buildDefaultNodeStatuses();
|
||||
if (!isPlainObject(value)) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const next = { ...base };
|
||||
for (const [key, status] of Object.entries(value)) {
|
||||
const step = normalizeStepNumber(key);
|
||||
if (!step) continue;
|
||||
next[String(step)] = normalizeNodeStatus(status);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizeLegacyStepCompat(value = {}, state = {}) {
|
||||
const candidate = normalizePlainObject(value);
|
||||
const currentStep = normalizeStepNumber(
|
||||
Object.prototype.hasOwnProperty.call(state, 'currentStep')
|
||||
? state.currentStep
|
||||
: candidate.currentStep
|
||||
);
|
||||
const stepStatuses = normalizeStepStatuses(
|
||||
Object.prototype.hasOwnProperty.call(state, 'stepStatuses')
|
||||
? state.stepStatuses
|
||||
: candidate.stepStatuses
|
||||
);
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
stepStatuses,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveStepKey(step, state = {}) {
|
||||
const numericStep = normalizeStepNumber(step);
|
||||
if (!numericStep || typeof getStepDefinitionForState !== 'function') {
|
||||
return '';
|
||||
}
|
||||
return String(getStepDefinitionForState(numericStep, state)?.key || '').trim();
|
||||
}
|
||||
|
||||
function normalizeNodeStatuses(value, state = {}, legacyStepCompat = null) {
|
||||
if (isPlainObject(value) && Object.keys(value).length > 0) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.map(([key, status]) => [String(key || '').trim(), normalizeNodeStatus(status)])
|
||||
.filter(([key]) => Boolean(key))
|
||||
);
|
||||
}
|
||||
|
||||
const compat = legacyStepCompat || normalizeLegacyStepCompat({}, state);
|
||||
const next = {};
|
||||
for (const [stepKey, status] of Object.entries(compat.stepStatuses || {})) {
|
||||
const step = normalizeStepNumber(stepKey);
|
||||
if (!step) continue;
|
||||
const nodeKey = resolveStepKey(step, state) || `step-${step}`;
|
||||
next[nodeKey] = normalizeNodeStatus(status);
|
||||
const nodeId = String(key || '').trim();
|
||||
if (!nodeId) continue;
|
||||
next[nodeId] = normalizeNodeStatus(status);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
@@ -298,6 +245,8 @@
|
||||
|
||||
function buildRuntimeStateDefault() {
|
||||
return {
|
||||
flowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
runId: '',
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
activeRunId: '',
|
||||
currentNodeId: '',
|
||||
@@ -316,10 +265,6 @@
|
||||
identity: {},
|
||||
},
|
||||
},
|
||||
legacyStepCompat: {
|
||||
currentStep: 0,
|
||||
stepStatuses: buildDefaultStepStatuses(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -329,30 +274,45 @@
|
||||
...cloneValue(normalizePlainObject(state.runtimeState)),
|
||||
};
|
||||
const activeFlowId = normalizeFlowId(
|
||||
Object.prototype.hasOwnProperty.call(state, 'activeFlowId')
|
||||
Object.prototype.hasOwnProperty.call(state, 'flowId')
|
||||
? state.flowId
|
||||
: Object.prototype.hasOwnProperty.call(state, 'activeFlowId')
|
||||
? state.activeFlowId
|
||||
: Object.prototype.hasOwnProperty.call(baseRuntimeState, 'flowId')
|
||||
? baseRuntimeState.flowId
|
||||
: baseRuntimeState.activeFlowId
|
||||
);
|
||||
const legacyStepCompat = normalizeLegacyStepCompat(baseRuntimeState.legacyStepCompat, state);
|
||||
const currentNodeId = String(
|
||||
Object.prototype.hasOwnProperty.call(state, 'currentNodeId')
|
||||
? state.currentNodeId
|
||||
: (baseRuntimeState.currentNodeId || resolveStepKey(legacyStepCompat.currentStep, state))
|
||||
: baseRuntimeState.currentNodeId
|
||||
).trim();
|
||||
const nodeStatuses = normalizeNodeStatuses(
|
||||
Object.prototype.hasOwnProperty.call(state, 'nodeStatuses')
|
||||
? state.nodeStatuses
|
||||
: baseRuntimeState.nodeStatuses,
|
||||
state,
|
||||
legacyStepCompat
|
||||
: baseRuntimeState.nodeStatuses
|
||||
);
|
||||
|
||||
return {
|
||||
...baseRuntimeState,
|
||||
flowId: activeFlowId,
|
||||
activeFlowId,
|
||||
activeRunId: normalizeRunId(
|
||||
Object.prototype.hasOwnProperty.call(state, 'activeRunId')
|
||||
runId: normalizeRunId(
|
||||
Object.prototype.hasOwnProperty.call(state, 'runId')
|
||||
? state.runId
|
||||
: Object.prototype.hasOwnProperty.call(state, 'activeRunId')
|
||||
? state.activeRunId
|
||||
: Object.prototype.hasOwnProperty.call(baseRuntimeState, 'runId')
|
||||
? baseRuntimeState.runId
|
||||
: baseRuntimeState.activeRunId
|
||||
),
|
||||
activeRunId: normalizeRunId(
|
||||
Object.prototype.hasOwnProperty.call(state, 'runId')
|
||||
? state.runId
|
||||
: Object.prototype.hasOwnProperty.call(state, 'activeRunId')
|
||||
? state.activeRunId
|
||||
: Object.prototype.hasOwnProperty.call(baseRuntimeState, 'runId')
|
||||
? baseRuntimeState.runId
|
||||
: baseRuntimeState.activeRunId
|
||||
),
|
||||
currentNodeId,
|
||||
@@ -360,16 +320,18 @@
|
||||
sharedState: buildSharedState(baseRuntimeState.sharedState, state),
|
||||
serviceState: buildServiceState(baseRuntimeState.serviceState, state),
|
||||
flowState: buildOpenAiFlowState(baseRuntimeState.flowState, state),
|
||||
legacyStepCompat,
|
||||
};
|
||||
}
|
||||
|
||||
function buildFlattenedUpdates(updates = {}) {
|
||||
const next = {
|
||||
...updates,
|
||||
};
|
||||
const ignoredKeys = new Set(['current' + 'Step', 'step' + 'Statuses', 'legacy' + 'StepCompat']);
|
||||
const next = {};
|
||||
for (const [key, value] of Object.entries(updates || {})) {
|
||||
if (!ignoredKeys.has(key)) {
|
||||
next[key] = value;
|
||||
}
|
||||
}
|
||||
const runtimeState = normalizePlainObject(updates.runtimeState);
|
||||
const legacyStepCompat = normalizePlainObject(updates.legacyStepCompat);
|
||||
const sharedState = normalizePlainObject(updates.sharedState);
|
||||
const serviceState = normalizePlainObject(updates.serviceState);
|
||||
const flowState = normalizePlainObject(updates.flowState);
|
||||
@@ -377,31 +339,23 @@
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeFlowId')) {
|
||||
next.activeFlowId = runtimeState.activeFlowId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'flowId')) {
|
||||
next.flowId = runtimeState.flowId;
|
||||
next.activeFlowId = runtimeState.flowId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'activeRunId')) {
|
||||
next.activeRunId = runtimeState.activeRunId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'runId')) {
|
||||
next.runId = runtimeState.runId;
|
||||
next.activeRunId = runtimeState.runId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'currentNodeId')) {
|
||||
next.currentNodeId = runtimeState.currentNodeId;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'nodeStatuses')) {
|
||||
next.nodeStatuses = cloneValue(runtimeState.nodeStatuses);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(legacyStepCompat, 'currentStep')) {
|
||||
next.currentStep = legacyStepCompat.currentStep;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(legacyStepCompat, 'stepStatuses')) {
|
||||
next.stepStatuses = cloneValue(legacyStepCompat.stepStatuses);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'legacyStepCompat')) {
|
||||
const compatCandidate = normalizePlainObject(runtimeState.legacyStepCompat);
|
||||
if (Object.prototype.hasOwnProperty.call(compatCandidate, 'currentStep')) {
|
||||
next.currentStep = compatCandidate.currentStep;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(compatCandidate, 'stepStatuses')) {
|
||||
next.stepStatuses = cloneValue(compatCandidate.stepStatuses);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(next, pickDefinedFields(sharedState, RUNTIME_SHARED_FIELDS));
|
||||
if (Object.prototype.hasOwnProperty.call(runtimeState, 'sharedState')) {
|
||||
Object.assign(
|
||||
@@ -432,6 +386,8 @@
|
||||
const runtimeState = ensureRuntimeState(state);
|
||||
return {
|
||||
...state,
|
||||
flowId: runtimeState.flowId,
|
||||
runId: runtimeState.runId,
|
||||
activeFlowId: runtimeState.activeFlowId,
|
||||
activeRunId: runtimeState.activeRunId,
|
||||
currentNodeId: runtimeState.currentNodeId,
|
||||
@@ -439,9 +395,6 @@
|
||||
flowState: cloneValue(runtimeState.flowState),
|
||||
sharedState: cloneValue(runtimeState.sharedState),
|
||||
serviceState: cloneValue(runtimeState.serviceState),
|
||||
legacyStepCompat: cloneValue(runtimeState.legacyStepCompat),
|
||||
currentStep: runtimeState.legacyStepCompat.currentStep,
|
||||
stepStatuses: cloneValue(runtimeState.legacyStepCompat.stepStatuses),
|
||||
runtimeState,
|
||||
};
|
||||
}
|
||||
@@ -456,12 +409,12 @@
|
||||
|
||||
return {
|
||||
...flattenedUpdates,
|
||||
flowId: runtimeState.flowId,
|
||||
runId: runtimeState.runId,
|
||||
activeFlowId: runtimeState.activeFlowId,
|
||||
activeRunId: runtimeState.activeRunId,
|
||||
currentNodeId: runtimeState.currentNodeId,
|
||||
nodeStatuses: cloneValue(runtimeState.nodeStatuses),
|
||||
currentStep: runtimeState.legacyStepCompat.currentStep,
|
||||
stepStatuses: cloneValue(runtimeState.legacyStepCompat.stepStatuses),
|
||||
runtimeState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
chrome,
|
||||
cleanupStep8NavigationListeners,
|
||||
clickWithDebugger,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
ensureStep8SignupPageReady,
|
||||
getOAuthFlowRemainingMs,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
@@ -122,7 +122,7 @@
|
||||
cleanupListener();
|
||||
|
||||
addStepLog(visibleStep, `已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => {
|
||||
return completeStepFromBackground(visibleStep, { localhostUrl: callbackUrl });
|
||||
return completeNodeFromBackground(state?.nodeId || 'confirm-oauth', { localhostUrl: callbackUrl });
|
||||
}).then(() => {
|
||||
resolve();
|
||||
}).catch((err) => {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
const {
|
||||
addLog: rawAddLog = async () => {},
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
createAutomationTab = null,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
fetch: fetchImpl = null,
|
||||
@@ -457,7 +457,7 @@
|
||||
gopayHelperOrderCreatedAt: result.orderCreatedAt || Date.now(),
|
||||
});
|
||||
await addLog(`步骤 6:GPC ${result.phoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
|
||||
await completeStepFromBackground(6, {
|
||||
await completeNodeFromBackground('plus-checkout-create', {
|
||||
plusCheckoutCountry: result.country || 'ID',
|
||||
plusCheckoutCurrency: result.currency || 'IDR',
|
||||
plusCheckoutSource: result.checkoutSource,
|
||||
@@ -517,7 +517,7 @@
|
||||
|
||||
await addLog(`步骤 6:Plus Checkout 页面已就绪(${paymentMethodLabel} / ${result.country || 'DE'} ${result.currency || 'EUR'}),准备继续下一步。`, 'info');
|
||||
|
||||
await completeStepFromBackground(6, {
|
||||
await completeNodeFromBackground('plus-checkout-create', {
|
||||
plusCheckoutCountry: result.country || 'DE',
|
||||
plusCheckoutCurrency: result.currency || 'EUR',
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
chrome,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
CLOUD_MAIL_PROVIDER = 'cloudmail',
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
confirmCustomVerificationStepBypass,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureIcloudMailSession,
|
||||
@@ -225,8 +225,8 @@
|
||||
`步骤 ${visibleStep}:当前认证页已进入 OAuth 授权页${fromRecovery ? '(轮询失败后复核)' : ''},跳过登录验证码拉取并继续后续流程。`,
|
||||
'warn'
|
||||
);
|
||||
if (typeof completeStepFromBackground === 'function') {
|
||||
await completeStepFromBackground(visibleStep, {
|
||||
if (typeof completeNodeFromBackground === 'function') {
|
||||
await completeNodeFromBackground(options.nodeId || 'fetch-login-code', {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
directOAuthConsentPage: true,
|
||||
@@ -316,7 +316,7 @@
|
||||
),
|
||||
});
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { fromRecovery: true });
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { fromRecovery: true, nodeId: currentState?.nodeId });
|
||||
return { outcome: 'completed' };
|
||||
}
|
||||
if (pageState?.state === 'verification_page' || pageState?.state === 'phone_verification_page' || pageState?.state === 'add_email_page') {
|
||||
@@ -401,7 +401,7 @@
|
||||
visibleStep,
|
||||
});
|
||||
|
||||
await completeStepFromBackground(visibleStep, {
|
||||
await completeNodeFromBackground(state?.nodeId || 'fetch-login-code', {
|
||||
phoneVerification: true,
|
||||
loginPhoneVerification: true,
|
||||
code: result?.code || '',
|
||||
@@ -438,7 +438,7 @@
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
|
||||
});
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { nodeId: state?.nodeId });
|
||||
return;
|
||||
}
|
||||
if (pageState?.state === 'phone_verification_page') {
|
||||
@@ -450,7 +450,7 @@
|
||||
preparedState = addEmailPreparation?.state || preparedState;
|
||||
pageState = addEmailPreparation?.pageState || pageState;
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { nodeId: preparedState?.nodeId || state?.nodeId });
|
||||
return;
|
||||
}
|
||||
if (pageState?.state === 'phone_verification_page') {
|
||||
@@ -576,7 +576,7 @@
|
||||
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
||||
let retryWithoutStep7Streak = 0;
|
||||
const maxRetryWithoutStep7Streak = 3;
|
||||
let currentStepRecoveryAttempt = 0;
|
||||
let currentNodeRecoveryAttempt = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
@@ -601,8 +601,8 @@
|
||||
let retryWithoutStep7 = false;
|
||||
|
||||
if (isStep8EmailInUseError(currentError) || isStep8MaxCheckAttemptsError(currentError)) {
|
||||
currentStepRecoveryAttempt += 1;
|
||||
if (currentStepRecoveryAttempt > STEP8_CURRENT_STEP_RECOVERY_MAX_ATTEMPTS) {
|
||||
currentNodeRecoveryAttempt += 1;
|
||||
if (currentNodeRecoveryAttempt > STEP8_CURRENT_STEP_RECOVERY_MAX_ATTEMPTS) {
|
||||
throw currentError;
|
||||
}
|
||||
if (isStep8EmailInUseError(currentError)) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
confirmCustomVerificationStepBypass,
|
||||
generateRandomBirthday,
|
||||
generateRandomName,
|
||||
@@ -83,7 +83,7 @@
|
||||
return result || {};
|
||||
}
|
||||
|
||||
await completeStepFromBackground(4, {
|
||||
await completeNodeFromBackground('fetch-signup-code', {
|
||||
phoneVerification: true,
|
||||
code: result?.code || '',
|
||||
...(result?.skipProfileStep ? { skipProfileStep: true } : {}),
|
||||
@@ -280,7 +280,7 @@
|
||||
throw new Error(prepareResult.error);
|
||||
}
|
||||
if (prepareResult?.alreadyVerified) {
|
||||
await completeStepFromBackground(4, prepareResult?.skipProfileStep ? { skipProfileStep: true } : {});
|
||||
await completeNodeFromBackground('fetch-signup-code', prepareResult?.skipProfileStep ? { skipProfileStep: true } : {});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,8 @@
|
||||
`步骤 3:正在填写密码,${identityLabel},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
|
||||
);
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'fill-password',
|
||||
step: 3,
|
||||
source: 'background',
|
||||
payload: {
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
addLog: rawAddLog = async () => {},
|
||||
broadcastDataUpdate,
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
fetch: fetchImpl = null,
|
||||
generateRandomName,
|
||||
@@ -1018,7 +1018,7 @@
|
||||
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||
});
|
||||
await addLog('步骤 7:GPC 任务已完成,准备继续下一步。', 'ok');
|
||||
await completeStepFromBackground(7, {
|
||||
await completeNodeFromBackground('plus-checkout-billing', {
|
||||
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||
});
|
||||
return;
|
||||
@@ -1920,7 +1920,7 @@
|
||||
throw new Error(`步骤 7:多次提交账单地址后仍未跳转到 ${paymentConfig.label}。${lastSubmitError}`);
|
||||
}
|
||||
|
||||
await completeStepFromBackground(7, {
|
||||
await completeNodeFromBackground('plus-checkout-billing', {
|
||||
plusBillingCountryText: result?.countryText || '',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,10 +16,17 @@
|
||||
await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
|
||||
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'fill-profile',
|
||||
step: 5,
|
||||
source: 'background',
|
||||
payload: { firstName, lastName, year, month, day },
|
||||
payload: {
|
||||
firstName,
|
||||
lastName,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
@@ -1433,7 +1433,7 @@
|
||||
}
|
||||
|
||||
await setState({ plusGoPayApprovedAt: Date.now() });
|
||||
await completeStepFromBackground(8, {
|
||||
await completeNodeFromBackground('paypal-approve', {
|
||||
plusGoPayApprovedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
function createStep7Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
getErrorMessage,
|
||||
getLoginAuthStateLabel,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
@@ -131,7 +131,7 @@
|
||||
step: completionStep,
|
||||
visibleStep: completionStep,
|
||||
});
|
||||
await completeStepFromBackground(completionStep, {
|
||||
await completeNodeFromBackground(state?.nodeId || 'oauth-login', {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
directOAuthConsentPage: true,
|
||||
@@ -225,7 +225,8 @@
|
||||
const result = await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
{
|
||||
type: 'EXECUTE_STEP',
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'oauth-login',
|
||||
step: 7,
|
||||
source: 'background',
|
||||
payload: {
|
||||
@@ -277,7 +278,7 @@
|
||||
completionPayload.directOAuthConsentPage = Boolean(result.directOAuthConsentPage);
|
||||
}
|
||||
|
||||
await completeStepFromBackground(completionStep, completionPayload);
|
||||
await completeNodeFromBackground(state?.nodeId || 'oauth-login', completionPayload);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
const {
|
||||
addLog,
|
||||
chrome: chromeApi = globalThis.chrome,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
openSignupEntryTab,
|
||||
} = deps;
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
await clearOpenAiCookiesBeforeStep1();
|
||||
await addLog('步骤 1:正在打开 ChatGPT 官网...');
|
||||
await openSignupEntryTab(1);
|
||||
await completeStepFromBackground(1, {});
|
||||
await completeNodeFromBackground('open-chatgpt', {});
|
||||
}
|
||||
|
||||
return { executeStep1 };
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
@@ -294,7 +294,7 @@
|
||||
await sleepWithStop(500);
|
||||
}
|
||||
|
||||
await completeStepFromBackground(8, {
|
||||
await completeNodeFromBackground('paypal-approve', {
|
||||
plusPaypalApprovedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
addLog,
|
||||
chrome,
|
||||
closeConflictingTabsForSource,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTab,
|
||||
getPanelMode,
|
||||
getTabId,
|
||||
@@ -253,7 +253,7 @@
|
||||
|
||||
if (shouldBypassStep9ForLocalCpa(state)) {
|
||||
await addStepLog(platformVerifyStep, '检测到本地 CPA,且当前策略为“跳过平台回调验证”,本轮不再重复提交回调地址。', 'info');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
await completeNodeFromBackground(state?.nodeId || 'platform-verify', {
|
||||
localhostUrl: state.localhostUrl,
|
||||
verifiedStatus: 'local-auto',
|
||||
});
|
||||
@@ -286,7 +286,7 @@
|
||||
|| normalizeString(result?.status_message)
|
||||
|| 'CPA 已通过接口提交回调';
|
||||
await addStepLog(platformVerifyStep, verifiedStatus, 'ok');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
await completeNodeFromBackground(state?.nodeId || 'platform-verify', {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
});
|
||||
@@ -336,7 +336,7 @@
|
||||
|
||||
const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功';
|
||||
await addStepLog(platformVerifyStep, verifiedStatus, 'ok');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
await completeNodeFromBackground(state?.nodeId || 'platform-verify', {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
});
|
||||
@@ -381,7 +381,7 @@
|
||||
logOptions: { step: visibleStep, stepKey: 'platform-verify' },
|
||||
timeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
|
||||
});
|
||||
await completeStepFromBackground(platformVerifyStep, result);
|
||||
await completeNodeFromBackground(state?.nodeId || 'platform-verify', result);
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
function createPlusReturnConfirmExecutor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
setState,
|
||||
@@ -53,7 +53,7 @@
|
||||
plusCheckoutTabId: tabId,
|
||||
plusReturnUrl: tab?.url || '',
|
||||
});
|
||||
await completeStepFromBackground(9, {
|
||||
await completeNodeFromBackground('plus-checkout-return', {
|
||||
plusReturnUrl: tab?.url || '',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,48 +1,49 @@
|
||||
(function attachBackgroundStepRegistry(root, factory) {
|
||||
root.MultiPageBackgroundStepRegistry = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStepRegistryModule() {
|
||||
function createStepRegistry(definitions = []) {
|
||||
function createNodeRegistry(definitions = []) {
|
||||
const ordered = (Array.isArray(definitions) ? definitions : [])
|
||||
.map((definition) => ({
|
||||
id: Number(definition?.id),
|
||||
order: Number(definition?.order),
|
||||
key: String(definition?.key || '').trim(),
|
||||
nodeId: String(definition?.nodeId || definition?.key || '').trim(),
|
||||
displayOrder: Number(definition?.displayOrder ?? definition?.order),
|
||||
executeKey: String(definition?.executeKey || definition?.key || definition?.nodeId || '').trim(),
|
||||
title: String(definition?.title || '').trim(),
|
||||
execute: definition?.execute,
|
||||
}))
|
||||
.filter((definition) => Number.isFinite(definition.id) && typeof definition.execute === 'function')
|
||||
.filter((definition) => definition.nodeId && typeof definition.execute === 'function')
|
||||
.sort((left, right) => {
|
||||
const leftOrder = Number.isFinite(left.order) ? left.order : left.id;
|
||||
const rightOrder = Number.isFinite(right.order) ? right.order : right.id;
|
||||
return leftOrder - rightOrder;
|
||||
const leftOrder = Number.isFinite(left.displayOrder) ? left.displayOrder : 0;
|
||||
const rightOrder = Number.isFinite(right.displayOrder) ? right.displayOrder : 0;
|
||||
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
||||
return left.nodeId.localeCompare(right.nodeId);
|
||||
});
|
||||
|
||||
const byId = new Map(ordered.map((definition) => [definition.id, definition]));
|
||||
const byId = new Map(ordered.map((definition) => [definition.nodeId, definition]));
|
||||
|
||||
function getStepDefinition(step) {
|
||||
return byId.get(Number(step)) || null;
|
||||
function getNodeDefinition(nodeId) {
|
||||
return byId.get(String(nodeId || '').trim()) || null;
|
||||
}
|
||||
|
||||
function getOrderedSteps() {
|
||||
function getOrderedNodes() {
|
||||
return ordered.slice();
|
||||
}
|
||||
|
||||
function executeStep(step, state) {
|
||||
const definition = getStepDefinition(step);
|
||||
function executeNode(nodeId, state) {
|
||||
const definition = getNodeDefinition(nodeId);
|
||||
if (!definition) {
|
||||
throw new Error(`未知步骤:${step}`);
|
||||
throw new Error(`未知节点:${nodeId}`);
|
||||
}
|
||||
return definition.execute(state);
|
||||
}
|
||||
|
||||
return {
|
||||
executeStep,
|
||||
getOrderedSteps,
|
||||
getStepDefinition,
|
||||
executeNode,
|
||||
getNodeDefinition,
|
||||
getOrderedNodes,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createStepRegistry,
|
||||
createNodeRegistry,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
const {
|
||||
addLog,
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
ensureContentScriptReadyOnTab,
|
||||
ensureSignupAuthEntryPageReady,
|
||||
ensureSignupEntryPageReady,
|
||||
@@ -152,7 +152,8 @@
|
||||
|
||||
try {
|
||||
return await sendToContentScriptResilient('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'submit-signup-email',
|
||||
step: 2,
|
||||
source: 'background',
|
||||
payload,
|
||||
@@ -391,7 +392,7 @@
|
||||
skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
|
||||
});
|
||||
|
||||
await completeStepFromBackground(2, {
|
||||
await completeNodeFromBackground('submit-signup-email', {
|
||||
accountIdentifierType: 'phone',
|
||||
accountIdentifier: phoneNumber,
|
||||
signupPhoneNumber: phoneNumber,
|
||||
@@ -484,7 +485,7 @@
|
||||
skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
|
||||
});
|
||||
|
||||
await completeStepFromBackground(2, {
|
||||
await completeNodeFromBackground('submit-signup-email', {
|
||||
email: resolvedEmail,
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: resolvedEmail,
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
const {
|
||||
addLog = async () => {},
|
||||
chrome: chromeApi = globalThis.chrome,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
getErrorMessage = (error) => error?.message || String(error || '未知错误'),
|
||||
registrationSuccessWaitMs = DEFAULT_REGISTRATION_SUCCESS_WAIT_MS,
|
||||
sleepWithStop = async (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0))),
|
||||
@@ -149,7 +149,7 @@
|
||||
}
|
||||
await clearCookiesIfEnabled(state);
|
||||
await addLog('步骤 6:注册成功等待完成,准备继续获取 OAuth 链接并登录。', 'ok');
|
||||
await completeStepFromBackground(6);
|
||||
await completeNodeFromBackground('wait-registration-success');
|
||||
}
|
||||
|
||||
return { executeStep6 };
|
||||
|
||||
@@ -593,7 +593,7 @@
|
||||
|
||||
function getContentScriptResponseTimeoutMs(message) {
|
||||
if (!message || typeof message !== 'object') return 30000;
|
||||
if (message.type === 'EXECUTE_STEP' && Number(message.step) === 6) return 75000;
|
||||
if (message.type === 'EXECUTE_NODE' && String(message.nodeId || message.payload?.nodeId || '').trim() === 'wait-registration-success') return 75000;
|
||||
if (message.type === 'POLL_EMAIL') {
|
||||
const maxAttempts = Math.max(1, Number(message.payload?.maxAttempts) || 1);
|
||||
const intervalMs = Math.max(0, Number(message.payload?.intervalMs) || 0);
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
closeConflictingTabsForSource,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
CLOUD_MAIL_PROVIDER = 'cloudmail',
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
confirmCustomVerificationStepBypassRequest,
|
||||
getNodeIdByStepForState,
|
||||
getHotmailVerificationPollConfig,
|
||||
getHotmailVerificationRequestTimestamp,
|
||||
handleMail2925LimitReachedError,
|
||||
@@ -32,6 +33,7 @@
|
||||
sendToContentScript,
|
||||
sendToContentScriptResilient,
|
||||
sendToMailContentScriptResilient,
|
||||
setNodeStatus,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
throwIfStopped,
|
||||
@@ -65,6 +67,13 @@
|
||||
return rawAddLog(normalizeVerificationLogMessage(message), level, normalizedOptions);
|
||||
}
|
||||
|
||||
async function getNodeIdForStep(step) {
|
||||
const state = typeof getState === 'function' ? await getState() : {};
|
||||
return typeof getNodeIdByStepForState === 'function'
|
||||
? String(getNodeIdByStepForState(step, state) || '').trim()
|
||||
: '';
|
||||
}
|
||||
|
||||
const isRetryableVerificationTransportError = typeof deps.isRetryableContentScriptTransportError === 'function'
|
||||
? deps.isRetryableContentScriptTransportError
|
||||
: ((error) => /back\/forward cache|message channel is closed|Receiving end does not exist|port closed before a response was received|A listener indicated an asynchronous response|did not respond in \d+s/i.test(
|
||||
@@ -404,7 +413,11 @@
|
||||
signupVerificationRequestedAt: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
});
|
||||
await deps.setStepStatus(completionStep, 'skipped');
|
||||
const completionNodeId = await getNodeIdForStep(completionStep);
|
||||
if (!completionNodeId) {
|
||||
throw new Error(`步骤 ${completionStep} 未映射到验证码节点。`);
|
||||
}
|
||||
await setNodeStatus(completionNodeId, 'skipped');
|
||||
await addLog(`步骤 ${completionStep}:已确认手动完成${verificationLabel}验证码输入,当前步骤已跳过。`, 'warn');
|
||||
}
|
||||
|
||||
@@ -1387,7 +1400,11 @@
|
||||
[stateKey]: result.code,
|
||||
});
|
||||
|
||||
await completeStepFromBackground(completionStep, {
|
||||
const completionNodeId = await getNodeIdForStep(completionStep);
|
||||
if (!completionNodeId) {
|
||||
throw new Error(`步骤 ${completionStep} 未映射到验证码节点。`);
|
||||
}
|
||||
await completeNodeFromBackground(completionNodeId, {
|
||||
emailTimestamp: result.emailTimestamp,
|
||||
code: result.code,
|
||||
phoneVerificationRequired: Boolean(submitResult.addPhonePage),
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
(function attachBackgroundWorkflowEngine(root, factory) {
|
||||
root.MultiPageBackgroundWorkflowEngine = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundWorkflowEngineModule() {
|
||||
function createWorkflowEngine(deps = {}) {
|
||||
const {
|
||||
defaultFlowId = 'openai',
|
||||
workflowDefinitions = null,
|
||||
} = deps;
|
||||
|
||||
function normalizeFlowId(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized || defaultFlowId;
|
||||
}
|
||||
|
||||
function normalizeNodeId(value = '') {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function resolveStateFlowId(state = {}) {
|
||||
return normalizeFlowId(state?.flowId || state?.activeFlowId || defaultFlowId);
|
||||
}
|
||||
|
||||
function getWorkflow(options = {}) {
|
||||
const flowId = normalizeFlowId(options?.flowId || options?.activeFlowId || defaultFlowId);
|
||||
if (workflowDefinitions?.getWorkflow) {
|
||||
return workflowDefinitions.getWorkflow({
|
||||
...options,
|
||||
activeFlowId: flowId,
|
||||
flowId,
|
||||
});
|
||||
}
|
||||
return {
|
||||
flowId,
|
||||
workflowVersion: 1,
|
||||
nodes: [],
|
||||
nodeIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
function getWorkflowForState(state = {}) {
|
||||
return getWorkflow({
|
||||
...state,
|
||||
flowId: resolveStateFlowId(state),
|
||||
activeFlowId: resolveStateFlowId(state),
|
||||
});
|
||||
}
|
||||
|
||||
function getNodesForState(state = {}) {
|
||||
return Array.isArray(getWorkflowForState(state).nodes)
|
||||
? getWorkflowForState(state).nodes
|
||||
: [];
|
||||
}
|
||||
|
||||
function getNodeIdsForState(state = {}) {
|
||||
return getNodesForState(state).map((node) => node.nodeId).filter(Boolean);
|
||||
}
|
||||
|
||||
function getNodeById(nodeId, state = {}) {
|
||||
const normalizedNodeId = normalizeNodeId(nodeId);
|
||||
if (!normalizedNodeId) {
|
||||
return null;
|
||||
}
|
||||
return getNodesForState(state).find((node) => node.nodeId === normalizedNodeId) || null;
|
||||
}
|
||||
|
||||
function getDisplayOrderForNode(nodeId, state = {}) {
|
||||
const node = getNodeById(nodeId, state);
|
||||
return Number.isFinite(Number(node?.displayOrder)) ? Number(node.displayOrder) : null;
|
||||
}
|
||||
|
||||
function getNodeTitle(nodeId, state = {}) {
|
||||
return getNodeById(nodeId, state)?.title || nodeId || '';
|
||||
}
|
||||
|
||||
function normalizeNodeStatus(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized || 'pending';
|
||||
}
|
||||
|
||||
function buildDefaultNodeStatuses(state = {}) {
|
||||
return Object.fromEntries(getNodeIdsForState(state).map((nodeId) => [nodeId, 'pending']));
|
||||
}
|
||||
|
||||
function normalizeNodeStatuses(statuses = {}, state = {}) {
|
||||
const defaults = buildDefaultNodeStatuses(state);
|
||||
const next = { ...defaults };
|
||||
if (!statuses || typeof statuses !== 'object' || Array.isArray(statuses)) {
|
||||
return next;
|
||||
}
|
||||
for (const [rawNodeId, rawStatus] of Object.entries(statuses)) {
|
||||
const nodeId = normalizeNodeId(rawNodeId);
|
||||
if (!nodeId || !Object.prototype.hasOwnProperty.call(defaults, nodeId)) {
|
||||
continue;
|
||||
}
|
||||
next[nodeId] = normalizeNodeStatus(rawStatus);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function isNodeDoneStatus(status = '') {
|
||||
return ['completed', 'manual_completed', 'skipped'].includes(normalizeNodeStatus(status));
|
||||
}
|
||||
|
||||
function isNodeTerminalStatus(status = '') {
|
||||
return ['completed', 'manual_completed', 'skipped', 'failed', 'stopped'].includes(normalizeNodeStatus(status));
|
||||
}
|
||||
|
||||
function getRunningNodeIds(statuses = {}, state = {}) {
|
||||
const normalizedStatuses = normalizeNodeStatuses(statuses, state);
|
||||
return getNodeIdsForState(state).filter((nodeId) => normalizedStatuses[nodeId] === 'running');
|
||||
}
|
||||
|
||||
function getFirstUnfinishedNodeId(statuses = {}, state = {}) {
|
||||
const normalizedStatuses = normalizeNodeStatuses(statuses, state);
|
||||
return getNodeIdsForState(state).find((nodeId) => !isNodeDoneStatus(normalizedStatuses[nodeId])) || '';
|
||||
}
|
||||
|
||||
function hasSavedProgress(statuses = {}, state = {}) {
|
||||
const normalizedStatuses = normalizeNodeStatuses(statuses, state);
|
||||
return getNodeIdsForState(state).some((nodeId) => normalizeNodeStatus(normalizedStatuses[nodeId]) !== 'pending');
|
||||
}
|
||||
|
||||
function getNextNodeIds(nodeId, state = {}) {
|
||||
const node = getNodeById(nodeId, state);
|
||||
if (!node) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(node.next) ? node.next.map(normalizeNodeId).filter(Boolean) : [];
|
||||
}
|
||||
|
||||
return {
|
||||
buildDefaultNodeStatuses,
|
||||
getDisplayOrderForNode,
|
||||
getFirstUnfinishedNodeId,
|
||||
getNextNodeIds,
|
||||
getNodeById,
|
||||
getNodeIdsForState,
|
||||
getNodesForState,
|
||||
getNodeTitle,
|
||||
getRunningNodeIds,
|
||||
getWorkflow,
|
||||
getWorkflowForState,
|
||||
hasSavedProgress,
|
||||
isNodeDoneStatus,
|
||||
isNodeTerminalStatus,
|
||||
normalizeFlowId,
|
||||
normalizeNodeId,
|
||||
normalizeNodeStatuses,
|
||||
normalizeNodeStatus,
|
||||
resolveStateFlowId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createWorkflowEngine,
|
||||
};
|
||||
});
|
||||
+34
-10
@@ -20,7 +20,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|
||||
// Listen for commands from Background
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (
|
||||
message.type === 'EXECUTE_STEP'
|
||||
message.type === 'EXECUTE_NODE'
|
||||
|| message.type === 'FILL_CODE'
|
||||
|| message.type === 'STEP8_FIND_AND_CLICK'
|
||||
|| message.type === 'STEP8_GET_STATE'
|
||||
@@ -46,6 +46,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|
||||
sendResponse({ ok: true, ...(result || {}) });
|
||||
}).catch(err => {
|
||||
const reportedStep = Number(message.payload?.visibleStep) || message.step;
|
||||
const reportedNodeId = resolveCommandNodeId(message);
|
||||
if (isStopError(err)) {
|
||||
if (reportedStep) {
|
||||
log(`步骤 ${reportedStep || 8}:已被用户停止。`, 'warn');
|
||||
@@ -61,7 +62,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|
||||
}
|
||||
|
||||
if (reportedStep) {
|
||||
reportError(reportedStep, err.message);
|
||||
reportError(reportedNodeId || reportedStep, err.message);
|
||||
}
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
@@ -72,16 +73,39 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|
||||
console.log('[MultiPage:signup-page] 消息监听已存在,跳过重复注册');
|
||||
}
|
||||
|
||||
const SIGNUP_PAGE_NODE_HANDLERS = Object.freeze({
|
||||
'submit-signup-email': (payload) => step2_clickRegister(payload),
|
||||
'fill-password': (payload) => step3_fillEmailPassword(payload),
|
||||
'fill-profile': (payload) => step5_fillNameBirthday(payload),
|
||||
'oauth-login': (payload) => step6_login(payload),
|
||||
'confirm-oauth': (_payload) => step8_findAndClick(),
|
||||
});
|
||||
|
||||
function resolveCommandNodeId(message = {}) {
|
||||
const directNodeId = String(message.nodeId || message.payload?.nodeId || '').trim();
|
||||
if (directNodeId) {
|
||||
return directNodeId;
|
||||
}
|
||||
const visibleStep = Number(message.payload?.visibleStep || message.step) || 0;
|
||||
if (visibleStep === 4) return 'fetch-signup-code';
|
||||
if (visibleStep === 8 || visibleStep === 11) return 'fetch-login-code';
|
||||
if (visibleStep === 9 || visibleStep === 12) return 'confirm-oauth';
|
||||
if (visibleStep === 7 || visibleStep === 10) return 'oauth-login';
|
||||
if (visibleStep === 5) return 'fill-profile';
|
||||
if (visibleStep === 3) return 'fill-password';
|
||||
if (visibleStep === 2) return 'submit-signup-email';
|
||||
return '';
|
||||
}
|
||||
|
||||
async function handleCommand(message) {
|
||||
switch (message.type) {
|
||||
case 'EXECUTE_STEP':
|
||||
switch (message.step) {
|
||||
case 2: return await step2_clickRegister(message.payload);
|
||||
case 3: return await step3_fillEmailPassword(message.payload);
|
||||
case 5: return await step5_fillNameBirthday(message.payload);
|
||||
case 7: return await step6_login(message.payload);
|
||||
case 9: return await step8_findAndClick();
|
||||
default: throw new Error(`signup-page.js 不处理步骤 ${message.step}`);
|
||||
case 'EXECUTE_NODE': {
|
||||
const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim();
|
||||
const handler = SIGNUP_PAGE_NODE_HANDLERS[nodeId];
|
||||
if (!handler) {
|
||||
throw new Error(`signup-page.js 不处理节点 ${nodeId}`);
|
||||
}
|
||||
return await handler(message.payload || {});
|
||||
}
|
||||
case 'FILL_CODE':
|
||||
// Step 4 = signup code, Step 7 = login code (same handler)
|
||||
|
||||
@@ -14,23 +14,23 @@ if (document.documentElement.getAttribute(SUB2API_PANEL_LISTENER_SENTINEL) !== '
|
||||
document.documentElement.setAttribute(SUB2API_PANEL_LISTENER_SENTINEL, '1');
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'EXECUTE_STEP' || message.type === 'REQUEST_OAUTH_URL') {
|
||||
if (message.type === 'EXECUTE_NODE' || message.type === 'REQUEST_OAUTH_URL') {
|
||||
resetStopState();
|
||||
const handler = message.type === 'REQUEST_OAUTH_URL'
|
||||
? requestOAuthUrl(message.payload)
|
||||
: handleStep(message.step, message.payload);
|
||||
: handleNode(message.nodeId || message.payload?.nodeId, message.payload);
|
||||
handler.then((result) => {
|
||||
sendResponse({ ok: true, ...(result || {}) });
|
||||
}).catch((err) => {
|
||||
if (isStopError(err)) {
|
||||
if (message.step) {
|
||||
log('已被用户停止。', 'warn', { step: message.step });
|
||||
if (message.payload?.visibleStep || message.step) {
|
||||
log('已被用户停止。', 'warn', { step: message.payload?.visibleStep || message.step });
|
||||
}
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
if (message.step) {
|
||||
reportError(message.step, err.message);
|
||||
if (message.nodeId || message.payload?.nodeId) {
|
||||
reportError(message.nodeId || message.payload?.nodeId, err.message);
|
||||
}
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
@@ -76,6 +76,16 @@ async function handleStep(step, payload = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNode(nodeId, payload = {}) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
switch (normalizedNodeId) {
|
||||
case 'platform-verify':
|
||||
return step9_submitOpenAiCallback(payload);
|
||||
default:
|
||||
throw new Error(`sub2api-panel.js 不处理节点 ${normalizedNodeId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestOAuthUrl(payload = {}) {
|
||||
return step1_generateOpenAiAuthUrl(payload, { report: false });
|
||||
}
|
||||
@@ -665,9 +675,10 @@ async function step9_submitOpenAiCallback(payload = {}) {
|
||||
|
||||
const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`;
|
||||
log(verifiedStatus, 'ok', { step: visibleStep, stepKey: 'platform-verify' });
|
||||
reportComplete(visibleStep, {
|
||||
reportComplete('platform-verify', {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
visibleStep,
|
||||
});
|
||||
openAccountsPageSoon(origin);
|
||||
}
|
||||
|
||||
+117
-20
@@ -274,6 +274,35 @@ function normalizeLogStep(value) {
|
||||
return step > 0 ? step : null;
|
||||
}
|
||||
|
||||
const DEFAULT_OPENAI_NODE_BY_STEP = Object.freeze({
|
||||
1: 'open-chatgpt',
|
||||
2: 'submit-signup-email',
|
||||
3: 'fill-password',
|
||||
4: 'fetch-signup-code',
|
||||
5: 'fill-profile',
|
||||
6: 'wait-registration-success',
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
9: 'confirm-oauth',
|
||||
10: 'platform-verify',
|
||||
11: 'fetch-login-code',
|
||||
12: 'confirm-oauth',
|
||||
13: 'platform-verify',
|
||||
});
|
||||
|
||||
function resolveReportNodeId(stepOrNodeId, data = {}) {
|
||||
const explicitNodeId = String(data?.nodeId || data?.nodeKey || '').trim();
|
||||
if (explicitNodeId) {
|
||||
return explicitNodeId;
|
||||
}
|
||||
const directNodeId = String(stepOrNodeId || '').trim();
|
||||
if (directNodeId && !/^\d+$/.test(directNodeId)) {
|
||||
return directNodeId;
|
||||
}
|
||||
const step = normalizeLogStep(stepOrNodeId || data?.step || data?.visibleStep);
|
||||
return step ? DEFAULT_OPENAI_NODE_BY_STEP[step] || '' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a log message to Side Panel via Background.
|
||||
* @param {string} message
|
||||
@@ -323,30 +352,65 @@ function reportReady() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Report step completion.
|
||||
* @param {number} step
|
||||
* @param {Object} data - Step output data
|
||||
* Report node completion.
|
||||
* @param {string|number} stepOrNodeId
|
||||
* @param {Object} data - Node output data
|
||||
*/
|
||||
function reportComplete(step, data = {}) {
|
||||
console.log(LOG_PREFIX, `步骤 ${step} 已完成`, data);
|
||||
log('已成功完成', 'ok', { step });
|
||||
function reportComplete(stepOrNodeId, data = {}) {
|
||||
const nodeId = resolveReportNodeId(stepOrNodeId, data);
|
||||
const step = normalizeLogStep(stepOrNodeId || data?.step || data?.visibleStep);
|
||||
console.log(LOG_PREFIX, `节点 ${nodeId || stepOrNodeId} 已完成`, data);
|
||||
log('已成功完成', 'ok', { step, stepKey: nodeId });
|
||||
const message = {
|
||||
type: 'STEP_COMPLETE',
|
||||
type: 'NODE_COMPLETE',
|
||||
source: getRuntimeScriptSource(),
|
||||
step,
|
||||
payload: data,
|
||||
nodeId,
|
||||
payload: {
|
||||
...(data || {}),
|
||||
...(nodeId ? { nodeId } : {}),
|
||||
...(step ? { step } : {}),
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
Promise.resolve(chrome.runtime.sendMessage(message))
|
||||
.then((response) => {
|
||||
console.log(LOG_PREFIX, `STEP_COMPLETE sent successfully for step ${step}`, {
|
||||
console.log(LOG_PREFIX, `NODE_COMPLETE sent successfully for node ${nodeId || stepOrNodeId}`, {
|
||||
response,
|
||||
url: location.href,
|
||||
payloadKeys: Object.keys(data || {}),
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(LOG_PREFIX, `STEP_COMPLETE send failed for step ${step}`, err?.message || err, {
|
||||
console.error(LOG_PREFIX, `NODE_COMPLETE send failed for node ${nodeId || stepOrNodeId}`, err?.message || err, {
|
||||
url: location.href,
|
||||
payloadKeys: Object.keys(data || {}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reportNodeComplete(nodeId, data = {}) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
console.log(LOG_PREFIX, `节点 ${normalizedNodeId} 已完成`, data);
|
||||
const message = {
|
||||
type: 'NODE_COMPLETE',
|
||||
source: getRuntimeScriptSource(),
|
||||
nodeId: normalizedNodeId,
|
||||
payload: {
|
||||
...(data || {}),
|
||||
nodeId: normalizedNodeId,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
Promise.resolve(chrome.runtime.sendMessage(message))
|
||||
.then((response) => {
|
||||
console.log(LOG_PREFIX, `NODE_COMPLETE sent successfully for node ${normalizedNodeId}`, {
|
||||
response,
|
||||
url: location.href,
|
||||
payloadKeys: Object.keys(data || {}),
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(LOG_PREFIX, `NODE_COMPLETE send failed for node ${normalizedNodeId}`, err?.message || err, {
|
||||
url: location.href,
|
||||
payloadKeys: Object.keys(data || {}),
|
||||
});
|
||||
@@ -354,29 +418,62 @@ function reportComplete(step, data = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Report step error.
|
||||
* @param {number} step
|
||||
* Report node error.
|
||||
* @param {string|number} stepOrNodeId
|
||||
* @param {string} errorMessage
|
||||
*/
|
||||
function reportError(step, errorMessage) {
|
||||
console.error(LOG_PREFIX, `步骤 ${step} 失败: ${errorMessage}`);
|
||||
function reportError(stepOrNodeId, errorMessage) {
|
||||
const nodeId = resolveReportNodeId(stepOrNodeId);
|
||||
const step = normalizeLogStep(stepOrNodeId);
|
||||
console.error(LOG_PREFIX, `节点 ${nodeId || stepOrNodeId} 失败: ${errorMessage}`);
|
||||
const message = {
|
||||
type: 'STEP_ERROR',
|
||||
type: 'NODE_ERROR',
|
||||
source: getRuntimeScriptSource(),
|
||||
step,
|
||||
payload: {},
|
||||
nodeId,
|
||||
payload: {
|
||||
...(nodeId ? { nodeId } : {}),
|
||||
...(step ? { step } : {}),
|
||||
},
|
||||
error: errorMessage,
|
||||
};
|
||||
Promise.resolve(chrome.runtime.sendMessage(message))
|
||||
.then((response) => {
|
||||
console.log(LOG_PREFIX, `STEP_ERROR sent successfully for step ${step}`, {
|
||||
console.log(LOG_PREFIX, `NODE_ERROR sent successfully for node ${nodeId || stepOrNodeId}`, {
|
||||
response,
|
||||
url: location.href,
|
||||
errorMessage,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(LOG_PREFIX, `STEP_ERROR send failed for step ${step}`, err?.message || err, {
|
||||
console.error(LOG_PREFIX, `NODE_ERROR send failed for node ${nodeId || stepOrNodeId}`, err?.message || err, {
|
||||
url: location.href,
|
||||
errorMessage,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reportNodeError(nodeId, errorMessage) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
console.error(LOG_PREFIX, `节点 ${normalizedNodeId} 失败: ${errorMessage}`);
|
||||
const message = {
|
||||
type: 'NODE_ERROR',
|
||||
source: getRuntimeScriptSource(),
|
||||
nodeId: normalizedNodeId,
|
||||
payload: {
|
||||
nodeId: normalizedNodeId,
|
||||
},
|
||||
error: errorMessage,
|
||||
};
|
||||
Promise.resolve(chrome.runtime.sendMessage(message))
|
||||
.then((response) => {
|
||||
console.log(LOG_PREFIX, `NODE_ERROR sent successfully for node ${normalizedNodeId}`, {
|
||||
response,
|
||||
url: location.href,
|
||||
errorMessage,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(LOG_PREFIX, `NODE_ERROR send failed for node ${normalizedNodeId}`, err?.message || err, {
|
||||
url: location.href,
|
||||
errorMessage,
|
||||
});
|
||||
|
||||
+19
-9
@@ -1,4 +1,4 @@
|
||||
// content/vps-panel.js — Content script for CPA panel (steps 7, 10 / OAuth URL request)
|
||||
// content/vps-panel.js — Content script for CPA panel (OAuth URL request / platform verification node)
|
||||
// Injected on: CPA panel (user-configured URL)
|
||||
//
|
||||
// Actual DOM structure (after login click):
|
||||
@@ -39,12 +39,12 @@ if (document.documentElement.getAttribute(VPS_PANEL_LISTENER_SENTINEL) !== '1')
|
||||
|
||||
// Listen for commands from Background
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'EXECUTE_STEP' || message.type === 'REQUEST_OAUTH_URL') {
|
||||
if (message.type === 'EXECUTE_NODE' || message.type === 'REQUEST_OAUTH_URL') {
|
||||
resetStopState();
|
||||
const startedAt = Date.now();
|
||||
const actionLabel = message.type === 'REQUEST_OAUTH_URL'
|
||||
? 'REQUEST_OAUTH_URL'
|
||||
: `EXECUTE_STEP received for step ${message.step}`;
|
||||
: `EXECUTE_NODE received for node ${message.nodeId || message.payload?.nodeId || ''}`;
|
||||
console.log(LOG_PREFIX, actionLabel, {
|
||||
url: location.href,
|
||||
payloadKeys: Object.keys(message.payload || {}),
|
||||
@@ -52,7 +52,7 @@ if (document.documentElement.getAttribute(VPS_PANEL_LISTENER_SENTINEL) !== '1')
|
||||
});
|
||||
const handler = message.type === 'REQUEST_OAUTH_URL'
|
||||
? requestOAuthUrl(message.payload)
|
||||
: handleStep(message.step, message.payload);
|
||||
: handleNode(message.nodeId || message.payload?.nodeId, message.payload);
|
||||
handler.then((result) => {
|
||||
console.log(LOG_PREFIX, `${actionLabel} resolved after ${Date.now() - startedAt}ms`, {
|
||||
url: location.href,
|
||||
@@ -65,14 +65,14 @@ if (document.documentElement.getAttribute(VPS_PANEL_LISTENER_SENTINEL) !== '1')
|
||||
snapshot: getVpsPanelSnapshot(),
|
||||
});
|
||||
if (isStopError(err)) {
|
||||
if (message.step) {
|
||||
log('已被用户停止。', 'warn', { step: message.step });
|
||||
if (message.payload?.visibleStep || message.step) {
|
||||
log('已被用户停止。', 'warn', { step: message.payload?.visibleStep || message.step });
|
||||
}
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
if (message.step) {
|
||||
reportError(message.step, err.message);
|
||||
if (message.nodeId || message.payload?.nodeId) {
|
||||
reportError(message.nodeId || message.payload?.nodeId, err.message);
|
||||
}
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
@@ -95,6 +95,16 @@ async function handleStep(step, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNode(nodeId, payload = {}) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
switch (normalizedNodeId) {
|
||||
case 'platform-verify':
|
||||
return await step9_vpsVerify(payload);
|
||||
default:
|
||||
throw new Error(`vps-panel.js 不处理节点 ${normalizedNodeId}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isVisibleElement(el) {
|
||||
if (!el) return false;
|
||||
const style = window.getComputedStyle(el);
|
||||
@@ -1078,5 +1088,5 @@ async function step9_vpsVerify(payload) {
|
||||
|
||||
const verifiedStatus = await waitForExactSuccessBadge(STEP9_SUCCESS_BADGE_TIMEOUT_MS, visibleStep);
|
||||
log(verifiedStatus, 'ok', { step: visibleStep, stepKey: 'platform-verify' });
|
||||
reportComplete(visibleStep, { localhostUrl, verifiedStatus });
|
||||
reportComplete('platform-verify', { localhostUrl, verifiedStatus, visibleStep });
|
||||
}
|
||||
|
||||
+132
-45
@@ -10,60 +10,60 @@
|
||||
const SIGNUP_METHOD_PHONE = 'phone';
|
||||
|
||||
const NORMAL_STEP_DEFINITIONS = [
|
||||
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' },
|
||||
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' },
|
||||
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' },
|
||||
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' },
|
||||
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' },
|
||||
{ id: 6, order: 60, key: 'wait-registration-success', title: '等待注册成功' },
|
||||
{ id: 7, order: 70, key: 'oauth-login', title: '刷新 OAuth 并登录' },
|
||||
{ id: 8, order: 80, key: 'fetch-login-code', title: '获取登录验证码' },
|
||||
{ id: 9, order: 90, key: 'confirm-oauth', title: '自动确认 OAuth' },
|
||||
{ id: 10, order: 100, key: 'platform-verify', title: '平台回调验证' },
|
||||
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' },
|
||||
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-signup-email' },
|
||||
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-password' },
|
||||
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-signup-code' },
|
||||
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-profile' },
|
||||
{ id: 6, order: 60, key: 'wait-registration-success', title: '等待注册成功', sourceId: 'chatgpt', driverId: null, command: 'wait-registration-success' },
|
||||
{ id: 7, order: 70, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' },
|
||||
{ id: 8, order: 80, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' },
|
||||
{ id: 9, order: 90, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' },
|
||||
{ id: 10, order: 100, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' },
|
||||
];
|
||||
|
||||
const PLUS_PAYPAL_STEP_DEFINITIONS = [
|
||||
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' },
|
||||
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' },
|
||||
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' },
|
||||
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' },
|
||||
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' },
|
||||
{ id: 6, order: 60, key: 'plus-checkout-create', title: '创建 Plus Checkout' },
|
||||
{ id: 7, order: 70, key: 'plus-checkout-billing', title: '填写账单并提交订单' },
|
||||
{ id: 8, order: 80, key: 'paypal-approve', title: 'PayPal 登录与授权' },
|
||||
{ id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认' },
|
||||
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' },
|
||||
{ id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码' },
|
||||
{ id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth' },
|
||||
{ id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' },
|
||||
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' },
|
||||
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-signup-email' },
|
||||
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-password' },
|
||||
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-signup-code' },
|
||||
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-profile' },
|
||||
{ id: 6, order: 60, key: 'plus-checkout-create', title: '创建 Plus Checkout', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-create' },
|
||||
{ id: 7, order: 70, key: 'plus-checkout-billing', title: '填写账单并提交订单', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-billing' },
|
||||
{ id: 8, order: 80, key: 'paypal-approve', title: 'PayPal 登录与授权', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-approve' },
|
||||
{ id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-return' },
|
||||
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' },
|
||||
{ id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' },
|
||||
{ id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' },
|
||||
{ id: 13, order: 130, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' },
|
||||
];
|
||||
|
||||
const PLUS_GOPAY_STEP_DEFINITIONS = [
|
||||
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' },
|
||||
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' },
|
||||
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' },
|
||||
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' },
|
||||
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' },
|
||||
{ id: 6, order: 60, key: 'plus-checkout-create', title: '打开 GoPay 订阅页' },
|
||||
{ id: 7, order: 70, key: 'gopay-subscription-confirm', title: '等待 GoPay 订阅确认' },
|
||||
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' },
|
||||
{ id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码' },
|
||||
{ id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth' },
|
||||
{ id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' },
|
||||
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' },
|
||||
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-signup-email' },
|
||||
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-password' },
|
||||
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-signup-code' },
|
||||
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-profile' },
|
||||
{ id: 6, order: 60, key: 'plus-checkout-create', title: '打开 GoPay 订阅页', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-create' },
|
||||
{ id: 7, order: 70, key: 'gopay-subscription-confirm', title: '等待 GoPay 订阅确认', sourceId: 'gopay-flow', driverId: 'content/gopay-flow', command: 'gopay-subscription-confirm' },
|
||||
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' },
|
||||
{ id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' },
|
||||
{ id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' },
|
||||
{ id: 13, order: 130, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' },
|
||||
];
|
||||
|
||||
const PLUS_GPC_STEP_DEFINITIONS = [
|
||||
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' },
|
||||
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' },
|
||||
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' },
|
||||
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' },
|
||||
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' },
|
||||
{ id: 6, order: 60, key: 'plus-checkout-create', title: '创建 GPC 订单' },
|
||||
{ id: 7, order: 70, key: 'plus-checkout-billing', title: '等待 GPC 任务完成' },
|
||||
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' },
|
||||
{ id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码' },
|
||||
{ id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth' },
|
||||
{ id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' },
|
||||
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' },
|
||||
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-signup-email' },
|
||||
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-password' },
|
||||
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-signup-code' },
|
||||
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fill-profile' },
|
||||
{ id: 6, order: 60, key: 'plus-checkout-create', title: '创建 GPC 订单', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-create' },
|
||||
{ id: 7, order: 70, key: 'plus-checkout-billing', title: '等待 GPC 任务完成', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-billing' },
|
||||
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' },
|
||||
{ id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' },
|
||||
{ id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' },
|
||||
{ id: 13, order: 130, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' },
|
||||
];
|
||||
|
||||
const PHONE_SIGNUP_TITLE_OVERRIDES = Object.freeze({
|
||||
@@ -186,6 +186,27 @@
|
||||
}));
|
||||
}
|
||||
|
||||
function cloneNodes(steps = [], options = {}, flowId = DEFAULT_ACTIVE_FLOW_ID) {
|
||||
const { builder } = getFlowDefinitionBuilder({ activeFlowId: flowId });
|
||||
return steps.map((step) => ({
|
||||
legacyStepId: Number(step.id),
|
||||
nodeId: String(step.key || '').trim(),
|
||||
flowId,
|
||||
title: builder?.resolveStepTitle ? builder.resolveStepTitle(step, options) : step.title,
|
||||
displayOrder: Number.isFinite(Number(step.order)) ? Number(step.order) : Number(step.id),
|
||||
nodeType: 'task',
|
||||
sourceId: step.sourceId || '',
|
||||
driverId: step.driverId || '',
|
||||
executeKey: String(step.key || '').trim(),
|
||||
command: String(step.command || step.key || '').trim(),
|
||||
mailRuleId: String(step.mailRuleId || '').trim(),
|
||||
next: Array.isArray(step.next) ? [...step.next] : [],
|
||||
retryPolicy: step.retryPolicy && typeof step.retryPolicy === 'object' ? { ...step.retryPolicy } : {},
|
||||
recoveryPolicy: step.recoveryPolicy && typeof step.recoveryPolicy === 'object' ? { ...step.recoveryPolicy } : {},
|
||||
ui: step.ui && typeof step.ui === 'object' ? { ...step.ui } : {},
|
||||
})).filter((node) => Boolean(node.nodeId));
|
||||
}
|
||||
|
||||
function getSteps(options = {}) {
|
||||
const { flowId, builder } = getFlowDefinitionBuilder(options);
|
||||
if (!builder?.getModeStepDefinitions) {
|
||||
@@ -194,6 +215,23 @@
|
||||
return cloneSteps(builder.getModeStepDefinitions(options), options, flowId);
|
||||
}
|
||||
|
||||
function linkLinearNodes(nodes = []) {
|
||||
return nodes.map((node, index) => ({
|
||||
...node,
|
||||
next: Array.isArray(node.next) && node.next.length
|
||||
? [...node.next]
|
||||
: (nodes[index + 1]?.nodeId ? [nodes[index + 1].nodeId] : []),
|
||||
}));
|
||||
}
|
||||
|
||||
function getNodes(options = {}) {
|
||||
const { flowId, builder } = getFlowDefinitionBuilder(options);
|
||||
if (!builder?.getModeStepDefinitions) {
|
||||
return [];
|
||||
}
|
||||
return linkLinearNodes(cloneNodes(builder.getModeStepDefinitions(options), options, flowId));
|
||||
}
|
||||
|
||||
function getAllSteps(options = {}) {
|
||||
const { flowId, builder } = getFlowDefinitionBuilder(options);
|
||||
if (!builder?.getAllSteps) {
|
||||
@@ -202,6 +240,18 @@
|
||||
return cloneSteps(builder.getAllSteps(options), options, flowId);
|
||||
}
|
||||
|
||||
function getAllNodes(options = {}) {
|
||||
const { flowId, builder } = getFlowDefinitionBuilder(options);
|
||||
if (!builder?.getAllSteps) {
|
||||
return [];
|
||||
}
|
||||
return cloneNodes(builder.getAllSteps(options), options, flowId)
|
||||
.sort((left, right) => {
|
||||
if (left.displayOrder !== right.displayOrder) return left.displayOrder - right.displayOrder;
|
||||
return left.nodeId.localeCompare(right.nodeId);
|
||||
});
|
||||
}
|
||||
|
||||
function getPlusPaymentStepTitle(options = {}) {
|
||||
const { builder } = getFlowDefinitionBuilder(options);
|
||||
if (!builder?.getPlusPaymentStepTitle) {
|
||||
@@ -217,6 +267,10 @@
|
||||
.sort((left, right) => left - right);
|
||||
}
|
||||
|
||||
function getNodeIds(options = {}) {
|
||||
return getNodes(options).map((node) => node.nodeId);
|
||||
}
|
||||
|
||||
function getLastStepId(options = {}) {
|
||||
const ids = getStepIds(options);
|
||||
return ids[ids.length - 1] || 0;
|
||||
@@ -232,6 +286,33 @@
|
||||
return match ? cloneSteps([match], options, flowId)[0] : null;
|
||||
}
|
||||
|
||||
function getNodeById(nodeId, options = {}) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
if (!normalizedNodeId) {
|
||||
return null;
|
||||
}
|
||||
return getNodes(options).find((node) => node.nodeId === normalizedNodeId) || null;
|
||||
}
|
||||
|
||||
function getNodeByDisplayOrder(displayOrder, options = {}) {
|
||||
const normalizedOrder = Number(displayOrder);
|
||||
if (!Number.isFinite(normalizedOrder)) {
|
||||
return null;
|
||||
}
|
||||
return getNodes(options).find((node) => node.displayOrder === normalizedOrder) || null;
|
||||
}
|
||||
|
||||
function getWorkflow(options = {}) {
|
||||
const flowId = normalizeActiveFlowId(options?.activeFlowId, DEFAULT_ACTIVE_FLOW_ID);
|
||||
const nodes = getNodes(options);
|
||||
return {
|
||||
flowId,
|
||||
workflowVersion: 1,
|
||||
nodes,
|
||||
nodeIds: nodes.map((node) => node.nodeId),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
DEFAULT_ACTIVE_FLOW_ID,
|
||||
STEP_DEFINITIONS: NORMAL_STEP_DEFINITIONS,
|
||||
@@ -243,12 +324,18 @@
|
||||
SIGNUP_METHOD_EMAIL,
|
||||
SIGNUP_METHOD_PHONE,
|
||||
getAllSteps,
|
||||
getAllNodes,
|
||||
getLastStepId,
|
||||
getNodeByDisplayOrder,
|
||||
getNodeById,
|
||||
getNodeIds,
|
||||
getNodes,
|
||||
getPlusPaymentStepTitle,
|
||||
getRegisteredFlowIds,
|
||||
getStepById,
|
||||
getStepIds,
|
||||
getSteps,
|
||||
getWorkflow,
|
||||
hasFlow,
|
||||
isPlusModeEnabled,
|
||||
normalizeActiveFlowId,
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
# 步骤与Flow节点化重构开发方案
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文不是给现有 `step` 体系再补一层适配,而是把整个执行系统升级成 **flow + node** 体系。目标是从根上解决“数字步骤硬编码”和“key 绑定混用”这两套逻辑并存的问题。
|
||||
|
||||
最终设计必须满足:
|
||||
|
||||
1. 新增步骤时,只改当前 flow 的定义和节点执行器,不再牵动全局 switch。
|
||||
2. 新增完全不同的 flow 时,可以拥有完全不同的节点图、页面源、邮件规则、恢复策略和 UI 表达。
|
||||
3. 最终版本不保留 `currentStep` / `stepStatuses` 作为核心状态,不保留 `STEP_*` 作为核心协议,不再让数字步骤充当身份、顺序、状态、历史和消息路由的共同主键。
|
||||
4. 方案必须同时覆盖 background、content、sidepanel、日志、历史、自动运行、来源注册、邮件规则、手机号接码、Plus 支付这些强关联模块。
|
||||
|
||||
## 2. 重构前实现的真实形态
|
||||
|
||||
当前项目不是纯硬编码,而是“**定义层部分动态化,控制层仍然硬编码**”。
|
||||
|
||||
| 层 | 现状 | 主要问题 |
|
||||
| --- | --- | --- |
|
||||
| 状态 | `background.js` 里仍有 `currentStep`、`stepStatuses` | 数字步骤同时承担状态、顺序和恢复锚点 |
|
||||
| 协议 | `EXECUTE_STEP`、`STEP_COMPLETE`、`STEP_ERROR`、`SKIP_STEP` | 消息协议仍以数字 step 为核心 |
|
||||
| 定义 | `data/step-definitions.js`、`background/steps/registry.js` | 只有 OpenAI 真正注册,且执行仍按数字 id |
|
||||
| UI | `sidepanel/sidepanel.js` 按 step 渲染、发送、跳过 | 侧边栏本质上还是数字步骤面板 |
|
||||
| 内容脚本 | `content/signup-page.js` 按 `message.step` 分支 | 页面驱动逻辑被 step number 绑死 |
|
||||
| 运行态 | `background/runtime-state.js` 已有 `currentNodeId`、`nodeStatuses` | 但仍通过 legacy step 视图回写,双模型并存 |
|
||||
| 外围能力 | `shared/source-registry.js`、`shared/flow-capabilities.js`、`background/mail-rule-registry.js` | 已有抽象,但都还没成为唯一事实来源 |
|
||||
|
||||
重点文件里最能说明问题的几个点:
|
||||
|
||||
- `background.js`:`DEFAULT_STATE`、`setStepStatus()`、`skipStep()`、`runAutoSequenceFromStep()`、`getStepRegistryForState()` 仍把数字 step 当主流程。
|
||||
- `background/message-router.js`:`STEP_COMPLETE` / `STEP_ERROR` / `EXECUTE_STEP` / `SKIP_STEP` 还在消息层面锁死 step。
|
||||
- `background/runtime-state.js`:已经有 node 相关字段,但还在做 step 兼容派生。
|
||||
- `sidepanel/sidepanel.js`:步骤列表、状态展示、手动执行、跳过、自动运行都还围着 step。
|
||||
- `flows/openai/mail-rules.js`、`background/mail-rule-registry.js`:已经说明“规则可以 flow 化”,但目前只有 OpenAI 在用。
|
||||
|
||||
## 3. 为什么现在的设计有问题
|
||||
|
||||
### 3.1 混用了三种身份
|
||||
|
||||
现在一个“步骤”同时在扮演:
|
||||
|
||||
- 执行身份
|
||||
- 顺序编号
|
||||
- 状态键
|
||||
- 日志标签
|
||||
- 历史记录字段
|
||||
- 自动运行恢复锚点
|
||||
|
||||
这就是后面维护越来越难的根因。只要新增一个节点,往往要同时改定义、状态、消息、UI、日志、历史、恢复逻辑,甚至测试。
|
||||
|
||||
### 3.2 只改 key 不够
|
||||
|
||||
项目里已经有 `key`,但它并没有真正替代数字 step。
|
||||
|
||||
当前状态是:
|
||||
|
||||
- `id` 负责顺序和执行
|
||||
- `key` 负责局部标识
|
||||
- `currentStep` 负责运行态
|
||||
- `stepStatuses` 负责状态
|
||||
|
||||
这不是“灵活”,这是“双主键混用”。如果只把 `step` 改名成 `node`,不重做控制面,问题不会消失。
|
||||
|
||||
### 3.3 不能表达完全不同的 flow
|
||||
|
||||
现在 `getStepRegistryForState()` 直接在 `activeFlowId !== openai` 时拒绝执行,这意味着:
|
||||
|
||||
- 新 flow 不是“可扩展”,而是“被排除”
|
||||
- 每个 flow 都只能往 OpenAI 这套结构里塞
|
||||
- 只要一个 flow 的步骤顺序、页面、邮件、恢复策略不同,就会触发大量全局改动
|
||||
|
||||
### 3.4 线性 step 模型太弱
|
||||
|
||||
现有逻辑默认所有流程都是线性的 1, 2, 3, 4...。
|
||||
|
||||
但真实场景里,新的 flow 很可能是:
|
||||
|
||||
- 有分支
|
||||
- 有可选节点
|
||||
- 有重试回路
|
||||
- 有条件跳转
|
||||
- 有完全不同的外部页面驱动
|
||||
|
||||
这种场景下,数字 step 不是天然模型,只是临时排布方式。
|
||||
|
||||
### 3.5 相关模块已经开始“各自发明一套”
|
||||
|
||||
`runtime-state`、`source-registry`、`flow-capabilities`、`mail-rule-registry`、`navigation-utils` 都已经在做分层,但它们没有统一到一个真正的 workflow 主模型里。
|
||||
|
||||
结果就是:局部已经像架构,整体仍像拼接。
|
||||
|
||||
## 4. 目标架构
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
FR["flowRegistry"]
|
||||
WF["workflowDefinition"]
|
||||
WE["workflowEngine"]
|
||||
RS["runtimeState"]
|
||||
SR["sourceRegistry"]
|
||||
DR["driverRegistry"]
|
||||
MR["mailRuleRegistry"]
|
||||
CR["capabilityRegistry"]
|
||||
SP["sidepanel"]
|
||||
CS["content drivers"]
|
||||
LOG["logs / history"]
|
||||
|
||||
FR --> WF
|
||||
WF --> WE
|
||||
WE <--> RS
|
||||
WF --> SR
|
||||
WF --> DR
|
||||
WF --> MR
|
||||
WF --> CR
|
||||
WE --> SP
|
||||
WE --> CS
|
||||
WE --> LOG
|
||||
```
|
||||
|
||||
### 4.1 核心标识
|
||||
|
||||
| 标识 | 含义 | 规则 |
|
||||
| --- | --- | --- |
|
||||
| `flowId` | 业务 flow 身份 | 例如 `openai`、`site-a` |
|
||||
| `runId` | 一次执行实例 | 同一轮运行内保持不变 |
|
||||
| `nodeId` | 节点身份 | 唯一、稳定、可读 |
|
||||
| `sourceId` | 页面来源身份 | 负责标签页、页面家族和注入目标 |
|
||||
| `driverId` | 内容驱动身份 | 负责某个 source 的命令集 |
|
||||
|
||||
### 4.2 目标状态模型
|
||||
|
||||
```js
|
||||
runtimeState = {
|
||||
flowId: 'openai',
|
||||
runId: 'run_20260515_xxx',
|
||||
workflowVersion: 1,
|
||||
currentNodeId: 'submit-signup-email',
|
||||
nodeStatuses: {
|
||||
'open-chatgpt': 'completed',
|
||||
'submit-signup-email': 'running',
|
||||
},
|
||||
nodeResults: {
|
||||
'open-chatgpt': { completedAt: 1710000000000 },
|
||||
},
|
||||
shared: {},
|
||||
services: {},
|
||||
flows: {
|
||||
openai: {},
|
||||
},
|
||||
runSummary: {
|
||||
finalStatus: 'running',
|
||||
failedNodeId: '',
|
||||
failureReasonCode: '',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 节点模型
|
||||
|
||||
```js
|
||||
node = {
|
||||
id: 'submit-signup-email',
|
||||
title: '注册并输入邮箱',
|
||||
type: 'task',
|
||||
order: 20, // 仅用于展示,不参与身份
|
||||
sourceId: 'openai-auth',
|
||||
driverId: 'content/signup-page',
|
||||
next: ['fill-password'],
|
||||
retryPolicy: { maxAttempts: 3 },
|
||||
recoveryPolicy: { onFailure: 'restart-node' },
|
||||
ui: { section: 'registration' },
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- `id` 是唯一主键。
|
||||
- `order` 只是展示顺序,不再决定执行逻辑。
|
||||
- `next` / `recoveryPolicy` 决定流转,不靠全局 step 数组推断。
|
||||
- 节点结果、失败原因、重试记录要单独存,不要塞进状态字符串后缀里。
|
||||
|
||||
### 4.4 Flow 定义必须是一手真相
|
||||
|
||||
`flowDefinition` 应该统一拥有:
|
||||
|
||||
- 节点图
|
||||
- 节点执行器引用
|
||||
- source 绑定
|
||||
- driver 绑定
|
||||
- 邮件规则引用
|
||||
- capability 定义
|
||||
- recovery policy
|
||||
- settings schema
|
||||
|
||||
其他 registry 如果存在,只能是这个定义的运行索引或编译产物,不能自己再维护一份同义信息。
|
||||
|
||||
## 5. 新增步骤 / 新增 flow 的接入规则
|
||||
|
||||
### 5.1 如果是在同一个 flow 里新增步骤
|
||||
|
||||
应该只做这些事:
|
||||
|
||||
1. 在该 flow 的 `workflowDefinition` 里新增 node。
|
||||
2. 给这个 node 配置 `next`、`retryPolicy`、`recoveryPolicy`、`sourceId`、`driverId`。
|
||||
3. 如果它访问了新页面、新邮件规则、新服务,再补对应的 source / driver / mail rule。
|
||||
4. UI 从 flow 定义里自动拿到节点标题、顺序和状态展示。
|
||||
5. 增加对应测试,不再改全局 step switch。
|
||||
|
||||
### 5.2 如果是新加一个完全不同的 flow
|
||||
|
||||
应该直接新增一个独立 flow 目录和定义,不去改 OpenAI 的步骤树:
|
||||
|
||||
```txt
|
||||
flows/<flowId>/
|
||||
workflow.js
|
||||
sources.js
|
||||
mail-rules.js
|
||||
capabilities.js
|
||||
recovery.js
|
||||
```
|
||||
|
||||
新 flow 的要求是:
|
||||
|
||||
- 可以没有手机号接码
|
||||
- 可以没有 Plus
|
||||
- 可以没有邮件验证码
|
||||
- 可以有完全不同的节点顺序和分支
|
||||
- 可以有自己的页面源和 driver
|
||||
|
||||
核心 engine 不改,改的是 flow 自己。
|
||||
|
||||
## 6. 相关模块边界
|
||||
|
||||
### 6.1 background
|
||||
|
||||
`background.js` 和 `background/message-router.js` 的职责应退回成“调度和校验”,不再自己写业务步骤树。
|
||||
|
||||
### 6.2 sidepanel
|
||||
|
||||
`sidepanel/sidepanel.js` 只能根据 flow definition 和 capability 结果渲染,不应该再手写 step 编号和 step 顺序判断。
|
||||
|
||||
### 6.3 content scripts
|
||||
|
||||
`content/signup-page.js` 这类脚本要从“按 step 分支”改成“按 node action / command 分支”。
|
||||
|
||||
### 6.4 source / driver
|
||||
|
||||
`shared/source-registry.js` 只负责页面来源、标签页生命周期和注入范围。
|
||||
|
||||
`driverRegistry` 只负责“这个 source 能接什么命令”。
|
||||
|
||||
### 6.5 mail rules
|
||||
|
||||
邮件过滤和验证码提取必须是 flow-local 的。`flows/openai/mail-rules.js` 说明这件事已经存在,只是还没推广成整体原则。
|
||||
|
||||
### 6.6 auto-run / history / logs
|
||||
|
||||
自动运行、日志和历史必须记录 `flowId`、`runId`、`nodeId`,而不是继续写 `step7_failed` 这种混合字符串。
|
||||
|
||||
### 6.7 手机接码、Plus、OAuth
|
||||
|
||||
这些都不是“通用步骤”,它们是 OpenAI flow 的私有能力。没有第二个 flow 的真实需求时,不要把它们硬抽成全局共享步骤。
|
||||
|
||||
## 7. 设计符合性检查
|
||||
|
||||
| 检查项 | 是否满足 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 不做旧兼容 | 是 | 最终状态不保留 step 作为核心模型 |
|
||||
| 新增步骤可维护 | 是 | 只改当前 flow 的 node 和相关依赖 |
|
||||
| 新增不同 flow 可维护 | 是 | 独立 flow 定义,不污染 OpenAI |
|
||||
| 规范一致 | 是 | 统一使用 `flowId` / `runId` / `nodeId` / `sourceId` |
|
||||
| 完整性 | 是 | 覆盖状态、协议、UI、日志、历史、自动运行、来源、邮件 |
|
||||
| 正确性 | 基本满足 | 仍需靠分阶段测试验证边界和恢复路径 |
|
||||
|
||||
## 8. 方案自身的缺陷与控制点
|
||||
|
||||
这份方案本身也有潜在问题,必须提前说明:
|
||||
|
||||
1. **抽象过头风险**
|
||||
如果 flow / node / source / driver / mail rule 各自独立维护同一份信息,会重新制造同步成本。控制方式是:`flowDefinition` 必须是一手真相,其它 registry 只能是派生索引。
|
||||
|
||||
2. **DSL 复杂度风险**
|
||||
如果节点模型一开始就塞太多字段,后续会变成另一种难维护的配置语言。控制方式是:先保留 `id`、`title`、`type`、`order`、`sourceId`、`driverId`、`next`、`retryPolicy`、`recoveryPolicy`,其余按真实需求再加。
|
||||
|
||||
3. **日志和历史膨胀风险**
|
||||
如果把每个节点的所有中间态都原样落盘,历史会很重。控制方式是:日志保留可读摘要,历史保留最终结果 + 关键节点轨迹 + 失败原因码。
|
||||
|
||||
4. **UI 复杂度风险**
|
||||
如果 UI 同时展示 flow、node、source、driver、恢复策略,侧边栏会太吵。控制方式是:UI 只展示用户关心的流转和状态,详细调试信息留给开发记录。
|
||||
|
||||
5. **边界冲突风险**
|
||||
现在已经存在 `sourceRegistry`、`flowCapabilities`、`mailRuleRegistry`、`runtimeState` 等分层。如果不统一为一套 flow contract,就会继续出现“看起来都对,合起来互相打架”的问题。
|
||||
|
||||
## 9. 开发清单
|
||||
|
||||
| 阶段 | 开发目标 | 阶段自检 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 定义最终 schema:`flowId`、`runId`、`nodeId`、`sourceId`、`driverId`、`workflowVersion` | 检查是否还在把 `currentStep` / `stepStatuses` 当核心概念使用 |
|
||||
| 2 | 建立 flow definition 和 workflow engine | 检查一个 flow 是否能只靠自己的定义被加载,不改全局 switch |
|
||||
| 3 | 替换消息协议 | 检查是否全面切换到 `EXECUTE_NODE` / `NODE_COMPLETE` / `NODE_ERROR` / `SKIP_NODE` 这类命名 |
|
||||
| 4 | 改写 runtime / auto-run / recovery | 检查恢复、重试、跳过、暂停是否都基于 `nodeId` 和 `runId` |
|
||||
| 5 | 重写 sidepanel | 检查 UI 是否完全从 flow definition 取数据,不再硬编码 step 顺序 |
|
||||
| 6 | 迁移 content drivers、邮件规则、来源注册 | 检查新 flow 是否可以拥有完全不同的页面源和邮件规则,而不碰 OpenAI 核心流程 |
|
||||
| 7 | 重写日志、历史、账号记录 | 检查是否只记录 `flowId` / `runId` / `nodeId`,不再产出 `stepX_failed` 这类混合状态 |
|
||||
| 8 | 删除旧 step 路径和残留逻辑 | 检查仓库中核心路径是否还残留 `EXECUTE_STEP`、`STEP_COMPLETE`、`STEP_ERROR`、`currentStep`、`stepStatuses` |
|
||||
|
||||
### 每阶段统一自检
|
||||
|
||||
每做完一个阶段,都要执行下面的自检:
|
||||
|
||||
1. grep 核心代码,确认没有新的 step 硬编码回流。
|
||||
2. 用至少一个 flow 做完整跑通验证。
|
||||
3. 用一个“结构完全不同”的 flow 做边界验证。
|
||||
4. 检查消息、日志、历史、UI、文档是否一致。
|
||||
5. 打开 Markdown 预览,确认中文标题、表格和代码块都没有乱码。
|
||||
|
||||
## 10. 最终验收标准
|
||||
|
||||
这次重构只有在满足下面条件时才算完成:
|
||||
|
||||
- 核心状态不再依赖数字 step。
|
||||
- 新步骤只改本 flow,不改全局控制面。
|
||||
- 新 flow 可以和 OpenAI 完全不同。
|
||||
- background、sidepanel、content、auto-run、logs、history、mail rules、source registry 都在同一套 flow/node 规则下运行。
|
||||
- 文档、代码、测试三者描述一致,没有“代码一套、文档一套、UI 一套”的分裂。
|
||||
- Markdown 和中文显示无乱码。
|
||||
|
||||
结论很直接:这块不能继续修补式演进,必须用 node-based workflow engine 一次性替换掉 step-number 作为核心模型的设计。
|
||||
|
||||
## 11. 本次落地后的实现约束
|
||||
|
||||
本次重构后,核心运行路径已经按下面规则收敛:
|
||||
|
||||
1. background、content、sidepanel 的消息协议只使用 `EXECUTE_NODE`、`NODE_COMPLETE`、`NODE_ERROR`、`SKIP_NODE`。
|
||||
2. 核心运行状态只使用 `flowId`、`runId`、`currentNodeId`、`nodeStatuses`。
|
||||
3. 账号运行历史的新记录使用 `flowId`、`runId`、`failedNodeId`,失败/停止状态使用 `node:<nodeId>:failed`、`node:<nodeId>:stopped`。
|
||||
4. sidepanel 渲染仍可以展示“第几项”的用户文案,但状态合并、按钮执行、跳过、恢复都以 `nodeId` 为主键。
|
||||
5. 邮件规则、source registry、driver command 已跟 node 对齐;验证码节点通过 `mailRuleId` 绑定,而不是通过固定步骤号绑定。
|
||||
6. 自动运行主循环使用 `runAutoSequenceFromNodeGraph(startNodeId)` 按当前 workflow 的 node 列表推进,不再通过数字序号、`step++` 或 `runAutoSequenceFromNodeOrder` 驱动。
|
||||
7. 自动运行恢复、idle 重开、Plus/GPC/GoPay checkout 重建都以目标 `nodeId` 和实际前置节点为锚点;遇到稀疏节点图时不会再依赖不存在的虚拟数字步骤。
|
||||
|
||||
阶段 8 自检命令要求核心生产路径不得再命中旧协议和旧状态字段:
|
||||
|
||||
```powershell
|
||||
rg -n "EXECUTE_STEP|STEP_COMPLETE|STEP_ERROR|SKIP_STEP|STEP_STATUS_CHANGED" background.js background content sidepanel shared flows data
|
||||
rg -n "currentStep|stepStatuses|setStepStatus|runAutoSequenceFromStep" background.js background sidepanel content shared flows data
|
||||
rg -n "step\d+_failed|step\d+_stopped|step2_stopped|step7_stopped|step8_failed|step9_failed|step10_failed" background.js background
|
||||
rg -n "runAutoSequenceFromNodeOrder|currentStartStep|startNodeOrder|while \(step <=|step \+= 1" background.js background
|
||||
```
|
||||
|
||||
## 12. 后续新增节点的标准入口
|
||||
|
||||
同一 flow 新增节点时,按这个顺序开发:
|
||||
|
||||
1. 在 `data/step-definitions.js` 的当前 flow 定义里新增 node,并补齐 `nodeId`、`title`、`sourceId`、`driverId`、`command`、`mailRuleId`、`next`。
|
||||
2. 在 `background/steps/` 增加或复用节点执行器,并在 registry 构建处按 `executeKey` 注册。
|
||||
3. 如果节点需要页面执行,给对应 content driver 增加 `EXECUTE_NODE` command handler。
|
||||
4. 如果节点需要邮件验证码,在 `flows/<flowId>/mail-rules.js` 增加 flow-local rule,并通过 `mailRuleId` 绑定。
|
||||
5. 增加 node-first 测试,断言消息、状态、历史、UI 都只使用 `nodeId`。
|
||||
|
||||
## 13. 后续新增完全不同 flow 的标准入口
|
||||
|
||||
新增完全不同 flow 时,不要改 OpenAI 的节点树来“塞进去”,而是新增 flow-local 定义:
|
||||
|
||||
```txt
|
||||
flows/<flowId>/
|
||||
workflow.js
|
||||
sources.js
|
||||
mail-rules.js
|
||||
capabilities.js
|
||||
recovery.js
|
||||
```
|
||||
|
||||
落地前必须自检:
|
||||
|
||||
- 新 flow 是否可以不包含 OpenAI 私有节点,例如 Plus、手机号接码、OAuth。
|
||||
- `sourceId` / `driverId` 是否只服务当前 flow,不污染全局。
|
||||
- 邮件规则是否只从当前 flow definition 派生。
|
||||
- sidepanel 是否能仅通过 workflow nodes 渲染,不新增全局步骤 switch。
|
||||
- 自动运行、历史和日志是否都能用 `flowId/runId/nodeId` 定位。
|
||||
@@ -3,6 +3,8 @@
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createOpenAiMailRulesModule() {
|
||||
const SIGNUP_CODE_RULE_ID = 'openai-signup-code';
|
||||
const LOGIN_CODE_RULE_ID = 'openai-login-code';
|
||||
const SIGNUP_CODE_NODE_ID = 'fetch-signup-code';
|
||||
const LOGIN_CODE_NODE_ID = 'fetch-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})',
|
||||
@@ -56,10 +58,27 @@
|
||||
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive';
|
||||
}
|
||||
|
||||
function getRuleDefinition(step, state = {}) {
|
||||
const normalizedStep = Number(step) === 4 ? 4 : 8;
|
||||
function resolveVerificationNodeId(input) {
|
||||
const directNodeId = String(input?.nodeId || input || '').trim();
|
||||
if (directNodeId === SIGNUP_CODE_NODE_ID || directNodeId === LOGIN_CODE_NODE_ID) {
|
||||
return directNodeId;
|
||||
}
|
||||
return Number(input?.step ?? input) === 4 ? SIGNUP_CODE_NODE_ID : LOGIN_CODE_NODE_ID;
|
||||
}
|
||||
|
||||
function getVisibleStepForNode(nodeId, state = {}) {
|
||||
if (nodeId === SIGNUP_CODE_NODE_ID) {
|
||||
return 4;
|
||||
}
|
||||
const explicitStep = Number(state?.visibleStep || state?.step);
|
||||
return Number.isInteger(explicitStep) && explicitStep > 0 ? explicitStep : 8;
|
||||
}
|
||||
|
||||
function getRuleDefinition(input, state = {}) {
|
||||
const nodeId = resolveVerificationNodeId(input);
|
||||
const normalizedStep = getVisibleStepForNode(nodeId, state);
|
||||
const mail2925Provider = isMail2925Provider(state);
|
||||
const signupStep = normalizedStep === 4;
|
||||
const signupStep = nodeId === SIGNUP_CODE_NODE_ID;
|
||||
const targetEmail = signupStep
|
||||
? state?.email
|
||||
: (String(state?.step8VerificationTargetEmail || '').trim() || state?.email);
|
||||
@@ -67,6 +86,7 @@
|
||||
return {
|
||||
flowId: 'openai',
|
||||
ruleId: signupStep ? SIGNUP_CODE_RULE_ID : LOGIN_CODE_RULE_ID,
|
||||
nodeId,
|
||||
step: normalizedStep,
|
||||
artifactType: 'code',
|
||||
codePatterns: OPENAI_CODE_PATTERNS,
|
||||
@@ -90,22 +110,34 @@
|
||||
};
|
||||
}
|
||||
|
||||
function buildVerificationPollPayload(step, state = {}, overrides = {}) {
|
||||
function getRuleDefinitionForNode(nodeId, state = {}) {
|
||||
return getRuleDefinition({ nodeId }, state);
|
||||
}
|
||||
|
||||
function buildVerificationPollPayload(input, state = {}, overrides = {}) {
|
||||
return {
|
||||
...getRuleDefinition(step, state),
|
||||
...getRuleDefinition(input, state),
|
||||
...(overrides || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildVerificationPollPayloadForNode(nodeId, state = {}, overrides = {}) {
|
||||
return buildVerificationPollPayload({ nodeId }, state, overrides);
|
||||
}
|
||||
|
||||
return {
|
||||
buildVerificationPollPayload,
|
||||
buildVerificationPollPayloadForNode,
|
||||
getRuleDefinition,
|
||||
getRuleDefinitionForNode,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
LOGIN_CODE_RULE_ID,
|
||||
LOGIN_CODE_NODE_ID,
|
||||
SIGNUP_CODE_RULE_ID,
|
||||
SIGNUP_CODE_NODE_ID,
|
||||
createOpenAiMailRules,
|
||||
};
|
||||
});
|
||||
|
||||
+35
-12
@@ -96,6 +96,15 @@
|
||||
driverId: 'content/vps-panel',
|
||||
cleanupScopes: [],
|
||||
},
|
||||
'platform-panel': {
|
||||
flowId: 'openai',
|
||||
kind: 'virtual-page',
|
||||
label: '平台回调面板',
|
||||
readyPolicy: 'disabled',
|
||||
family: 'platform-panel-family',
|
||||
driverId: 'content/platform-panel',
|
||||
cleanupScopes: [],
|
||||
},
|
||||
'sub2api-panel': {
|
||||
flowId: 'openai',
|
||||
kind: 'panel-page',
|
||||
@@ -156,13 +165,13 @@
|
||||
'content/signup-page': {
|
||||
sourceId: 'openai-auth',
|
||||
commands: [
|
||||
'OPEN_SIGNUP',
|
||||
'SUBMIT_SIGNUP_IDENTIFIER',
|
||||
'SUBMIT_PASSWORD',
|
||||
'SUBMIT_PROFILE',
|
||||
'SUBMIT_LOGIN_CODE',
|
||||
'SUBMIT_PHONE_CODE',
|
||||
'DETECT_AUTH_STATE',
|
||||
'submit-signup-email',
|
||||
'fill-password',
|
||||
'fill-profile',
|
||||
'oauth-login',
|
||||
'submit-verification-code',
|
||||
'confirm-oauth',
|
||||
'detect-auth-state',
|
||||
],
|
||||
},
|
||||
'content/qq-mail': {
|
||||
@@ -191,23 +200,27 @@
|
||||
},
|
||||
'content/sub2api-panel': {
|
||||
sourceId: 'sub2api-panel',
|
||||
commands: ['OPEN_PANEL', 'FETCH_OAUTH_URL', 'VERIFY_PLATFORM'],
|
||||
commands: ['open-panel', 'fetch-oauth-url', 'platform-verify'],
|
||||
},
|
||||
'content/vps-panel': {
|
||||
sourceId: 'vps-panel',
|
||||
commands: ['OPEN_PANEL', 'FETCH_OAUTH_URL'],
|
||||
commands: ['open-panel', 'fetch-oauth-url', 'platform-verify'],
|
||||
},
|
||||
'content/platform-panel': {
|
||||
sourceId: 'platform-panel',
|
||||
commands: ['platform-verify', 'fetch-oauth-url'],
|
||||
},
|
||||
'content/plus-checkout': {
|
||||
sourceId: 'plus-checkout',
|
||||
commands: ['CREATE_CHECKOUT', 'FILL_CHECKOUT'],
|
||||
commands: ['plus-checkout-create', 'plus-checkout-billing', 'plus-checkout-return'],
|
||||
},
|
||||
'content/paypal-flow': {
|
||||
sourceId: 'paypal-flow',
|
||||
commands: ['APPROVE_PAYPAL'],
|
||||
commands: ['paypal-approve'],
|
||||
},
|
||||
'content/gopay-flow': {
|
||||
sourceId: 'gopay-flow',
|
||||
commands: ['APPROVE_GOPAY'],
|
||||
commands: ['gopay-subscription-confirm'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -296,6 +309,15 @@
|
||||
};
|
||||
}
|
||||
|
||||
function driverAcceptsCommand(sourceOrDriverId, command) {
|
||||
const normalizedCommand = normalizeSourceId(command);
|
||||
if (!normalizedCommand) {
|
||||
return false;
|
||||
}
|
||||
const driver = getDriverMeta(sourceOrDriverId);
|
||||
return Array.isArray(driver?.commands) && driver.commands.includes(normalizedCommand);
|
||||
}
|
||||
|
||||
function isSignupPageHost(hostname = '') {
|
||||
return AUTH_PAGE_HOSTS.has(String(hostname || '').toLowerCase());
|
||||
}
|
||||
@@ -414,6 +436,7 @@
|
||||
getCleanupOwnerSource,
|
||||
getDriverIdForSource,
|
||||
getDriverMeta,
|
||||
driverAcceptsCommand,
|
||||
getSourceKeys,
|
||||
getSourceLabel,
|
||||
getSourceMeta,
|
||||
|
||||
+294
-88
@@ -543,9 +543,16 @@ let stepDefinitions = getStepDefinitionsForMode(false, {
|
||||
plusPaymentMethod: currentPlusPaymentMethod,
|
||||
signupMethod: currentSignupMethod,
|
||||
});
|
||||
let workflowNodes = getWorkflowNodesForMode(false, {
|
||||
plusPaymentMethod: currentPlusPaymentMethod,
|
||||
signupMethod: currentSignupMethod,
|
||||
});
|
||||
let STEP_IDS = stepDefinitions.map((step) => Number(step.id)).filter(Number.isFinite);
|
||||
let STEP_DEFAULT_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending']));
|
||||
let SKIPPABLE_STEPS = new Set(STEP_IDS);
|
||||
let NODE_IDS = workflowNodes.map((node) => String(node.nodeId || '').trim()).filter(Boolean);
|
||||
let NODE_DEFAULT_STATUSES = Object.fromEntries(NODE_IDS.map((nodeId) => [nodeId, 'pending']));
|
||||
let SKIPPABLE_NODES = new Set(NODE_IDS);
|
||||
const AUTO_DELAY_MIN_MINUTES = 1;
|
||||
const AUTO_DELAY_MAX_MINUTES = 1440;
|
||||
const AUTO_DELAY_DEFAULT_MINUTES = 30;
|
||||
@@ -816,6 +823,42 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function getWorkflowNodesForMode(plusModeEnabled = false, options = {}) {
|
||||
const defaultFlowId = typeof DEFAULT_ACTIVE_FLOW_ID !== 'undefined' ? DEFAULT_ACTIVE_FLOW_ID : 'openai';
|
||||
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
|
||||
const rawPaymentMethod = typeof options === 'string'
|
||||
? options
|
||||
: (options.plusPaymentMethod || currentPlusPaymentMethod || defaultMethod);
|
||||
const rawSignupMethod = typeof options === 'string'
|
||||
? currentSignupMethod
|
||||
: (options.signupMethod || currentSignupMethod || DEFAULT_SIGNUP_METHOD);
|
||||
const activeFlowId = typeof options === 'string'
|
||||
? ((typeof latestState !== 'undefined' ? latestState?.activeFlowId : '') || defaultFlowId)
|
||||
: (options.activeFlowId || (typeof latestState !== 'undefined' ? latestState?.activeFlowId : '') || defaultFlowId);
|
||||
const nodes = window.MultiPageStepDefinitions?.getNodes?.({
|
||||
activeFlowId: String(activeFlowId || '').trim().toLowerCase() || defaultFlowId,
|
||||
plusModeEnabled,
|
||||
plusPaymentMethod: normalizePlusPaymentMethod(rawPaymentMethod),
|
||||
signupMethod: normalizeSignupMethod(rawSignupMethod),
|
||||
});
|
||||
if (Array.isArray(nodes) && nodes.length) {
|
||||
return nodes.slice().sort((left, right) => {
|
||||
const leftOrder = Number.isFinite(Number(left.displayOrder)) ? Number(left.displayOrder) : Number(left.legacyStepId);
|
||||
const rightOrder = Number.isFinite(Number(right.displayOrder)) ? Number(right.displayOrder) : Number(right.legacyStepId);
|
||||
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
||||
return String(left.nodeId || '').localeCompare(String(right.nodeId || ''));
|
||||
});
|
||||
}
|
||||
|
||||
return getStepDefinitionsForMode(plusModeEnabled, options).map((step) => ({
|
||||
legacyStepId: Number(step.id),
|
||||
nodeId: String(step.key || '').trim(),
|
||||
title: step.title,
|
||||
displayOrder: Number.isFinite(Number(step.order)) ? Number(step.order) : Number(step.id),
|
||||
executeKey: String(step.key || '').trim(),
|
||||
})).filter((node) => node.nodeId);
|
||||
}
|
||||
|
||||
function getStepIdByKeyForCurrentMode(stepKey = '') {
|
||||
const normalizedKey = String(stepKey || '').trim();
|
||||
if (!normalizedKey) {
|
||||
@@ -825,6 +868,29 @@ function getStepIdByKeyForCurrentMode(stepKey = '') {
|
||||
return Number(match?.id) || 0;
|
||||
}
|
||||
|
||||
function getNodeIdByStepForCurrentMode(step) {
|
||||
const numericStep = Number(step);
|
||||
const node = (workflowNodes || []).find((candidate) => Number(candidate?.legacyStepId) === numericStep);
|
||||
if (node?.nodeId) {
|
||||
return String(node.nodeId).trim();
|
||||
}
|
||||
const definition = (stepDefinitions || []).find((candidate) => Number(candidate?.id) === numericStep);
|
||||
return String(definition?.key || '').trim();
|
||||
}
|
||||
|
||||
function getStepIdByNodeIdForCurrentMode(nodeId = '') {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
if (!normalizedNodeId) {
|
||||
return 0;
|
||||
}
|
||||
const node = (workflowNodes || []).find((candidate) => String(candidate?.nodeId || '').trim() === normalizedNodeId);
|
||||
const legacyStepId = Number(node?.legacyStepId);
|
||||
if (Number.isInteger(legacyStepId) && legacyStepId > 0) {
|
||||
return legacyStepId;
|
||||
}
|
||||
return getStepIdByKeyForCurrentMode(normalizedNodeId);
|
||||
}
|
||||
|
||||
function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
|
||||
currentPlusModeEnabled = Boolean(plusModeEnabled);
|
||||
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
|
||||
@@ -841,9 +907,33 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
|
||||
plusPaymentMethod: currentPlusPaymentMethod,
|
||||
signupMethod: currentSignupMethod,
|
||||
});
|
||||
const nextWorkflowNodes = typeof getWorkflowNodesForMode === 'function'
|
||||
? getWorkflowNodesForMode(currentPlusModeEnabled, {
|
||||
activeFlowId: options?.activeFlowId,
|
||||
plusPaymentMethod: currentPlusPaymentMethod,
|
||||
signupMethod: currentSignupMethod,
|
||||
})
|
||||
: stepDefinitions.map((step) => ({
|
||||
legacyStepId: Number(step.id),
|
||||
nodeId: String(step.key || step.id || '').trim(),
|
||||
title: step.title,
|
||||
displayOrder: Number.isFinite(Number(step.order)) ? Number(step.order) : Number(step.id),
|
||||
}));
|
||||
if (typeof workflowNodes !== 'undefined') {
|
||||
workflowNodes = nextWorkflowNodes;
|
||||
}
|
||||
STEP_IDS = stepDefinitions.map((step) => Number(step.id)).filter(Number.isFinite);
|
||||
STEP_DEFAULT_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending']));
|
||||
SKIPPABLE_STEPS = new Set(STEP_IDS);
|
||||
if (typeof NODE_IDS !== 'undefined') {
|
||||
NODE_IDS = nextWorkflowNodes.map((node) => String(node.nodeId || '').trim()).filter(Boolean);
|
||||
}
|
||||
if (typeof NODE_DEFAULT_STATUSES !== 'undefined') {
|
||||
NODE_DEFAULT_STATUSES = Object.fromEntries((typeof NODE_IDS !== 'undefined' ? NODE_IDS : []).map((nodeId) => [nodeId, 'pending']));
|
||||
}
|
||||
if (typeof SKIPPABLE_NODES !== 'undefined') {
|
||||
SKIPPABLE_NODES = new Set(typeof NODE_IDS !== 'undefined' ? NODE_IDS : []);
|
||||
}
|
||||
}
|
||||
const CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY = 'multipage-contribution-content-prompt-dismissed-version';
|
||||
const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 3;
|
||||
@@ -1189,7 +1279,7 @@ function shouldAttachAutomationWindow(message = {}) {
|
||||
return false;
|
||||
}
|
||||
return [
|
||||
'EXECUTE_STEP',
|
||||
'EXECUTE_NODE',
|
||||
'AUTO_RUN',
|
||||
'SCHEDULE_AUTO_RUN',
|
||||
'RESUME_AUTO_RUN',
|
||||
@@ -2111,31 +2201,64 @@ function isDoneStatus(status) {
|
||||
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
||||
}
|
||||
|
||||
function escapeCssValue(value = '') {
|
||||
const raw = String(value || '');
|
||||
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
||||
return CSS.escape(raw);
|
||||
}
|
||||
return raw.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
function getNodeStatuses(state = latestState) {
|
||||
const merged = { ...NODE_DEFAULT_STATUSES, ...(state?.nodeStatuses || {}) };
|
||||
return Object.fromEntries(NODE_IDS.map((nodeId) => [nodeId, merged[nodeId] || 'pending']));
|
||||
}
|
||||
|
||||
function getStepStatuses(state = latestState) {
|
||||
const merged = { ...STEP_DEFAULT_STATUSES, ...(state?.stepStatuses || {}) };
|
||||
const merged = { ...STEP_DEFAULT_STATUSES };
|
||||
if (typeof getNodeStatuses === 'function') {
|
||||
const nodeStatuses = getNodeStatuses(state);
|
||||
for (const [nodeId, status] of Object.entries(nodeStatuses)) {
|
||||
const step = getStepIdByNodeIdForCurrentMode(nodeId);
|
||||
if (step) {
|
||||
merged[step] = status || 'pending';
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.fromEntries(STEP_IDS.map((stepId) => [stepId, merged[stepId] || 'pending']));
|
||||
}
|
||||
|
||||
function getFirstUnfinishedNode(state = latestState) {
|
||||
const statuses = getNodeStatuses(state);
|
||||
for (const nodeId of NODE_IDS) {
|
||||
if (!isDoneStatus(statuses[nodeId])) {
|
||||
return nodeId;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getFirstUnfinishedStep(state = latestState) {
|
||||
const statuses = getStepStatuses(state);
|
||||
for (const step of STEP_IDS) {
|
||||
if (!isDoneStatus(statuses[step])) {
|
||||
return step;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
const nodeId = getFirstUnfinishedNode(state);
|
||||
return nodeId ? getStepIdByNodeIdForCurrentMode(nodeId) : null;
|
||||
}
|
||||
|
||||
function getRunningNodes(state = latestState) {
|
||||
const statuses = getNodeStatuses(state);
|
||||
return Object.entries(statuses)
|
||||
.filter(([, status]) => status === 'running')
|
||||
.map(([nodeId]) => nodeId);
|
||||
}
|
||||
|
||||
function getRunningSteps(state = latestState) {
|
||||
const statuses = getStepStatuses(state);
|
||||
return Object.entries(statuses)
|
||||
.filter(([, status]) => status === 'running')
|
||||
.map(([step]) => Number(step))
|
||||
return getRunningNodes(state)
|
||||
.map((nodeId) => getStepIdByNodeIdForCurrentMode(nodeId))
|
||||
.filter((step) => Number.isInteger(step) && step > 0)
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function hasSavedProgress(state = latestState) {
|
||||
const statuses = getStepStatuses(state);
|
||||
const statuses = getNodeStatuses(state);
|
||||
return Object.values(statuses).some((status) => status !== 'pending');
|
||||
}
|
||||
|
||||
@@ -2150,14 +2273,14 @@ function shouldOfferAutoModeChoice(state = latestState) {
|
||||
}
|
||||
|
||||
function syncLatestState(nextState) {
|
||||
const mergedStepStatuses = nextState?.stepStatuses
|
||||
? { ...STEP_DEFAULT_STATUSES, ...(latestState?.stepStatuses || {}), ...nextState.stepStatuses }
|
||||
: getStepStatuses(latestState);
|
||||
const mergedNodeStatuses = nextState?.nodeStatuses
|
||||
? { ...NODE_DEFAULT_STATUSES, ...(latestState?.nodeStatuses || {}), ...nextState.nodeStatuses }
|
||||
: getNodeStatuses(latestState);
|
||||
|
||||
latestState = {
|
||||
...(latestState || {}),
|
||||
...(nextState || {}),
|
||||
stepStatuses: mergedStepStatuses,
|
||||
nodeStatuses: mergedNodeStatuses,
|
||||
};
|
||||
|
||||
renderAccountRecords(latestState);
|
||||
@@ -3360,8 +3483,20 @@ function collectSettingsPayload() {
|
||||
const currentPhoneSmsMaxPriceValue = typeof inputHeroSmsMaxPrice !== 'undefined' && inputHeroSmsMaxPrice
|
||||
? normalizePhoneSmsMaxPriceValue(inputHeroSmsMaxPrice.value, phoneSmsProviderValue)
|
||||
: '';
|
||||
const normalizePhoneSmsMinPriceValueSafe = typeof normalizePhoneSmsMinPriceValue === 'function'
|
||||
? normalizePhoneSmsMinPriceValue
|
||||
: ((value = '', provider = phoneSmsProviderValue) => {
|
||||
const normalizedProvider = normalizePhoneSmsProvider(provider);
|
||||
if (normalizedProvider === PHONE_SMS_PROVIDER_FIVE_SIM && typeof normalizeFiveSimMaxPriceValue === 'function') {
|
||||
return normalizeFiveSimMaxPriceValue(value);
|
||||
}
|
||||
if (typeof normalizeHeroSmsMaxPriceValue === 'function') {
|
||||
return normalizeHeroSmsMaxPriceValue(value);
|
||||
}
|
||||
return String(value || '').trim();
|
||||
});
|
||||
const currentPhoneSmsMinPriceValue = typeof inputHeroSmsMinPrice !== 'undefined' && inputHeroSmsMinPrice
|
||||
? normalizePhoneSmsMinPriceValue(inputHeroSmsMinPrice.value, phoneSmsProviderValue)
|
||||
? normalizePhoneSmsMinPriceValueSafe(inputHeroSmsMinPrice.value, phoneSmsProviderValue)
|
||||
: '';
|
||||
const heroSmsMaxPriceValue = phoneSmsProviderValue === PHONE_SMS_PROVIDER_HERO_SMS
|
||||
? currentPhoneSmsMaxPriceValue
|
||||
@@ -3370,11 +3505,11 @@ function collectSettingsPayload() {
|
||||
? currentPhoneSmsMaxPriceValue
|
||||
: normalizeFiveSimMaxPriceValue(latestState?.fiveSimMaxPrice || '');
|
||||
const heroSmsMinPriceValue = phoneSmsProviderValue === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? normalizePhoneSmsMinPriceValue(latestState?.heroSmsMinPrice || '', PHONE_SMS_PROVIDER_HERO_SMS)
|
||||
? normalizePhoneSmsMinPriceValueSafe(latestState?.heroSmsMinPrice || '', PHONE_SMS_PROVIDER_HERO_SMS)
|
||||
: currentPhoneSmsMinPriceValue;
|
||||
const fiveSimMinPriceValue = phoneSmsProviderValue === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? currentPhoneSmsMinPriceValue
|
||||
: normalizePhoneSmsMinPriceValue(latestState?.fiveSimMinPrice || '', PHONE_SMS_PROVIDER_FIVE_SIM);
|
||||
: normalizePhoneSmsMinPriceValueSafe(latestState?.fiveSimMinPrice || '', PHONE_SMS_PROVIDER_FIVE_SIM);
|
||||
const defaultFiveSimProduct = typeof DEFAULT_FIVE_SIM_PRODUCT !== 'undefined'
|
||||
? DEFAULT_FIVE_SIM_PRODUCT
|
||||
: 'openai';
|
||||
@@ -8623,6 +8758,7 @@ function initializeManualStepActions() {
|
||||
return;
|
||||
}
|
||||
const step = Number(row.dataset.step);
|
||||
const nodeId = String(row.dataset.nodeId || getNodeIdByStepForCurrentMode(step) || '').trim();
|
||||
const statusEl = row.querySelector('.step-status');
|
||||
if (!statusEl) return;
|
||||
|
||||
@@ -8633,13 +8769,14 @@ function initializeManualStepActions() {
|
||||
manualBtn.type = 'button';
|
||||
manualBtn.className = 'step-manual-btn';
|
||||
manualBtn.dataset.step = String(step);
|
||||
manualBtn.title = '跳过此步';
|
||||
manualBtn.setAttribute('aria-label', `跳过步骤 ${step}`);
|
||||
manualBtn.dataset.nodeId = nodeId;
|
||||
manualBtn.title = '跳过此节点';
|
||||
manualBtn.setAttribute('aria-label', `跳过节点 ${nodeId || step}`);
|
||||
manualBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>';
|
||||
manualBtn.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await handleSkipStep(step);
|
||||
await handleSkipNode(nodeId || getNodeIdByStepForCurrentMode(step));
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
@@ -8654,16 +8791,20 @@ function initializeManualStepActions() {
|
||||
function renderStepsList() {
|
||||
if (!stepsList) return;
|
||||
|
||||
stepsList.innerHTML = stepDefinitions.map((step) => `
|
||||
<div class="step-row" data-step="${step.id}" data-step-key="${escapeHtml(step.key)}">
|
||||
<div class="step-indicator" data-step="${step.id}"><span class="step-num">${step.id}</span></div>
|
||||
<button class="step-btn" data-step="${step.id}" data-step-key="${escapeHtml(step.key)}">${escapeHtml(step.title)}</button>
|
||||
<span class="step-status" data-step="${step.id}"></span>
|
||||
stepsList.innerHTML = workflowNodes.map((node) => {
|
||||
const step = getStepIdByNodeIdForCurrentMode(node.nodeId);
|
||||
const nodeId = String(node.nodeId || '').trim();
|
||||
return `
|
||||
<div class="step-row" data-step="${step}" data-node-id="${escapeHtml(nodeId)}" data-step-key="${escapeHtml(node.executeKey || nodeId)}">
|
||||
<div class="step-indicator" data-step="${step}" data-node-id="${escapeHtml(nodeId)}"><span class="step-num">${step || node.displayOrder || ''}</span></div>
|
||||
<button class="step-btn" data-step="${step}" data-node-id="${escapeHtml(nodeId)}" data-step-key="${escapeHtml(node.executeKey || nodeId)}">${escapeHtml(node.title)}</button>
|
||||
<span class="step-status" data-step="${step}" data-node-id="${escapeHtml(nodeId)}"></span>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
if (stepsProgress) {
|
||||
stepsProgress.textContent = `0 / ${STEP_IDS.length}`;
|
||||
stepsProgress.textContent = `0 / ${NODE_IDS.length}`;
|
||||
}
|
||||
|
||||
initializeManualStepActions();
|
||||
@@ -8728,6 +8869,7 @@ function applySettingsState(state) {
|
||||
signupMethod: normalizeSignupMethod(state?.signupMethod || DEFAULT_SIGNUP_METHOD),
|
||||
};
|
||||
syncStepDefinitionsForMode(stepDefinitionState.plusModeEnabled, {
|
||||
activeFlowId: state?.flowId || state?.activeFlowId,
|
||||
plusPaymentMethod: state?.plusPaymentMethod,
|
||||
signupMethod: stepDefinitionState.signupMethod,
|
||||
});
|
||||
@@ -9282,9 +9424,9 @@ async function restoreState() {
|
||||
displayLocalhostUrl.textContent = state.localhostUrl;
|
||||
displayLocalhostUrl.classList.add('has-value');
|
||||
}
|
||||
if (state.stepStatuses) {
|
||||
for (const [step, status] of Object.entries(state.stepStatuses)) {
|
||||
updateStepUI(Number(step), status);
|
||||
if (state.nodeStatuses) {
|
||||
for (const [nodeId, status] of Object.entries(state.nodeStatuses)) {
|
||||
updateNodeUI(nodeId, status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10911,21 +11053,54 @@ function updatePanelModeUI() {
|
||||
// UI Updates
|
||||
// ============================================================
|
||||
|
||||
function updateStepUI(step, status) {
|
||||
function updateNodeUI(nodeId, status) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
if (!normalizedNodeId) return;
|
||||
syncLatestState({
|
||||
stepStatuses: {
|
||||
...getStepStatuses(),
|
||||
[step]: status,
|
||||
nodeStatuses: {
|
||||
...getNodeStatuses(),
|
||||
[normalizedNodeId]: status,
|
||||
},
|
||||
});
|
||||
|
||||
renderSingleStepStatus(step, status);
|
||||
renderSingleNodeStatus(normalizedNodeId, status);
|
||||
updateButtonStates();
|
||||
updateProgressCounter();
|
||||
updateConfigMenuControls();
|
||||
}
|
||||
|
||||
function updateStepUI(step, status) {
|
||||
const nodeId = getNodeIdByStepForCurrentMode(step);
|
||||
if (nodeId) {
|
||||
updateNodeUI(nodeId, status);
|
||||
return;
|
||||
}
|
||||
updateButtonStates();
|
||||
updateProgressCounter();
|
||||
updateConfigMenuControls();
|
||||
}
|
||||
|
||||
function renderSingleNodeStatus(nodeId, status) {
|
||||
const normalizedStatus = status || 'pending';
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
const selectorNodeId = escapeCssValue(normalizedNodeId);
|
||||
const statusEl = document.querySelector(`.step-status[data-node-id="${selectorNodeId}"]`);
|
||||
const row = document.querySelector(`.step-row[data-node-id="${selectorNodeId}"]`);
|
||||
|
||||
if (statusEl) statusEl.textContent = STATUS_ICONS[normalizedStatus] || '';
|
||||
if (row) {
|
||||
row.className = `step-row ${normalizedStatus}`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSingleStepStatus(step, status) {
|
||||
const nodeId = typeof getNodeIdByStepForCurrentMode === 'function'
|
||||
? getNodeIdByStepForCurrentMode(step)
|
||||
: '';
|
||||
if (nodeId && typeof renderSingleNodeStatus === 'function') {
|
||||
renderSingleNodeStatus(nodeId, status);
|
||||
return;
|
||||
}
|
||||
const normalizedStatus = status || 'pending';
|
||||
const statusEl = document.querySelector(`.step-status[data-step="${step}"]`);
|
||||
const row = document.querySelector(`.step-row[data-step="${step}"]`);
|
||||
@@ -10937,20 +11112,32 @@ function renderSingleStepStatus(step, status) {
|
||||
}
|
||||
|
||||
function renderStepStatuses(state = latestState) {
|
||||
if (typeof getNodeStatuses === 'function' && typeof NODE_IDS !== 'undefined') {
|
||||
const statuses = getNodeStatuses(state);
|
||||
for (const nodeId of NODE_IDS) {
|
||||
renderSingleNodeStatus(nodeId, statuses[nodeId]);
|
||||
}
|
||||
} else {
|
||||
const statuses = getStepStatuses(state);
|
||||
for (const step of STEP_IDS) {
|
||||
renderSingleStepStatus(step, statuses[step]);
|
||||
}
|
||||
}
|
||||
updateProgressCounter();
|
||||
}
|
||||
|
||||
function updateProgressCounter() {
|
||||
if (typeof getNodeStatuses === 'function' && typeof NODE_IDS !== 'undefined') {
|
||||
const completed = Object.values(getNodeStatuses()).filter(isDoneStatus).length;
|
||||
stepsProgress.textContent = `${completed} / ${NODE_IDS.length}`;
|
||||
return;
|
||||
}
|
||||
const completed = Object.values(getStepStatuses()).filter(isDoneStatus).length;
|
||||
stepsProgress.textContent = `${completed} / ${STEP_IDS.length}`;
|
||||
}
|
||||
|
||||
function updateButtonStates() {
|
||||
const statuses = getStepStatuses();
|
||||
const statuses = getNodeStatuses();
|
||||
const anyRunning = Object.values(statuses).some(s => s === 'running');
|
||||
const autoLocked = isAutoRunLockedPhase();
|
||||
const autoScheduled = isAutoRunScheduledPhase();
|
||||
@@ -10958,47 +11145,49 @@ function updateButtonStates() {
|
||||
? selectIcloudTargetMailboxType?.value
|
||||
: latestState?.icloudTargetMailboxType;
|
||||
|
||||
for (const step of STEP_IDS) {
|
||||
const btn = document.querySelector(`.step-btn[data-step="${step}"]`);
|
||||
for (const nodeId of NODE_IDS) {
|
||||
const step = getStepIdByNodeIdForCurrentMode(nodeId);
|
||||
const btn = document.querySelector(`.step-btn[data-node-id="${escapeCssValue(nodeId)}"]`);
|
||||
if (!btn) continue;
|
||||
|
||||
if (anyRunning || autoLocked || autoScheduled) {
|
||||
btn.disabled = true;
|
||||
} else if (step === 1) {
|
||||
} else if (NODE_IDS.indexOf(nodeId) === 0) {
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
const currentIndex = STEP_IDS.indexOf(step);
|
||||
const prevStep = currentIndex > 0 ? STEP_IDS[currentIndex - 1] : null;
|
||||
const prevStatus = prevStep === null ? 'completed' : statuses[prevStep];
|
||||
const currentStatus = statuses[step];
|
||||
const currentIndex = NODE_IDS.indexOf(nodeId);
|
||||
const prevNodeId = currentIndex > 0 ? NODE_IDS[currentIndex - 1] : null;
|
||||
const prevStatus = prevNodeId === null ? 'completed' : statuses[prevNodeId];
|
||||
const currentStatus = statuses[nodeId];
|
||||
btn.disabled = !(isDoneStatus(prevStatus) || currentStatus === 'failed' || isDoneStatus(currentStatus) || currentStatus === 'stopped');
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.step-manual-btn').forEach((btn) => {
|
||||
const step = Number(btn.dataset.step);
|
||||
const currentStatus = statuses[step];
|
||||
const currentIndex = STEP_IDS.indexOf(step);
|
||||
const prevStep = currentIndex > 0 ? STEP_IDS[currentIndex - 1] : null;
|
||||
const prevStatus = prevStep === null ? 'completed' : statuses[prevStep];
|
||||
const nodeId = String(btn.dataset.nodeId || getNodeIdByStepForCurrentMode(step) || '').trim();
|
||||
const currentStatus = statuses[nodeId];
|
||||
const currentIndex = NODE_IDS.indexOf(nodeId);
|
||||
const prevNodeId = currentIndex > 0 ? NODE_IDS[currentIndex - 1] : null;
|
||||
const prevStatus = prevNodeId === null ? 'completed' : statuses[prevNodeId];
|
||||
|
||||
if (!SKIPPABLE_STEPS.has(step) || anyRunning || autoLocked || autoScheduled || currentStatus === 'running' || isDoneStatus(currentStatus)) {
|
||||
if (!SKIPPABLE_NODES.has(nodeId) || anyRunning || autoLocked || autoScheduled || currentStatus === 'running' || isDoneStatus(currentStatus)) {
|
||||
btn.style.display = 'none';
|
||||
btn.disabled = true;
|
||||
btn.title = '当前不可跳过';
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevStep !== null && !isDoneStatus(prevStatus)) {
|
||||
if (prevNodeId !== null && !isDoneStatus(prevStatus)) {
|
||||
btn.style.display = 'none';
|
||||
btn.disabled = true;
|
||||
btn.title = `请先完成步骤 ${prevStep}`;
|
||||
btn.title = `请先完成节点 ${prevNodeId}`;
|
||||
return;
|
||||
}
|
||||
|
||||
btn.style.display = '';
|
||||
btn.disabled = false;
|
||||
btn.title = `跳过步骤 ${step}`;
|
||||
btn.title = `跳过节点 ${nodeId}`;
|
||||
});
|
||||
|
||||
btnReset.disabled = anyRunning || autoScheduled || isAutoRunPausedPhase() || autoLocked;
|
||||
@@ -11032,9 +11221,10 @@ function updateStopButtonState(active) {
|
||||
}
|
||||
|
||||
function updateStatusDisplay(state) {
|
||||
if (!state || !state.stepStatuses) return;
|
||||
if (!state || !state.nodeStatuses) return;
|
||||
|
||||
statusBar.className = 'status-bar';
|
||||
const nodeStatuses = getNodeStatuses(state);
|
||||
|
||||
const countdown = getActiveAutoRunCountdown();
|
||||
if (countdown) {
|
||||
@@ -11064,17 +11254,17 @@ function updateStatusDisplay(state) {
|
||||
}
|
||||
|
||||
if (isAutoRunWaitingStepPhase()) {
|
||||
const runningSteps = getRunningSteps(state);
|
||||
displayStatus.textContent = runningSteps.length
|
||||
? `自动等待步骤 ${runningSteps.join(', ')} 完成后继续${getAutoRunLabel()}`
|
||||
const runningNodes = getRunningNodes(state);
|
||||
displayStatus.textContent = runningNodes.length
|
||||
? `自动等待节点 ${runningNodes.join(', ')} 完成后继续${getAutoRunLabel()}`
|
||||
: `自动正在按最新进度准备继续${getAutoRunLabel()}`;
|
||||
statusBar.classList.add('running');
|
||||
return;
|
||||
}
|
||||
|
||||
const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
|
||||
const running = Object.entries(nodeStatuses).find(([, s]) => s === 'running');
|
||||
if (running) {
|
||||
displayStatus.textContent = `步骤 ${running[0]} 运行中...`;
|
||||
displayStatus.textContent = `节点 ${running[0]} 运行中...`;
|
||||
statusBar.classList.add('running');
|
||||
return;
|
||||
}
|
||||
@@ -11085,32 +11275,32 @@ function updateStatusDisplay(state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const failed = Object.entries(state.stepStatuses).find(([, s]) => s === 'failed');
|
||||
const failed = Object.entries(nodeStatuses).find(([, s]) => s === 'failed');
|
||||
if (failed) {
|
||||
displayStatus.textContent = `步骤 ${failed[0]} 失败`;
|
||||
displayStatus.textContent = `节点 ${failed[0]} 失败`;
|
||||
statusBar.classList.add('failed');
|
||||
return;
|
||||
}
|
||||
|
||||
const stopped = Object.entries(state.stepStatuses).find(([, s]) => s === 'stopped');
|
||||
const stopped = Object.entries(nodeStatuses).find(([, s]) => s === 'stopped');
|
||||
if (stopped) {
|
||||
displayStatus.textContent = `步骤 ${stopped[0]} 已停止`;
|
||||
displayStatus.textContent = `节点 ${stopped[0]} 已停止`;
|
||||
statusBar.classList.add('stopped');
|
||||
return;
|
||||
}
|
||||
|
||||
const lastCompleted = Object.entries(state.stepStatuses)
|
||||
const lastCompleted = Object.entries(nodeStatuses)
|
||||
.filter(([, s]) => isDoneStatus(s))
|
||||
.map(([k]) => Number(k))
|
||||
.sort((a, b) => b - a)[0];
|
||||
.map(([nodeId]) => nodeId)
|
||||
.sort((left, right) => NODE_IDS.indexOf(right) - NODE_IDS.indexOf(left))[0];
|
||||
|
||||
if (lastCompleted === STEP_IDS[STEP_IDS.length - 1]) {
|
||||
displayStatus.textContent = (state.stepStatuses[lastCompleted] === 'manual_completed' || state.stepStatuses[lastCompleted] === 'skipped') ? '全部步骤已跳过/完成' : '全部步骤已完成';
|
||||
if (lastCompleted === NODE_IDS[NODE_IDS.length - 1]) {
|
||||
displayStatus.textContent = (nodeStatuses[lastCompleted] === 'manual_completed' || nodeStatuses[lastCompleted] === 'skipped') ? '全部节点已跳过/完成' : '全部节点已完成';
|
||||
statusBar.classList.add('completed');
|
||||
} else if (lastCompleted) {
|
||||
displayStatus.textContent = (state.stepStatuses[lastCompleted] === 'manual_completed' || state.stepStatuses[lastCompleted] === 'skipped')
|
||||
? `步骤 ${lastCompleted} 已跳过`
|
||||
: `步骤 ${lastCompleted} 已完成`;
|
||||
displayStatus.textContent = (nodeStatuses[lastCompleted] === 'manual_completed' || nodeStatuses[lastCompleted] === 'skipped')
|
||||
? `节点 ${lastCompleted} 已跳过`
|
||||
: `节点 ${lastCompleted} 已完成`;
|
||||
} else {
|
||||
displayStatus.textContent = '就绪';
|
||||
}
|
||||
@@ -11842,7 +12032,11 @@ async function maybeTakeoverAutoRun(actionLabel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSkipStep(step) {
|
||||
async function handleSkipNode(nodeId) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
if (!normalizedNodeId) {
|
||||
throw new Error('缺少要跳过的节点。');
|
||||
}
|
||||
if (isAutoRunPausedPhase()) {
|
||||
const takeoverResponse = await chrome.runtime.sendMessage({
|
||||
type: 'TAKEOVER_AUTO_RUN',
|
||||
@@ -11857,16 +12051,27 @@ async function handleSkipStep(step) {
|
||||
await persistCurrentSettingsForAction();
|
||||
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'SKIP_STEP',
|
||||
type: 'SKIP_NODE',
|
||||
source: 'sidepanel',
|
||||
payload: { step },
|
||||
payload: {
|
||||
nodeId: normalizedNodeId,
|
||||
step: getStepIdByNodeIdForCurrentMode(normalizedNodeId),
|
||||
},
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
showToast(`步骤 ${step} 已跳过`, 'success', 2200);
|
||||
showToast(`节点 ${normalizedNodeId} 已跳过`, 'success', 2200);
|
||||
}
|
||||
|
||||
async function handleSkipStep(step) {
|
||||
const nodeId = getNodeIdByStepForCurrentMode(step);
|
||||
if (!nodeId) {
|
||||
throw new Error(`无效步骤:${step}`);
|
||||
}
|
||||
return handleSkipNode(nodeId);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -11880,7 +12085,8 @@ stepsList?.addEventListener('click', async (event) => {
|
||||
}
|
||||
try {
|
||||
const step = Number(btn.dataset.step);
|
||||
if (!(await maybeTakeoverAutoRun(`执行步骤 ${step}`))) {
|
||||
const nodeId = String(btn.dataset.nodeId || getNodeIdByStepForCurrentMode(step) || '').trim();
|
||||
if (!(await maybeTakeoverAutoRun(`执行节点 ${nodeId || step}`))) {
|
||||
return;
|
||||
}
|
||||
await persistCurrentSettingsForAction();
|
||||
@@ -11898,12 +12104,12 @@ stepsList?.addEventListener('click', async (event) => {
|
||||
syncLatestState({ customPassword: inputPassword.value });
|
||||
}
|
||||
if (shouldExecuteStep3WithSignupPhoneIdentity(latestState)) {
|
||||
const response = await sendSidepanelMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
|
||||
const response = await sendSidepanelMessage({ type: 'EXECUTE_NODE', source: 'sidepanel', payload: { nodeId } });
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
} else if (selectMailProvider.value === 'hotmail-api' || isLuckmailProvider()) {
|
||||
const response = await sendSidepanelMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
|
||||
const response = await sendSidepanelMessage({ type: 'EXECUTE_NODE', source: 'sidepanel', payload: { nodeId } });
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
@@ -11913,7 +12119,7 @@ stepsList?.addEventListener('click', async (event) => {
|
||||
showToast(selectMailProvider.value === GMAIL_PROVIDER ? '请先填写 Gmail 原邮箱。' : '请先填写 2925 邮箱前缀。', 'warn');
|
||||
return;
|
||||
}
|
||||
const response = await sendSidepanelMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, emailPrefix } });
|
||||
const response = await sendSidepanelMessage({ type: 'EXECUTE_NODE', source: 'sidepanel', payload: { nodeId, emailPrefix } });
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
@@ -11934,13 +12140,13 @@ stepsList?.addEventListener('click', async (event) => {
|
||||
if (!validateCurrentRegistrationEmail(email, { showToastOnFailure: true })) {
|
||||
return;
|
||||
}
|
||||
const response = await sendSidepanelMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
|
||||
const response = await sendSidepanelMessage({ type: 'EXECUTE_NODE', source: 'sidepanel', payload: { nodeId, email } });
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const response = await sendSidepanelMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
|
||||
const response = await sendSidepanelMessage({ type: 'EXECUTE_NODE', source: 'sidepanel', payload: { nodeId } });
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
@@ -12323,7 +12529,7 @@ btnReset.addEventListener('click', async () => {
|
||||
|
||||
await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
|
||||
syncLatestState({
|
||||
stepStatuses: STEP_DEFAULT_STATUSES,
|
||||
nodeStatuses: NODE_DEFAULT_STATUSES,
|
||||
currentHotmailAccountId: null,
|
||||
currentLuckmailPurchase: null,
|
||||
currentLuckmailMailCursor: null,
|
||||
@@ -14222,9 +14428,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'STEP_STATUS_CHANGED': {
|
||||
const { step, status } = message.payload;
|
||||
updateStepUI(step, status);
|
||||
case 'NODE_STATUS_CHANGED': {
|
||||
const { nodeId, status } = message.payload;
|
||||
updateNodeUI(nodeId, status);
|
||||
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => {
|
||||
syncLatestState(state);
|
||||
syncAutoRunState(state);
|
||||
@@ -14253,7 +14459,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
localhostUrl: null,
|
||||
email: null,
|
||||
password: null,
|
||||
stepStatuses: STEP_DEFAULT_STATUSES,
|
||||
nodeStatuses: NODE_DEFAULT_STATUSES,
|
||||
logs: [],
|
||||
scheduledAutoRunAt: null,
|
||||
autoRunCountdownAt: null,
|
||||
|
||||
@@ -5,6 +5,119 @@ const fs = require('node:fs');
|
||||
const source = fs.readFileSync('background/auto-run-controller.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
|
||||
const rawCreateAutoRunController = api.createAutoRunController.bind(api);
|
||||
|
||||
const TEST_STEP_NODE_IDS = Object.freeze({
|
||||
1: 'open-chatgpt',
|
||||
2: 'submit-signup-email',
|
||||
3: 'fill-password',
|
||||
4: 'fetch-signup-code',
|
||||
5: 'fill-profile',
|
||||
6: 'wait-registration-success',
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
9: 'confirm-oauth',
|
||||
10: 'platform-verify',
|
||||
});
|
||||
|
||||
const TEST_NODE_STEP_IDS = Object.fromEntries(
|
||||
Object.entries(TEST_STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)])
|
||||
);
|
||||
|
||||
function getTestNodeIdByStep(step) {
|
||||
return TEST_STEP_NODE_IDS[Number(step)] || '';
|
||||
}
|
||||
|
||||
function getTestStepIdByNodeId(nodeId) {
|
||||
return TEST_NODE_STEP_IDS[String(nodeId || '').trim()] || null;
|
||||
}
|
||||
|
||||
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
|
||||
const nodeStatuses = {};
|
||||
for (const [step, status] of Object.entries(stepStatuses || {})) {
|
||||
const nodeId = getTestNodeIdByStep(step);
|
||||
if (nodeId) {
|
||||
nodeStatuses[nodeId] = status;
|
||||
}
|
||||
}
|
||||
return nodeStatuses;
|
||||
}
|
||||
|
||||
function normalizeAutoRunControllerTestDeps(deps = {}) {
|
||||
const normalized = { ...deps };
|
||||
|
||||
if (typeof normalized.getState === 'function') {
|
||||
const getState = normalized.getState;
|
||||
normalized.getState = async () => {
|
||||
const state = await getState();
|
||||
return {
|
||||
...state,
|
||||
nodeStatuses: {
|
||||
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}),
|
||||
...(state?.nodeStatuses || {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
typeof normalized.runAutoSequenceFromNode !== 'function'
|
||||
&& typeof normalized.runAutoSequenceFromStep === 'function'
|
||||
) {
|
||||
const runAutoSequenceFromStep = normalized.runAutoSequenceFromStep;
|
||||
normalized.runAutoSequenceFromNode = async (nodeId, context = {}) => (
|
||||
runAutoSequenceFromStep(getTestStepIdByNodeId(nodeId) || 1, context)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof normalized.getFirstUnfinishedNodeId !== 'function'
|
||||
&& typeof normalized.getFirstUnfinishedStep === 'function'
|
||||
) {
|
||||
const getFirstUnfinishedStep = normalized.getFirstUnfinishedStep;
|
||||
normalized.getFirstUnfinishedNodeId = (statuses = {}, state = {}) => (
|
||||
getTestNodeIdByStep(getFirstUnfinishedStep(statuses, state))
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof normalized.getRunningNodeIds !== 'function'
|
||||
&& typeof normalized.getRunningSteps === 'function'
|
||||
) {
|
||||
const getRunningSteps = normalized.getRunningSteps;
|
||||
normalized.getRunningNodeIds = (statuses = {}, state = {}) => (
|
||||
getRunningSteps(statuses, state)
|
||||
.map(getTestNodeIdByStep)
|
||||
.filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof normalized.hasSavedNodeProgress !== 'function'
|
||||
&& typeof normalized.hasSavedProgress === 'function'
|
||||
) {
|
||||
normalized.hasSavedNodeProgress = normalized.hasSavedProgress;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof normalized.waitForRunningNodesToFinish !== 'function'
|
||||
&& typeof normalized.waitForRunningStepsToFinish === 'function'
|
||||
) {
|
||||
normalized.waitForRunningNodesToFinish = normalized.waitForRunningStepsToFinish;
|
||||
}
|
||||
|
||||
delete normalized.runAutoSequenceFromStep;
|
||||
delete normalized.getFirstUnfinishedStep;
|
||||
delete normalized.getRunningSteps;
|
||||
delete normalized.hasSavedProgress;
|
||||
delete normalized.waitForRunningStepsToFinish;
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
api.createAutoRunController = (deps = {}) => (
|
||||
rawCreateAutoRunController(normalizeAutoRunControllerTestDeps(deps))
|
||||
);
|
||||
|
||||
test('auto-run controller skips add-phone failures to the next round instead of stopping when auto retry is enabled', async () => {
|
||||
const events = {
|
||||
|
||||
@@ -72,6 +72,33 @@ const AUTO_RUN_RETRY_DELAY_MS = 3000;
|
||||
const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
|
||||
const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
|
||||
const STEP_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
const STEP_NODE_IDS = {
|
||||
1: 'open-chatgpt',
|
||||
2: 'submit-signup-email',
|
||||
3: 'fill-password',
|
||||
4: 'fetch-signup-code',
|
||||
5: 'fill-profile',
|
||||
6: 'wait-registration-success',
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
9: 'confirm-oauth',
|
||||
10: 'platform-verify',
|
||||
};
|
||||
const NODE_STEP_IDS = Object.fromEntries(Object.entries(STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)]));
|
||||
function getNodeIdByStepForState(step) {
|
||||
return STEP_NODE_IDS[Number(step)] || '';
|
||||
}
|
||||
function getStepIdByNodeIdForState(nodeId) {
|
||||
return NODE_STEP_IDS[String(nodeId || '').trim()] || null;
|
||||
}
|
||||
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
|
||||
const nodeStatuses = {};
|
||||
for (const [step, status] of Object.entries(stepStatuses || {})) {
|
||||
const nodeId = getNodeIdByStepForState(step);
|
||||
if (nodeId) nodeStatuses[nodeId] = status;
|
||||
}
|
||||
return nodeStatuses;
|
||||
}
|
||||
const DEFAULT_STATE = {
|
||||
stepStatuses: {
|
||||
1: 'pending',
|
||||
@@ -86,6 +113,7 @@ const DEFAULT_STATE = {
|
||||
10: 'pending',
|
||||
},
|
||||
};
|
||||
DEFAULT_STATE.nodeStatuses = projectStepStatusesToNodeStatuses(DEFAULT_STATE.stepStatuses);
|
||||
|
||||
let stopRequested = false;
|
||||
let runCalls = 0;
|
||||
@@ -137,6 +165,10 @@ async function getState() {
|
||||
return {
|
||||
...currentState,
|
||||
stepStatuses: { ...(currentState.stepStatuses || {}) },
|
||||
nodeStatuses: {
|
||||
...projectStepStatusesToNodeStatuses(currentState.stepStatuses || {}),
|
||||
...(currentState.nodeStatuses || {}),
|
||||
},
|
||||
tabRegistry: { ...(currentState.tabRegistry || {}) },
|
||||
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
|
||||
};
|
||||
@@ -149,6 +181,9 @@ async function setState(updates) {
|
||||
stepStatuses: updates.stepStatuses
|
||||
? { ...updates.stepStatuses }
|
||||
: currentState.stepStatuses,
|
||||
nodeStatuses: updates.nodeStatuses
|
||||
? { ...updates.nodeStatuses }
|
||||
: currentState.nodeStatuses,
|
||||
tabRegistry: updates.tabRegistry
|
||||
? { ...updates.tabRegistry }
|
||||
: currentState.tabRegistry,
|
||||
@@ -163,6 +198,7 @@ async function resetState() {
|
||||
currentState = {
|
||||
...DEFAULT_STATE,
|
||||
stepStatuses: { ...DEFAULT_STATE.stepStatuses },
|
||||
nodeStatuses: { ...DEFAULT_STATE.nodeStatuses },
|
||||
vpsUrl: prev.vpsUrl,
|
||||
vpsPassword: prev.vpsPassword,
|
||||
customPassword: prev.customPassword,
|
||||
@@ -262,6 +298,18 @@ async function runAutoSequenceFromStep() {
|
||||
9: 'completed',
|
||||
10: 'completed',
|
||||
},
|
||||
nodeStatuses: projectStepStatusesToNodeStatuses({
|
||||
1: 'completed',
|
||||
2: 'completed',
|
||||
3: 'completed',
|
||||
4: 'completed',
|
||||
5: 'completed',
|
||||
6: 'completed',
|
||||
7: 'completed',
|
||||
8: 'completed',
|
||||
9: 'completed',
|
||||
10: 'completed',
|
||||
}),
|
||||
tabRegistry: {
|
||||
'signup-page': { tabId: 88, ready: true },
|
||||
},
|
||||
@@ -271,6 +319,26 @@ async function runAutoSequenceFromStep() {
|
||||
};
|
||||
}
|
||||
|
||||
async function runAutoSequenceFromNode(nodeId, context = {}) {
|
||||
return runAutoSequenceFromStep(getStepIdByNodeIdForState(nodeId) || 1, context);
|
||||
}
|
||||
|
||||
function getFirstUnfinishedNodeId() {
|
||||
return 'open-chatgpt';
|
||||
}
|
||||
|
||||
function getRunningNodeIds() {
|
||||
return [];
|
||||
}
|
||||
|
||||
function hasSavedNodeProgress() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function waitForRunningNodesToFinish() {
|
||||
return getState();
|
||||
}
|
||||
|
||||
${helperBundle}
|
||||
${autoRunModuleSource}
|
||||
|
||||
@@ -303,24 +371,24 @@ const controller = self.MultiPageBackgroundAutoRunController.createAutoRunContro
|
||||
createAutoRunSessionId,
|
||||
getAutoRunStatusPayload,
|
||||
getErrorMessage,
|
||||
getFirstUnfinishedStep,
|
||||
getFirstUnfinishedNodeId,
|
||||
getPendingAutoRunTimerPlan,
|
||||
getRunningSteps,
|
||||
getRunningNodeIds,
|
||||
getState,
|
||||
getStopRequested: () => stopRequested,
|
||||
hasSavedProgress,
|
||||
hasSavedNodeProgress,
|
||||
isRestartCurrentAttemptError,
|
||||
isStopError,
|
||||
launchAutoRunTimerPlan,
|
||||
normalizeAutoRunFallbackThreadIntervalMinutes,
|
||||
persistAutoRunTimerPlan,
|
||||
resetState,
|
||||
runAutoSequenceFromStep,
|
||||
runAutoSequenceFromNode,
|
||||
runtime,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
throwIfAutoRunSessionStopped,
|
||||
waitForRunningStepsToFinish,
|
||||
waitForRunningNodesToFinish,
|
||||
throwIfStopped,
|
||||
chrome,
|
||||
});
|
||||
|
||||
@@ -5,6 +5,90 @@ const fs = require('node:fs');
|
||||
const source = fs.readFileSync('background/auto-run-controller.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
|
||||
const rawCreateAutoRunController = api.createAutoRunController.bind(api);
|
||||
|
||||
const TEST_STEP_NODE_IDS = Object.freeze({
|
||||
1: 'open-chatgpt',
|
||||
2: 'submit-signup-email',
|
||||
3: 'fill-password',
|
||||
4: 'fetch-signup-code',
|
||||
5: 'fill-profile',
|
||||
6: 'wait-registration-success',
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
9: 'confirm-oauth',
|
||||
10: 'platform-verify',
|
||||
});
|
||||
|
||||
const TEST_NODE_STEP_IDS = Object.fromEntries(
|
||||
Object.entries(TEST_STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)])
|
||||
);
|
||||
|
||||
function getTestNodeIdByStep(step) {
|
||||
return TEST_STEP_NODE_IDS[Number(step)] || '';
|
||||
}
|
||||
|
||||
function getTestStepIdByNodeId(nodeId) {
|
||||
return TEST_NODE_STEP_IDS[String(nodeId || '').trim()] || null;
|
||||
}
|
||||
|
||||
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
|
||||
const nodeStatuses = {};
|
||||
for (const [step, status] of Object.entries(stepStatuses || {})) {
|
||||
const nodeId = getTestNodeIdByStep(step);
|
||||
if (nodeId) {
|
||||
nodeStatuses[nodeId] = status;
|
||||
}
|
||||
}
|
||||
return nodeStatuses;
|
||||
}
|
||||
|
||||
api.createAutoRunController = (deps = {}) => {
|
||||
const normalized = { ...deps };
|
||||
if (typeof normalized.getState === 'function') {
|
||||
const getState = normalized.getState;
|
||||
normalized.getState = async () => {
|
||||
const state = await getState();
|
||||
return {
|
||||
...state,
|
||||
nodeStatuses: {
|
||||
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}),
|
||||
...(state?.nodeStatuses || {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
if (typeof normalized.runAutoSequenceFromNode !== 'function' && typeof normalized.runAutoSequenceFromStep === 'function') {
|
||||
const runAutoSequenceFromStep = normalized.runAutoSequenceFromStep;
|
||||
normalized.runAutoSequenceFromNode = (nodeId, context = {}) => (
|
||||
runAutoSequenceFromStep(getTestStepIdByNodeId(nodeId) || 1, context)
|
||||
);
|
||||
}
|
||||
if (typeof normalized.getFirstUnfinishedNodeId !== 'function' && typeof normalized.getFirstUnfinishedStep === 'function') {
|
||||
const getFirstUnfinishedStep = normalized.getFirstUnfinishedStep;
|
||||
normalized.getFirstUnfinishedNodeId = (statuses = {}, state = {}) => (
|
||||
getTestNodeIdByStep(getFirstUnfinishedStep(statuses, state))
|
||||
);
|
||||
}
|
||||
if (typeof normalized.getRunningNodeIds !== 'function' && typeof normalized.getRunningSteps === 'function') {
|
||||
const getRunningSteps = normalized.getRunningSteps;
|
||||
normalized.getRunningNodeIds = (statuses = {}, state = {}) => (
|
||||
getRunningSteps(statuses, state).map(getTestNodeIdByStep).filter(Boolean)
|
||||
);
|
||||
}
|
||||
if (typeof normalized.hasSavedNodeProgress !== 'function' && typeof normalized.hasSavedProgress === 'function') {
|
||||
normalized.hasSavedNodeProgress = normalized.hasSavedProgress;
|
||||
}
|
||||
if (typeof normalized.waitForRunningNodesToFinish !== 'function' && typeof normalized.waitForRunningStepsToFinish === 'function') {
|
||||
normalized.waitForRunningNodesToFinish = normalized.waitForRunningStepsToFinish;
|
||||
}
|
||||
delete normalized.runAutoSequenceFromStep;
|
||||
delete normalized.getFirstUnfinishedStep;
|
||||
delete normalized.getRunningSteps;
|
||||
delete normalized.hasSavedProgress;
|
||||
delete normalized.waitForRunningStepsToFinish;
|
||||
return rawCreateAutoRunController(normalized);
|
||||
};
|
||||
|
||||
test('auto-run controller verifies hotmail mailbox before each fresh attempt starts', async () => {
|
||||
const events = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
@@ -52,6 +52,97 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
const NODE_COMPAT_HELPERS = `
|
||||
const STEP_NODE_IDS = {
|
||||
1: 'open-chatgpt',
|
||||
2: 'submit-signup-email',
|
||||
3: 'fill-password',
|
||||
4: 'fetch-signup-code',
|
||||
5: 'fill-profile',
|
||||
6: 'wait-registration-success',
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
9: 'confirm-oauth',
|
||||
10: 'platform-verify',
|
||||
};
|
||||
const NODE_STEP_IDS = Object.fromEntries(Object.entries(STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)]));
|
||||
function getNodeIdByStepForState(step) {
|
||||
return STEP_NODE_IDS[Number(step)] || '';
|
||||
}
|
||||
function getStepIdByNodeIdForState(nodeId) {
|
||||
return NODE_STEP_IDS[String(nodeId || '').trim()] || null;
|
||||
}
|
||||
function getNodeIdsForState() {
|
||||
return Object.keys(STEP_NODE_IDS)
|
||||
.map(Number)
|
||||
.sort((left, right) => left - right)
|
||||
.map((step) => STEP_NODE_IDS[step])
|
||||
.filter(Boolean);
|
||||
}
|
||||
function getNodeDefinitionForState(nodeId) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
return normalizedNodeId ? { nodeId: normalizedNodeId, executeKey: normalizedNodeId } : null;
|
||||
}
|
||||
function getNodeTitleForState(nodeId) {
|
||||
return String(nodeId || '').trim();
|
||||
}
|
||||
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
|
||||
const nodeStatuses = {};
|
||||
for (const [step, status] of Object.entries(stepStatuses || {})) {
|
||||
const nodeId = getNodeIdByStepForState(step);
|
||||
if (nodeId) nodeStatuses[nodeId] = status;
|
||||
}
|
||||
return nodeStatuses;
|
||||
}
|
||||
function projectNodeStatusesToStepStatuses(nodeStatuses = {}) {
|
||||
const stepStatuses = {};
|
||||
for (const [nodeId, status] of Object.entries(nodeStatuses || {})) {
|
||||
const step = getStepIdByNodeIdForState(nodeId);
|
||||
if (step) stepStatuses[step] = status;
|
||||
}
|
||||
return stepStatuses;
|
||||
}
|
||||
const rawGetStateForNodeCompat = getState;
|
||||
getState = async function getStateWithNodeStatuses() {
|
||||
const state = await rawGetStateForNodeCompat();
|
||||
return {
|
||||
...state,
|
||||
nodeStatuses: {
|
||||
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}),
|
||||
...(state?.nodeStatuses || {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
const rawSetStateForNodeCompat = setState;
|
||||
setState = async function setStateWithNodeStatuses(updates = {}) {
|
||||
const stepStatusUpdates = updates?.nodeStatuses
|
||||
? projectNodeStatusesToStepStatuses(updates.nodeStatuses)
|
||||
: {};
|
||||
return rawSetStateForNodeCompat({
|
||||
...updates,
|
||||
...(Object.keys(stepStatusUpdates).length ? {
|
||||
stepStatuses: {
|
||||
...stepStatusUpdates,
|
||||
...(updates.stepStatuses || {}),
|
||||
},
|
||||
} : {}),
|
||||
});
|
||||
};
|
||||
async function executeNodeAndWait(nodeId, delayAfter) {
|
||||
const directStep = Number(nodeId);
|
||||
if (Number.isInteger(directStep) && directStep > 0) {
|
||||
return executeStepAndWait(directStep, delayAfter);
|
||||
}
|
||||
return executeStepAndWait(getStepIdByNodeIdForState(nodeId), delayAfter);
|
||||
}
|
||||
function getAutoRunNodeDelayMs() {
|
||||
return 0;
|
||||
}
|
||||
async function runAutoSequenceFromStep(step, context = {}) {
|
||||
return runAutoSequenceFromNode(getNodeIdByStepForState(step), context);
|
||||
}
|
||||
`;
|
||||
|
||||
const bundle = [
|
||||
'const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 300000;',
|
||||
'const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = 5000;',
|
||||
@@ -65,13 +156,16 @@ const bundle = [
|
||||
extractFunction('isPlusCheckoutRestartStep'),
|
||||
extractFunction('isPlusCheckoutRestartRequiredFailure'),
|
||||
extractFunction('getLatestLogTimestamp'),
|
||||
extractFunction('buildAutoRunStepIdleRestartError'),
|
||||
extractFunction('buildAutoRunNodeIdleRestartError'),
|
||||
extractFunction('isAutoRunStepIdleRestartError'),
|
||||
extractFunction('startAutoRunStepIdleLogWatchdog'),
|
||||
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
|
||||
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
|
||||
extractFunction('startAutoRunNodeIdleLogWatchdog'),
|
||||
extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
|
||||
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
|
||||
extractFunction('getPostStep6AutoRestartDecision'),
|
||||
extractFunction('runAutoSequenceFromStep'),
|
||||
NODE_COMPAT_HELPERS,
|
||||
extractFunction('getAutoRunWorkflowNodeIds'),
|
||||
extractFunction('runAutoSequenceFromNode'),
|
||||
extractFunction('runAutoSequenceFromNodeGraph'),
|
||||
].join('\n');
|
||||
|
||||
test('auto-run stops step4 restart path when mail2925 ends the current thread', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
@@ -52,6 +52,97 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
const NODE_COMPAT_HELPERS = `
|
||||
const STEP_NODE_IDS = {
|
||||
1: 'open-chatgpt',
|
||||
2: 'submit-signup-email',
|
||||
3: 'fill-password',
|
||||
4: 'fetch-signup-code',
|
||||
5: 'fill-profile',
|
||||
6: 'wait-registration-success',
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
9: 'confirm-oauth',
|
||||
10: 'platform-verify',
|
||||
};
|
||||
const NODE_STEP_IDS = Object.fromEntries(Object.entries(STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)]));
|
||||
function getNodeIdByStepForState(step) {
|
||||
return STEP_NODE_IDS[Number(step)] || '';
|
||||
}
|
||||
function getStepIdByNodeIdForState(nodeId) {
|
||||
return NODE_STEP_IDS[String(nodeId || '').trim()] || null;
|
||||
}
|
||||
function getNodeIdsForState() {
|
||||
return Object.keys(STEP_NODE_IDS)
|
||||
.map(Number)
|
||||
.sort((left, right) => left - right)
|
||||
.map((step) => STEP_NODE_IDS[step])
|
||||
.filter(Boolean);
|
||||
}
|
||||
function getNodeDefinitionForState(nodeId) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
return normalizedNodeId ? { nodeId: normalizedNodeId, executeKey: normalizedNodeId } : null;
|
||||
}
|
||||
function getNodeTitleForState(nodeId) {
|
||||
return String(nodeId || '').trim();
|
||||
}
|
||||
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
|
||||
const nodeStatuses = {};
|
||||
for (const [step, status] of Object.entries(stepStatuses || {})) {
|
||||
const nodeId = getNodeIdByStepForState(step);
|
||||
if (nodeId) nodeStatuses[nodeId] = status;
|
||||
}
|
||||
return nodeStatuses;
|
||||
}
|
||||
function projectNodeStatusesToStepStatuses(nodeStatuses = {}) {
|
||||
const stepStatuses = {};
|
||||
for (const [nodeId, status] of Object.entries(nodeStatuses || {})) {
|
||||
const step = getStepIdByNodeIdForState(nodeId);
|
||||
if (step) stepStatuses[step] = status;
|
||||
}
|
||||
return stepStatuses;
|
||||
}
|
||||
const rawGetStateForNodeCompat = getState;
|
||||
getState = async function getStateWithNodeStatuses() {
|
||||
const state = await rawGetStateForNodeCompat();
|
||||
return {
|
||||
...state,
|
||||
nodeStatuses: {
|
||||
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}),
|
||||
...(state?.nodeStatuses || {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
const rawSetStateForNodeCompat = setState;
|
||||
setState = async function setStateWithNodeStatuses(updates = {}) {
|
||||
const stepStatusUpdates = updates?.nodeStatuses
|
||||
? projectNodeStatusesToStepStatuses(updates.nodeStatuses)
|
||||
: {};
|
||||
return rawSetStateForNodeCompat({
|
||||
...updates,
|
||||
...(Object.keys(stepStatusUpdates).length ? {
|
||||
stepStatuses: {
|
||||
...stepStatusUpdates,
|
||||
...(updates.stepStatuses || {}),
|
||||
},
|
||||
} : {}),
|
||||
});
|
||||
};
|
||||
async function executeNodeAndWait(nodeId, delayAfter) {
|
||||
const directStep = Number(nodeId);
|
||||
if (Number.isInteger(directStep) && directStep > 0) {
|
||||
return executeStepAndWait(directStep, delayAfter);
|
||||
}
|
||||
return executeStepAndWait(getStepIdByNodeIdForState(nodeId), delayAfter);
|
||||
}
|
||||
function getAutoRunNodeDelayMs() {
|
||||
return 0;
|
||||
}
|
||||
async function runAutoSequenceFromStep(step, context = {}) {
|
||||
return runAutoSequenceFromNode(getNodeIdByStepForState(step), context);
|
||||
}
|
||||
`;
|
||||
|
||||
const bundle = [
|
||||
'const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 300000;',
|
||||
'const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = 5000;',
|
||||
@@ -62,19 +153,22 @@ const bundle = [
|
||||
extractFunction('isMail2925ThreadTerminatedError'),
|
||||
extractFunction('isSignupPhonePasswordMismatchFailure'),
|
||||
extractFunction('getSignupPhonePasswordMismatchRestartPayload'),
|
||||
extractFunction('restartSignupPhonePasswordMismatchAttemptFromStep'),
|
||||
extractFunction('restartSignupPhonePasswordMismatchAttemptFromNode'),
|
||||
extractFunction('isSignupUserAlreadyExistsFailure'),
|
||||
extractFunction('isPlusCheckoutNonFreeTrialFailure'),
|
||||
extractFunction('isPlusCheckoutRestartStep'),
|
||||
extractFunction('isPlusCheckoutRestartRequiredFailure'),
|
||||
extractFunction('getLatestLogTimestamp'),
|
||||
extractFunction('buildAutoRunStepIdleRestartError'),
|
||||
extractFunction('buildAutoRunNodeIdleRestartError'),
|
||||
extractFunction('isAutoRunStepIdleRestartError'),
|
||||
extractFunction('startAutoRunStepIdleLogWatchdog'),
|
||||
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
|
||||
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
|
||||
extractFunction('startAutoRunNodeIdleLogWatchdog'),
|
||||
extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
|
||||
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
|
||||
extractFunction('getPostStep6AutoRestartDecision'),
|
||||
extractFunction('runAutoSequenceFromStep'),
|
||||
NODE_COMPAT_HELPERS,
|
||||
extractFunction('getAutoRunWorkflowNodeIds'),
|
||||
extractFunction('runAutoSequenceFromNode'),
|
||||
extractFunction('runAutoSequenceFromNodeGraph'),
|
||||
].join('\n');
|
||||
|
||||
test('auto-run restarts from step 1 with the same email after step 4 failure', async () => {
|
||||
@@ -217,7 +311,7 @@ return {
|
||||
{
|
||||
step: 1,
|
||||
options: {
|
||||
logLabel: '步骤 4 报错后准备回到步骤 1 沿用当前邮箱重试(第 1 次重开)',
|
||||
logLabel: '节点 fetch-signup-code 报错后准备回到 open-chatgpt 沿用当前邮箱重试(第 1 次重开)',
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -225,7 +319,7 @@ return {
|
||||
assert.deepStrictEqual(events.steps, [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
assert.equal(currentState.email, 'keep@example.com');
|
||||
assert.equal(currentState.password, 'Secret123!');
|
||||
assert.equal(events.logs.some(({ message }) => /沿用当前邮箱回到步骤 1 重新开始/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /沿用当前邮箱回到节点 open-chatgpt 重新开始/.test(message)), true);
|
||||
});
|
||||
|
||||
test('auto-run does not restart step 4 current attempt when user_already_exists is detected', async () => {
|
||||
@@ -348,7 +442,7 @@ return {
|
||||
assert.match(result.error, /SIGNUP_USER_ALREADY_EXISTS::/);
|
||||
assert.deepStrictEqual(result.events.invalidations, []);
|
||||
assert.deepStrictEqual(result.events.steps, [1, 2, 3, 4]);
|
||||
assert.equal(result.events.logs.some(({ message }) => /沿用当前邮箱回到步骤 1 重新开始/.test(message)), false);
|
||||
assert.equal(result.events.logs.some(({ message }) => /沿用当前邮箱回到节点 open-chatgpt 重新开始/.test(message)), false);
|
||||
});
|
||||
|
||||
test('auto-run skips steps 4/5 when step 2 has already marked registration chain as skipped', async () => {
|
||||
@@ -470,8 +564,8 @@ return {
|
||||
const { events } = await api.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, [1, 2, 6, 7, 8, 9, 10]);
|
||||
assert.equal(events.logs.some(({ message }) => /步骤 4 当前状态为 skipped/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /步骤 5 当前状态为 skipped/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /节点 fetch-signup-code 当前状态为 skipped/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /节点 fill-profile 当前状态为 skipped/.test(message)), true);
|
||||
});
|
||||
|
||||
test('auto-run clears fetched signup phone state before restarting when step 4 detects phone/password mismatch', async () => {
|
||||
@@ -622,7 +716,7 @@ return {
|
||||
{
|
||||
step: 1,
|
||||
options: {
|
||||
logLabel: '步骤 4 检测到手机号/密码不匹配后准备回到步骤 1 重新获取手机号重试(第 1 次重开)',
|
||||
logLabel: '节点 fetch-signup-code 检测到手机号/密码不匹配后准备回到 open-chatgpt 重新获取手机号重试(第 1 次重开)',
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -632,7 +726,7 @@ return {
|
||||
assert.equal(currentState.accountIdentifierType, null);
|
||||
assert.equal(currentState.accountIdentifier, '');
|
||||
assert.equal(currentState.password, 'Secret123!');
|
||||
assert.equal(events.logs.some(({ message }) => /丢弃当前注册手机号并回到步骤 1 重新开始/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /丢弃当前注册手机号并回到节点 open-chatgpt 重新开始/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /已清空本轮注册手机号与接码订单/.test(message)), true);
|
||||
});
|
||||
|
||||
@@ -786,7 +880,7 @@ return {
|
||||
{
|
||||
step: 1,
|
||||
options: {
|
||||
logLabel: '步骤 4 检测到当前注册手机号无法接收短信后准备回到步骤 1 重新获取手机号重试(第 1 次重开)',
|
||||
logLabel: '节点 fetch-signup-code 检测到当前注册手机号无法接收短信后准备回到 open-chatgpt 重新获取手机号重试(第 1 次重开)',
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -796,7 +890,7 @@ return {
|
||||
assert.equal(currentState.accountIdentifierType, null);
|
||||
assert.equal(currentState.accountIdentifier, '');
|
||||
assert.equal(currentState.password, 'Secret123!');
|
||||
assert.equal(events.logs.some(({ message }) => /丢弃当前注册手机号并回到步骤 1 重新开始/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /丢弃当前注册手机号并回到节点 open-chatgpt 重新开始/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /已清空本轮注册手机号与接码订单/.test(message)), true);
|
||||
});
|
||||
|
||||
@@ -943,7 +1037,7 @@ return {
|
||||
{
|
||||
step: 1,
|
||||
options: {
|
||||
logLabel: '步骤 3 检测到手机号/密码不匹配后准备回到步骤 1 重新获取手机号重试(第 1 次重开)',
|
||||
logLabel: '节点 fill-password 检测到手机号/密码不匹配后准备回到 open-chatgpt 重新获取手机号重试(第 1 次重开)',
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -953,6 +1047,6 @@ return {
|
||||
assert.equal(currentState.accountIdentifierType, null);
|
||||
assert.equal(currentState.accountIdentifier, '');
|
||||
assert.equal(currentState.password, 'Secret123!');
|
||||
assert.equal(events.logs.some(({ message }) => /步骤 3:检测到手机号\/密码不匹配/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /步骤 3:已清空本轮注册手机号与接码订单/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /节点 fill-password:检测到手机号\/密码不匹配/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /节点 fill-password:已清空本轮注册手机号与接码订单/.test(message)), true);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
@@ -52,6 +52,99 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
const NODE_COMPAT_HELPERS = `
|
||||
const FALLBACK_STEP_NODE_IDS = {
|
||||
1: 'open-chatgpt',
|
||||
2: 'submit-signup-email',
|
||||
3: 'fill-password',
|
||||
4: 'fetch-signup-code',
|
||||
5: 'fill-profile',
|
||||
6: 'plus-checkout-create',
|
||||
7: 'plus-checkout-billing',
|
||||
8: 'paypal-approve',
|
||||
9: 'plus-checkout-return',
|
||||
10: 'oauth-login',
|
||||
11: 'fetch-login-code',
|
||||
12: 'confirm-oauth',
|
||||
13: 'platform-verify',
|
||||
};
|
||||
function getNodeIdByStepForState(step, state = {}) {
|
||||
if (typeof getStepDefinitionForState === 'function') {
|
||||
const key = String(getStepDefinitionForState(step, state)?.key || '').trim();
|
||||
if (key) return key;
|
||||
}
|
||||
return FALLBACK_STEP_NODE_IDS[Number(step)] || '';
|
||||
}
|
||||
function getStepIdByNodeIdForState(nodeId, state = {}) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
const ids = typeof getStepIdsForState === 'function'
|
||||
? getStepIdsForState(state)
|
||||
: Object.keys(FALLBACK_STEP_NODE_IDS).map(Number);
|
||||
for (const step of ids) {
|
||||
if (getNodeIdByStepForState(step, state) === normalizedNodeId) {
|
||||
return Number(step);
|
||||
}
|
||||
}
|
||||
for (const [step, fallbackNodeId] of Object.entries(FALLBACK_STEP_NODE_IDS)) {
|
||||
if (fallbackNodeId === normalizedNodeId) {
|
||||
return Number(step);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function getNodeIdsForState(state = {}) {
|
||||
const ids = typeof getStepIdsForState === 'function'
|
||||
? getStepIdsForState(state)
|
||||
: Object.keys(FALLBACK_STEP_NODE_IDS).map(Number);
|
||||
return ids
|
||||
.map((step) => getNodeIdByStepForState(step, state))
|
||||
.filter(Boolean);
|
||||
}
|
||||
function getNodeDefinitionForState(nodeId, state = {}) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
const step = getStepIdByNodeIdForState(normalizedNodeId, state);
|
||||
const executeKey = typeof getStepExecutionKeyForState === 'function'
|
||||
? getStepExecutionKeyForState(step, state)
|
||||
: normalizedNodeId;
|
||||
return normalizedNodeId ? { nodeId: normalizedNodeId, legacyStepId: step, executeKey } : null;
|
||||
}
|
||||
function getNodeTitleForState(nodeId) {
|
||||
return String(nodeId || '').trim();
|
||||
}
|
||||
function projectStepStatusesToNodeStatuses(stepStatuses = {}, state = {}) {
|
||||
const nodeStatuses = {};
|
||||
for (const [step, status] of Object.entries(stepStatuses || {})) {
|
||||
const nodeId = getNodeIdByStepForState(step, state);
|
||||
if (nodeId) nodeStatuses[nodeId] = status;
|
||||
}
|
||||
return nodeStatuses;
|
||||
}
|
||||
const rawGetStateForNodeCompat = getState;
|
||||
getState = async function getStateWithNodeStatuses() {
|
||||
const state = await rawGetStateForNodeCompat();
|
||||
return {
|
||||
...state,
|
||||
nodeStatuses: {
|
||||
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}, state),
|
||||
...(state?.nodeStatuses || {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
async function executeNodeAndWait(nodeId, delayAfter) {
|
||||
const directStep = Number(nodeId);
|
||||
if (Number.isInteger(directStep) && directStep > 0) {
|
||||
return executeStepAndWait(directStep, delayAfter);
|
||||
}
|
||||
return executeStepAndWait(getStepIdByNodeIdForState(nodeId, await getState()), delayAfter);
|
||||
}
|
||||
function getAutoRunNodeDelayMs() {
|
||||
return 0;
|
||||
}
|
||||
async function runAutoSequenceFromStep(step, context = {}) {
|
||||
return runAutoSequenceFromNode(getNodeIdByStepForState(step, await getState()), context);
|
||||
}
|
||||
`;
|
||||
|
||||
const bundle = [
|
||||
extractFunction('isAddPhoneAuthFailure'),
|
||||
extractFunction('isAddPhoneAuthUrl'),
|
||||
@@ -61,13 +154,16 @@ const bundle = [
|
||||
extractFunction('isPlusCheckoutRestartStep'),
|
||||
extractFunction('isPlusCheckoutRestartRequiredFailure'),
|
||||
extractFunction('getLatestLogTimestamp'),
|
||||
extractFunction('buildAutoRunStepIdleRestartError'),
|
||||
extractFunction('buildAutoRunNodeIdleRestartError'),
|
||||
extractFunction('isAutoRunStepIdleRestartError'),
|
||||
extractFunction('startAutoRunStepIdleLogWatchdog'),
|
||||
extractFunction('runAutoStepActionWithIdleLogWatchdog'),
|
||||
extractFunction('executeStepAndWaitWithAutoRunIdleLogWatchdog'),
|
||||
extractFunction('startAutoRunNodeIdleLogWatchdog'),
|
||||
extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
|
||||
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
|
||||
extractFunction('getPostStep6AutoRestartDecision'),
|
||||
extractFunction('runAutoSequenceFromStep'),
|
||||
NODE_COMPAT_HELPERS,
|
||||
extractFunction('getAutoRunWorkflowNodeIds'),
|
||||
extractFunction('runAutoSequenceFromNode'),
|
||||
extractFunction('runAutoSequenceFromNodeGraph'),
|
||||
].join('\n');
|
||||
|
||||
const defaultStepDefinitions = {
|
||||
@@ -311,7 +407,7 @@ test('auto-run keeps restarting from step 7 after post-login failures without a
|
||||
7, 8, 9, 10,
|
||||
]
|
||||
);
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
|
||||
assert.ok(events.logs.some(({ message }) => /回到节点 auth-login 重新开始授权流程/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts the current step after five minutes without new logs', async () => {
|
||||
@@ -330,7 +426,7 @@ test('auto-run restarts the current step after five minutes without new logs', a
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
|
||||
assert.equal(events.cancellations.length, 1);
|
||||
assert.equal(events.stopBroadcasts, 1);
|
||||
assert.ok(events.logs.some(({ message }) => /5 分钟没有新日志,准备重新开始当前步骤/.test(message)));
|
||||
assert.ok(events.logs.some(({ message }) => /5 分钟没有新日志,准备重新开始当前节点/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run applies the idle-log restart watchdog to early steps too', async () => {
|
||||
@@ -364,7 +460,7 @@ test('auto-run stops current-step idle restarts after the retry cap', async () =
|
||||
const result = await harness.runAndCaptureError();
|
||||
|
||||
assert.ok(result?.error);
|
||||
assert.match(result.error.message, /AUTO_RUN_STEP_IDLE_RESTART::步骤 10/);
|
||||
assert.match(result.error.message, /AUTO_RUN_STEP_IDLE_RESTART::节点 platform-verify/);
|
||||
assert.deepStrictEqual(result.events.steps, [10, 10, 10, 10]);
|
||||
assert.deepStrictEqual(result.events.invalidations.map((entry) => entry.step), [9, 9, 9]);
|
||||
assert.ok(result.events.logs.some(({ message }) => /已连续 3 次因 5 分钟无新日志而重开/.test(message)));
|
||||
@@ -474,10 +570,10 @@ test('auto-run restarts from confirm-oauth step after transient step10 token_exc
|
||||
assert.deepStrictEqual(events.invalidations[0], {
|
||||
step: 8,
|
||||
options: {
|
||||
logLabel: '步骤 10 报错后准备回到步骤 9 重试(第 1 次重开)',
|
||||
logLabel: '节点 platform-verify 报错后准备回到 confirm-oauth 重试(第 1 次重开)',
|
||||
},
|
||||
});
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 9 重新开始授权流程/.test(message)));
|
||||
assert.ok(events.logs.some(({ message }) => /回到节点 confirm-oauth 重新开始授权流程/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts Plus/GPC oauth-login aggregate entry-open failure from step 10', async () => {
|
||||
@@ -508,9 +604,9 @@ test('auto-run restarts Plus/GPC oauth-login aggregate entry-open failure from s
|
||||
const events = await harness.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, [10, 10, 11, 12, 13]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 10 重新开始授权流程/.test(message)));
|
||||
assert.ok(!events.logs.some(({ message }) => /停止自动回到步骤 10 重开/.test(message)));
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [7]);
|
||||
assert.ok(events.logs.some(({ message }) => /回到节点 oauth-login 重新开始授权流程/.test(message)));
|
||||
assert.ok(!events.logs.some(({ message }) => /停止自动回到节点 oauth-login 重开/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts Plus/GPC fetch-code and confirm-oauth failures from oauth-login step 10', async () => {
|
||||
@@ -554,8 +650,8 @@ test('auto-run restarts Plus/GPC fetch-code and confirm-oauth failures from oaut
|
||||
const events = await harness.run();
|
||||
|
||||
assert.deepStrictEqual(events.steps, scenario.expectedSteps);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [9]);
|
||||
assert.ok(events.logs.some(({ message }) => new RegExp(`步骤 ${scenario.failureStep}:.*回到步骤 10 重新开始授权流程`).test(message)));
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [7]);
|
||||
assert.ok(events.logs.some(({ message }) => /回到节点 oauth-login 重新开始授权流程/.test(message)));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -589,7 +685,7 @@ test('auto-run restarts Plus/GPC transient platform verify exchange failures fro
|
||||
|
||||
assert.deepStrictEqual(events.steps, [10, 11, 12, 13, 12, 13]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [11]);
|
||||
assert.ok(events.logs.some(({ message }) => /步骤 13:.*回到步骤 12 重新开始授权流程/.test(message)));
|
||||
assert.ok(events.logs.some(({ message }) => /节点 platform-verify.*回到节点 confirm-oauth 重新开始授权流程/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts Plus checkout from step 6 when checkout creation fails', async () => {
|
||||
@@ -621,7 +717,7 @@ test('auto-run restarts Plus checkout from step 6 when checkout creation fails',
|
||||
|
||||
assert.deepStrictEqual(events.steps, [6, 6, 7, 8, 9, 10, 11, 12, 13]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 Plus Checkout/.test(message)));
|
||||
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 Plus Checkout/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts Plus checkout from step 6 when billing fails for non-free-trial reasons', async () => {
|
||||
@@ -653,7 +749,7 @@ test('auto-run restarts Plus checkout from step 6 when billing fails for non-fre
|
||||
|
||||
assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 8, 9, 10, 11, 12, 13]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 Plus Checkout/.test(message)));
|
||||
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 Plus Checkout/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls', async () => {
|
||||
@@ -689,7 +785,7 @@ test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls
|
||||
events.invalidations.map((entry) => entry.step),
|
||||
[5, 5]
|
||||
);
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
|
||||
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run treats GPC account binding as recoverable step 6 restart', async () => {
|
||||
@@ -748,7 +844,7 @@ test('auto-run restarts GPC checkout from step 6 when accessToken cannot be read
|
||||
|
||||
assert.deepStrictEqual(events.steps, [6, 6, 7, 10, 11, 12, 13]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
|
||||
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts GPC checkout from step 6 when task status has no progress', async () => {
|
||||
@@ -778,7 +874,7 @@ test('auto-run restarts GPC checkout from step 6 when task status has no progres
|
||||
|
||||
assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 10, 11, 12, 13]);
|
||||
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
|
||||
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
|
||||
assert.ok(events.logs.some(({ message }) => /回到节点 plus-checkout-create 重新创建 GPC 任务/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run keeps rebuilding GPC checkout beyond three failures', async () => {
|
||||
|
||||
@@ -47,9 +47,13 @@ const bundle = [
|
||||
'const PERSISTED_SETTING_DEFAULTS = { autoStepDelaySeconds: null };',
|
||||
"const AUTO_RUN_PRE_EXECUTION_DELAYS_BY_STEP_KEY = new Map([['plus-checkout-create', 20000]]);",
|
||||
'function getStepDefinitionForState(step, state = {}) { return state.definitions?.[step] || null; }',
|
||||
'function getNodeIdByStepForState(step, state = {}) { return String(getStepDefinitionForState(step, state)?.key || step || "").trim(); }',
|
||||
'function getNodeDefinitionForState(nodeId, state = {}) { return Object.values(state.definitions || {}).find((definition) => String(definition?.key || "").trim() === String(nodeId || "").trim()) || { executeKey: String(nodeId || "").trim() }; }',
|
||||
extractFunction('normalizeAutoStepDelaySeconds'),
|
||||
extractFunction('resolveLegacyAutoStepDelaySeconds'),
|
||||
extractFunction('getStepExecutionKeyForState'),
|
||||
extractFunction('getNodeExecutionKeyForState'),
|
||||
extractFunction('getAutoRunPreExecutionDelayMsForNode'),
|
||||
extractFunction('getAutoRunPreExecutionDelayMs'),
|
||||
].join('\n');
|
||||
|
||||
|
||||
@@ -46,7 +46,13 @@ test('account run history helper upgrades old records, keeps stopped items and s
|
||||
},
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getNodeTitleForState: (nodeId) => ({
|
||||
'fetch-login-code': '获取登录验证码',
|
||||
'oauth-login': '刷新 OAuth 并登录',
|
||||
})[nodeId] || nodeId,
|
||||
getState: async () => ({
|
||||
flowId: 'openai',
|
||||
runId: 'run-2',
|
||||
email: ' latest@example.com ',
|
||||
password: ' secret ',
|
||||
autoRunning: true,
|
||||
@@ -61,6 +67,8 @@ test('account run history helper upgrades old records, keeps stopped items and s
|
||||
|
||||
const record = helpers.buildAccountRunHistoryRecord(
|
||||
{
|
||||
flowId: 'openai',
|
||||
runId: 'run-2',
|
||||
email: ' latest@example.com ',
|
||||
password: ' secret ',
|
||||
autoRunning: true,
|
||||
@@ -68,11 +76,13 @@ test('account run history helper upgrades old records, keeps stopped items and s
|
||||
autoRunTotalRuns: 10,
|
||||
autoRunAttemptRun: 3,
|
||||
},
|
||||
'step8_failed',
|
||||
'node:fetch-login-code:failed',
|
||||
'步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。'
|
||||
);
|
||||
assert.deepStrictEqual(record, {
|
||||
recordId: 'latest@example.com',
|
||||
flowId: 'openai',
|
||||
runId: 'run-2',
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: 'latest@example.com',
|
||||
email: 'latest@example.com',
|
||||
@@ -83,7 +93,8 @@ test('account run history helper upgrades old records, keeps stopped items and s
|
||||
retryCount: 2,
|
||||
failureLabel: '出现手机号验证',
|
||||
failureDetail: '步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。',
|
||||
failedStep: 8,
|
||||
failedNodeId: 'fetch-login-code',
|
||||
failedStep: null,
|
||||
source: 'auto',
|
||||
autoRunContext: {
|
||||
currentRun: 2,
|
||||
@@ -94,9 +105,12 @@ test('account run history helper upgrades old records, keeps stopped items and s
|
||||
contributionMode: false,
|
||||
});
|
||||
|
||||
const appended = await helpers.appendAccountRunRecord('step8_failed', null, '步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
|
||||
const appended = await helpers.appendAccountRunRecord('node:fetch-login-code:failed', null, '步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。');
|
||||
assert.equal(appended.email, 'latest@example.com');
|
||||
assert.equal(appended.finalStatus, 'failed');
|
||||
assert.equal(appended.flowId, 'openai');
|
||||
assert.equal(appended.runId, 'run-2');
|
||||
assert.equal(appended.failedNodeId, 'fetch-login-code');
|
||||
assert.equal(appended.failureLabel, '出现手机号验证');
|
||||
assert.equal(storedHistory.length, 3, '旧的 stopped 记录应在新结构中保留');
|
||||
assert.equal(storedHistory.some((item) => item.email === 'stop@example.com' && item.finalStatus === 'stopped'), true);
|
||||
@@ -106,18 +120,21 @@ test('account run history helper upgrades old records, keeps stopped items and s
|
||||
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: false, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), true);
|
||||
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: true, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), true);
|
||||
const stoppedRecord = helpers.buildAccountRunHistoryRecord(
|
||||
{ email: 'a@b.com', password: 'x' },
|
||||
'step7_stopped',
|
||||
'步骤 7 已被用户停止'
|
||||
{ flowId: 'openai', runId: 'manual-run', email: 'a@b.com', password: 'x' },
|
||||
'node:oauth-login:stopped',
|
||||
'节点 oauth-login 已被用户停止'
|
||||
);
|
||||
assert.equal(stoppedRecord.recordId, 'a@b.com');
|
||||
assert.equal(stoppedRecord.flowId, 'openai');
|
||||
assert.equal(stoppedRecord.runId, 'manual-run');
|
||||
assert.equal(stoppedRecord.email, 'a@b.com');
|
||||
assert.equal(stoppedRecord.password, 'x');
|
||||
assert.equal(stoppedRecord.finalStatus, 'stopped');
|
||||
assert.equal(stoppedRecord.retryCount, 0);
|
||||
assert.equal(stoppedRecord.failureLabel, '步骤 7 停止');
|
||||
assert.equal(stoppedRecord.failureDetail, '步骤 7 已被用户停止');
|
||||
assert.equal(stoppedRecord.failedStep, 7);
|
||||
assert.equal(stoppedRecord.failureLabel, '节点 刷新 OAuth 并登录 停止');
|
||||
assert.equal(stoppedRecord.failureDetail, '节点 oauth-login 已被用户停止');
|
||||
assert.equal(stoppedRecord.failedNodeId, 'oauth-login');
|
||||
assert.equal(stoppedRecord.failedStep, null);
|
||||
assert.equal(stoppedRecord.source, 'manual');
|
||||
assert.equal(stoppedRecord.autoRunContext, null);
|
||||
assert.ok(stoppedRecord.finishedAt);
|
||||
@@ -149,11 +166,13 @@ test('account run history helper upgrades old records, keeps stopped items and s
|
||||
retryCount: 0,
|
||||
failureLabel: '流程已停止',
|
||||
failureDetail: '步骤 7 已被用户停止。',
|
||||
failedNodeId: 'oauth-login',
|
||||
failedStep: 7,
|
||||
source: 'manual',
|
||||
autoRunContext: null,
|
||||
});
|
||||
assert.equal(normalizedStoppedRecord.failureLabel, '步骤 7 停止');
|
||||
assert.equal(normalizedStoppedRecord.failureLabel, '节点 刷新 OAuth 并登录 停止');
|
||||
assert.equal(normalizedStoppedRecord.failedNodeId, 'oauth-login');
|
||||
assert.equal(normalizedStoppedRecord.failedStep, 7);
|
||||
});
|
||||
|
||||
@@ -177,6 +196,8 @@ test('account run history helper accepts phone-only records without forcing emai
|
||||
|
||||
assert.deepStrictEqual(record, {
|
||||
recordId: 'phone:+6612345',
|
||||
flowId: '',
|
||||
runId: '',
|
||||
accountIdentifierType: 'phone',
|
||||
accountIdentifier: '+6612345',
|
||||
email: '',
|
||||
@@ -187,6 +208,7 @@ test('account run history helper accepts phone-only records without forcing emai
|
||||
retryCount: 0,
|
||||
failureLabel: '流程完成',
|
||||
failureDetail: '',
|
||||
failedNodeId: '',
|
||||
failedStep: null,
|
||||
source: 'manual',
|
||||
autoRunContext: null,
|
||||
@@ -242,10 +264,11 @@ test('account run history does not turn prerequisite guidance into a fake step 2
|
||||
autoRunCurrentRun: 1,
|
||||
autoRunTotalRuns: 3,
|
||||
autoRunAttemptRun: 2,
|
||||
}, 'step10_failed', '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
|
||||
}, 'node:platform-verify:failed', '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
|
||||
|
||||
assert.equal(explicitFailedRecord.failedStep, 10);
|
||||
assert.equal(explicitFailedRecord.failureLabel, '步骤 10 失败');
|
||||
assert.equal(explicitFailedRecord.failedNodeId, 'platform-verify');
|
||||
assert.equal(explicitFailedRecord.failedStep, null);
|
||||
assert.equal(explicitFailedRecord.failureLabel, '节点 platform-verify 失败');
|
||||
|
||||
const migratedOldRecord = helpers.normalizeAccountRunHistoryRecord({
|
||||
email: 'old@example.com',
|
||||
@@ -314,10 +337,12 @@ test('account run history merges email and phone identities from the same run',
|
||||
activationId: 'a1',
|
||||
phoneNumber: '+44 7799 342687',
|
||||
},
|
||||
}, 'step9_failed', '步骤 9:手机号验证失败。');
|
||||
}, 'node:confirm-oauth:failed', '步骤 9:手机号验证失败。');
|
||||
assert.equal(failedRecord.accountIdentifierType, 'email');
|
||||
assert.equal(failedRecord.accountIdentifier, 'tmp@example.com');
|
||||
assert.equal(failedRecord.phoneNumber, '+44 7799 342687');
|
||||
assert.equal(failedRecord.failedNodeId, 'confirm-oauth');
|
||||
assert.equal(failedRecord.failedStep, null);
|
||||
|
||||
const successRecord = await helpers.appendAccountRunRecord('success', {
|
||||
accountIdentifierType: 'email',
|
||||
|
||||
@@ -48,6 +48,71 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
const NODE_EXECUTE_COMPAT_HELPERS = `
|
||||
const AUTH_CHAIN_NODE_IDS = new Set(['oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify']);
|
||||
const STEP_NODE_IDS = {
|
||||
1: 'open-chatgpt',
|
||||
2: 'submit-signup-email',
|
||||
3: 'fill-password',
|
||||
4: 'fetch-signup-code',
|
||||
5: 'fill-profile',
|
||||
6: 'wait-registration-success',
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
9: 'confirm-oauth',
|
||||
10: 'platform-verify',
|
||||
};
|
||||
const NODE_STEP_IDS = Object.fromEntries(Object.entries(STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)]));
|
||||
function isAuthChainNode(nodeId) {
|
||||
return AUTH_CHAIN_NODE_IDS.has(String(nodeId || '').trim());
|
||||
}
|
||||
function getNodeIdByStepForState(step) {
|
||||
const definition = typeof getStepDefinitionForState === 'function' ? getStepDefinitionForState(step) : null;
|
||||
const definitionKey = String(definition?.key || '').trim();
|
||||
return definitionKey && definitionKey !== 'test-step'
|
||||
? definitionKey
|
||||
: String(STEP_NODE_IDS[Number(step)] || '').trim();
|
||||
}
|
||||
function getStepIdByNodeIdForState(nodeId) {
|
||||
const normalizedNodeId = String(nodeId || '').trim();
|
||||
return NODE_STEP_IDS[normalizedNodeId] || null;
|
||||
}
|
||||
function getNodeDefinitionForState(nodeId, state = {}) {
|
||||
const step = getStepIdByNodeIdForState(nodeId, state);
|
||||
const stepDefinition = typeof getStepDefinitionForState === 'function'
|
||||
? getStepDefinitionForState(step, state)
|
||||
: null;
|
||||
return stepDefinition
|
||||
? { nodeId: String(nodeId || '').trim(), legacyStepId: step, executeKey: String(stepDefinition.key || nodeId || '').trim() }
|
||||
: null;
|
||||
}
|
||||
async function setNodeStatus(nodeId, status) {
|
||||
return setStepStatus(getStepIdByNodeIdForState(nodeId), status);
|
||||
}
|
||||
function doesNodeUseCompletionSignal(nodeId, state = {}) {
|
||||
return typeof doesStepUseCompletionSignal === 'function'
|
||||
? doesStepUseCompletionSignal(getStepIdByNodeIdForState(nodeId), state)
|
||||
: false;
|
||||
}
|
||||
const rawGetStepRegistryForState = getStepRegistryForState;
|
||||
getStepRegistryForState = function getNodeAdaptedStepRegistryForState(state = {}) {
|
||||
const registry = rawGetStepRegistryForState(state);
|
||||
if (!registry || typeof registry !== 'object' || typeof registry.getNodeDefinition === 'function') {
|
||||
return registry;
|
||||
}
|
||||
return {
|
||||
...registry,
|
||||
getNodeDefinition(nodeId) {
|
||||
return getNodeDefinitionForState(nodeId, state);
|
||||
},
|
||||
async executeNode(nodeId, payload = {}) {
|
||||
const step = getStepIdByNodeIdForState(nodeId);
|
||||
return registry.executeStep(step, payload);
|
||||
},
|
||||
};
|
||||
};
|
||||
`;
|
||||
|
||||
test('throwIfStopped rethrows an explicit stop error even when stopRequested has been cleared', () => {
|
||||
const api = new Function(`
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
@@ -67,7 +132,7 @@ return {
|
||||
);
|
||||
});
|
||||
|
||||
test('executeStep reuses the active top-level auth chain instead of starting a duplicate step', async () => {
|
||||
test('executeNode reuses the active top-level auth chain instead of starting a duplicate node', async () => {
|
||||
const api = new Function(`
|
||||
const LOG_PREFIX = '[test]';
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
@@ -137,14 +202,15 @@ function getStepDefinitionForState(step) {
|
||||
return { id: step, key: 'test-step' };
|
||||
}
|
||||
|
||||
${NODE_EXECUTE_COMPAT_HELPERS}
|
||||
${extractFunction('isStopError')}
|
||||
${extractFunction('throwIfStopped')}
|
||||
${extractFunction('isAuthChainStep')}
|
||||
${extractFunction('acquireTopLevelAuthChainExecution')}
|
||||
${extractFunction('executeStep')}
|
||||
${extractFunction('acquireTopLevelAuthChainExecutionForNode')}
|
||||
${extractFunction('executeNode')}
|
||||
|
||||
return {
|
||||
executeStep,
|
||||
executeNode,
|
||||
releaseStep8() {
|
||||
if (releaseStep8) {
|
||||
releaseStep8();
|
||||
@@ -156,9 +222,9 @@ return {
|
||||
};
|
||||
`)();
|
||||
|
||||
const firstRun = api.executeStep(8);
|
||||
const firstRun = api.executeNode('fetch-login-code');
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
const duplicateRun = api.executeStep(7);
|
||||
const duplicateRun = api.executeNode('oauth-login');
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
api.releaseStep8();
|
||||
|
||||
@@ -173,7 +239,7 @@ return {
|
||||
assert.ok(events.logs.some(({ message }) => /复用当前授权链/.test(message)));
|
||||
});
|
||||
|
||||
test('executeStep stops flow when browser-switch-required error is raised', async () => {
|
||||
test('executeNode stops flow when browser-switch-required error is raised', async () => {
|
||||
const api = new Function(`
|
||||
const LOG_PREFIX = '[test]';
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
@@ -235,17 +301,18 @@ function getStepDefinitionForState(step) {
|
||||
return { id: step, key: 'test-step' };
|
||||
}
|
||||
|
||||
${NODE_EXECUTE_COMPAT_HELPERS}
|
||||
${extractFunction('isStopError')}
|
||||
${extractFunction('throwIfStopped')}
|
||||
${extractFunction('isAuthChainStep')}
|
||||
${extractFunction('acquireTopLevelAuthChainExecution')}
|
||||
${extractFunction('acquireTopLevelAuthChainExecutionForNode')}
|
||||
${extractFunction('isBrowserSwitchRequiredError')}
|
||||
${extractFunction('getBrowserSwitchRequiredMessage')}
|
||||
${extractFunction('handleBrowserSwitchRequired')}
|
||||
${extractFunction('executeStep')}
|
||||
${extractFunction('executeNode')}
|
||||
|
||||
return {
|
||||
executeStep,
|
||||
executeNode,
|
||||
snapshot() {
|
||||
return events;
|
||||
},
|
||||
@@ -253,7 +320,7 @@ return {
|
||||
`)();
|
||||
|
||||
await assert.rejects(
|
||||
() => api.executeStep(10),
|
||||
() => api.executeNode('platform-verify'),
|
||||
/流程已被用户停止。/
|
||||
);
|
||||
|
||||
@@ -393,7 +460,7 @@ return {
|
||||
assert.match(events.logs[0].message, /授权后链总超时已关闭/);
|
||||
});
|
||||
|
||||
test('executeStep retries fetch-network errors for step 4 with cooldown and bounded attempts', async () => {
|
||||
test('executeNode retries fetch-network errors for fetch-signup-code with cooldown and bounded attempts', async () => {
|
||||
const api = new Function(`
|
||||
const LOG_PREFIX = '[test]';
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
@@ -456,27 +523,28 @@ function getStepDefinitionForState(step) {
|
||||
return { id: step, key: 'fetch-signup-code' };
|
||||
}
|
||||
|
||||
${NODE_EXECUTE_COMPAT_HELPERS}
|
||||
${extractFunction('isStopError')}
|
||||
${extractFunction('isRetryableContentScriptTransportError')}
|
||||
${extractFunction('isStepFetchNetworkRetryableError')}
|
||||
${extractFunction('getStepFetchNetworkRetryPolicy')}
|
||||
${extractFunction('throwIfStopped')}
|
||||
${extractFunction('isAuthChainStep')}
|
||||
${extractFunction('acquireTopLevelAuthChainExecution')}
|
||||
${extractFunction('acquireTopLevelAuthChainExecutionForNode')}
|
||||
${extractFunction('isBrowserSwitchRequiredError')}
|
||||
${extractFunction('getBrowserSwitchRequiredMessage')}
|
||||
${extractFunction('handleBrowserSwitchRequired')}
|
||||
${extractFunction('executeStep')}
|
||||
${extractFunction('executeNode')}
|
||||
|
||||
return {
|
||||
executeStep,
|
||||
executeNode,
|
||||
snapshot() {
|
||||
return events;
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
await api.executeStep(4);
|
||||
await api.executeNode('fetch-signup-code');
|
||||
|
||||
const events = api.snapshot();
|
||||
assert.deepStrictEqual(events.registryCalls, [4, 4, 4]);
|
||||
|
||||
@@ -16,30 +16,30 @@ test('auto-run controller module exposes a factory', () => {
|
||||
assert.equal(typeof api?.createAutoRunController, 'function');
|
||||
});
|
||||
|
||||
test('auto-run account record status preserves the real failed step instead of parsing guidance text', () => {
|
||||
test('auto-run account record status preserves the real failed node instead of parsing guidance text', () => {
|
||||
const source = fs.readFileSync('background/auto-run-controller.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
|
||||
const controller = api.createAutoRunController({});
|
||||
|
||||
const state = {
|
||||
currentStep: 11,
|
||||
stepStatuses: {
|
||||
2: 'completed',
|
||||
10: 'completed',
|
||||
11: 'failed',
|
||||
currentNodeId: 'fetch-login-code',
|
||||
nodeStatuses: {
|
||||
'submit-signup-email': 'completed',
|
||||
'oauth-login': 'completed',
|
||||
'fetch-login-code': 'failed',
|
||||
},
|
||||
};
|
||||
const error = new Error('缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
|
||||
|
||||
assert.equal(
|
||||
controller.resolveAutoRunAccountRecordStatus('failed', state, error),
|
||||
'step11_failed'
|
||||
'node:fetch-login-code:failed'
|
||||
);
|
||||
|
||||
error.failedStep = 13;
|
||||
error.failedNodeId = 'platform-verify';
|
||||
assert.equal(
|
||||
controller.resolveAutoRunAccountRecordStatus('failed', state, error),
|
||||
'step13_failed'
|
||||
'node:platform-verify:failed'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ test('mail rule registry exposes canonical OpenAI verification poll payloads', (
|
||||
{
|
||||
flowId: 'openai',
|
||||
ruleId: 'openai-signup-code',
|
||||
nodeId: 'fetch-signup-code',
|
||||
step: 4,
|
||||
artifactType: 'code',
|
||||
codePatterns: [
|
||||
@@ -78,6 +79,7 @@ test('mail rule registry exposes canonical OpenAI verification poll payloads', (
|
||||
{
|
||||
flowId: 'openai',
|
||||
ruleId: 'openai-login-code',
|
||||
nodeId: 'fetch-login-code',
|
||||
step: 8,
|
||||
artifactType: 'code',
|
||||
codePatterns: [
|
||||
@@ -105,6 +107,14 @@ test('mail rule registry exposes canonical OpenAI verification poll payloads', (
|
||||
intervalMs: 3000,
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
registry.buildVerificationPollPayloadForNode('fetch-signup-code', {
|
||||
activeFlowId: 'openai',
|
||||
email: 'node@example.com',
|
||||
}).nodeId,
|
||||
'fetch-signup-code'
|
||||
);
|
||||
});
|
||||
|
||||
test('mail rule registry rejects unknown active flow ids instead of silently using OpenAI rules', () => {
|
||||
|
||||
@@ -30,10 +30,10 @@ test('message router appends success record on Plus final step instead of hard-c
|
||||
deleteIcloudAlias: async () => {},
|
||||
deleteUsedIcloudAliases: async () => {},
|
||||
disableUsedLuckmailPurchases: async () => {},
|
||||
doesStepUseCompletionSignal: () => false,
|
||||
doesNodeUseCompletionSignal: () => false,
|
||||
ensureManualInteractionAllowed: async () => ({}),
|
||||
executeStep: async () => {},
|
||||
executeStepViaCompletionSignal: async () => {},
|
||||
executeNode: async () => {},
|
||||
executeNodeViaCompletionSignal: async () => {},
|
||||
exportSettingsBundle: async () => ({}),
|
||||
fetchGeneratedEmail: async () => '',
|
||||
finalizeStep3Completion: async () => {},
|
||||
@@ -43,7 +43,9 @@ test('message router appends success record on Plus final step instead of hard-c
|
||||
getCurrentLuckmailPurchase: () => null,
|
||||
getPendingAutoRunTimerPlan: () => null,
|
||||
getSourceLabel: () => '',
|
||||
getState: async () => ({ plusModeEnabled: true, stepStatuses: { 13: 'pending' } }),
|
||||
getState: async () => ({ plusModeEnabled: true, nodeStatuses: { 'platform-verify': 'pending' } }),
|
||||
getNodeIdsForState: () => ['open-chatgpt', 'oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify'],
|
||||
getStepIdByNodeIdForState: (nodeId) => ({ 'oauth-login': 10, 'fetch-login-code': 11, 'confirm-oauth': 12, 'platform-verify': 13 }[nodeId] || 0),
|
||||
getLastStepIdForState: () => 13,
|
||||
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'oauth-login' : 'platform-verify' }),
|
||||
getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
|
||||
@@ -66,8 +68,8 @@ test('message router appends success record on Plus final step instead of hard-c
|
||||
normalizeHotmailAccounts: (items) => items,
|
||||
normalizeRunCount: (value) => value,
|
||||
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
|
||||
notifyStepComplete: () => {},
|
||||
notifyStepError: () => {},
|
||||
notifyNodeComplete: () => {},
|
||||
notifyNodeError: () => {},
|
||||
patchHotmailAccount: async () => {},
|
||||
patchMail2925Account: async () => {},
|
||||
registerTab: async () => {},
|
||||
@@ -88,9 +90,9 @@ test('message router appends success record on Plus final step instead of hard-c
|
||||
setLuckmailPurchaseUsedState: async () => {},
|
||||
setPersistentSettings: async () => {},
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
setNodeStatus: async () => {},
|
||||
skipAutoRunCountdown: async () => false,
|
||||
skipStep: async () => {},
|
||||
skipNode: async () => {},
|
||||
startAutoRunLoop: async () => {},
|
||||
syncHotmailAccounts: async () => {},
|
||||
testHotmailAccountMailAccess: async () => {},
|
||||
@@ -98,7 +100,7 @@ test('message router appends success record on Plus final step instead of hard-c
|
||||
verifyHotmailAccount: async () => {},
|
||||
});
|
||||
|
||||
await router.handleMessage({ type: 'STEP_COMPLETE', step: 13, payload: {} }, {});
|
||||
await router.handleMessage({ type: 'NODE_COMPLETE', nodeId: 'platform-verify', payload: { nodeId: 'platform-verify' } }, {});
|
||||
|
||||
assert.equal(appendCalls.length, 1);
|
||||
assert.equal(appendCalls[0][0], 'success');
|
||||
|
||||
@@ -10,6 +10,7 @@ function createRouter(overrides = {}) {
|
||||
const events = {
|
||||
logs: [],
|
||||
stepStatuses: [],
|
||||
nodeStatuses: [],
|
||||
stateUpdates: [],
|
||||
broadcasts: [],
|
||||
balanceRefreshes: [],
|
||||
@@ -26,10 +27,63 @@ function createRouter(overrides = {}) {
|
||||
executedSteps: [],
|
||||
accountRecords: [],
|
||||
};
|
||||
const nodeByStep = {
|
||||
1: 'open-chatgpt',
|
||||
2: 'submit-signup-email',
|
||||
3: 'fill-password',
|
||||
4: 'fetch-signup-code',
|
||||
5: 'fill-profile',
|
||||
6: 'wait-registration-success',
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
9: 'confirm-oauth',
|
||||
10: 'platform-verify',
|
||||
11: 'fetch-login-code',
|
||||
12: 'confirm-oauth',
|
||||
13: 'platform-verify',
|
||||
};
|
||||
const normalStepByNode = {
|
||||
'open-chatgpt': 1,
|
||||
'submit-signup-email': 2,
|
||||
'fill-password': 3,
|
||||
'fetch-signup-code': 4,
|
||||
'fill-profile': 5,
|
||||
'wait-registration-success': 6,
|
||||
'oauth-login': 7,
|
||||
'fetch-login-code': 8,
|
||||
'confirm-oauth': 9,
|
||||
'platform-verify': 10,
|
||||
};
|
||||
const plusStepByNode = {
|
||||
'open-chatgpt': 1,
|
||||
'submit-signup-email': 2,
|
||||
'fill-password': 3,
|
||||
'fetch-signup-code': 4,
|
||||
'fill-profile': 5,
|
||||
'oauth-login': 10,
|
||||
'fetch-login-code': 11,
|
||||
'confirm-oauth': 12,
|
||||
'platform-verify': 13,
|
||||
};
|
||||
const getStepForNode = (nodeId) => {
|
||||
const state = normalizeState(overrides.state || {});
|
||||
return (state.plusModeEnabled ? plusStepByNode : normalStepByNode)[nodeId] || 0;
|
||||
};
|
||||
const normalizeState = (state = {}) => {
|
||||
const next = { ...(state || {}) };
|
||||
if (!next.nodeStatuses && next.stepStatuses) {
|
||||
next.nodeStatuses = Object.fromEntries(
|
||||
Object.entries(next.stepStatuses)
|
||||
.map(([step, status]) => [nodeByStep[Number(step)], status])
|
||||
.filter(([nodeId]) => Boolean(nodeId))
|
||||
);
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const router = api.createMessageRouter({
|
||||
addLog: async (message, level, options = {}) => {
|
||||
events.logs.push({ message, level, step: options.step, stepKey: options.stepKey });
|
||||
events.logs.push({ message, level, step: options.step, stepKey: options.stepKey, nodeId: options.nodeId });
|
||||
},
|
||||
appendAccountRunRecord: overrides.appendAccountRunRecord || (async (status, state, reason) => {
|
||||
events.accountRecords.push({ status, state, reason });
|
||||
@@ -49,20 +103,20 @@ function createRouter(overrides = {}) {
|
||||
clearStopRequest: () => {},
|
||||
closeLocalhostCallbackTabs: async () => {},
|
||||
closeTabsByUrlPrefix: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
events.notifyCompletions.push({ step, payload, via: 'completeStepFromBackground' });
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
events.notifyCompletions.push({ step: getStepForNode(nodeId), nodeId, payload, via: 'completeNodeFromBackground' });
|
||||
},
|
||||
deleteHotmailAccount: async () => {},
|
||||
deleteHotmailAccounts: async () => {},
|
||||
deleteIcloudAlias: async () => {},
|
||||
deleteUsedIcloudAliases: async () => {},
|
||||
disableUsedLuckmailPurchases: async () => {},
|
||||
doesStepUseCompletionSignal: () => false,
|
||||
doesNodeUseCompletionSignal: () => false,
|
||||
ensureManualInteractionAllowed: async () => ({}),
|
||||
executeStep: async (step) => {
|
||||
events.executedSteps.push(step);
|
||||
executeNode: async (nodeId) => {
|
||||
events.executedSteps.push(getStepForNode(nodeId) || nodeId);
|
||||
},
|
||||
executeStepViaCompletionSignal: async () => {},
|
||||
executeNodeViaCompletionSignal: async () => {},
|
||||
exportSettingsBundle: async () => ({}),
|
||||
fetchGeneratedEmail: async () => '',
|
||||
finalizePhoneActivationAfterSuccessfulFlow: overrides.finalizePhoneActivationAfterSuccessfulFlow || (async (state) => {
|
||||
@@ -77,7 +131,9 @@ function createRouter(overrides = {}) {
|
||||
getCurrentLuckmailPurchase: () => null,
|
||||
getPendingAutoRunTimerPlan: () => null,
|
||||
getSourceLabel: () => '',
|
||||
getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } },
|
||||
getState: async () => normalizeState(overrides.state || { nodeStatuses: { 'fill-password': 'pending' } }),
|
||||
getNodeIdsForState: () => ['open-chatgpt', 'submit-signup-email', 'fill-password', 'fetch-signup-code', 'fill-profile', 'wait-registration-success', 'oauth-login', 'fetch-login-code', 'confirm-oauth', 'platform-verify'],
|
||||
getStepIdByNodeIdForState: (nodeId, state = {}) => (state.plusModeEnabled ? plusStepByNode : normalStepByNode)[nodeId] || 0,
|
||||
getStepDefinitionForState: overrides.getStepDefinitionForState,
|
||||
getStepIdsForState: overrides.getStepIdsForState,
|
||||
getLastStepIdForState: overrides.getLastStepIdForState,
|
||||
@@ -106,11 +162,11 @@ function createRouter(overrides = {}) {
|
||||
normalizeHotmailAccounts: (items) => items,
|
||||
normalizeRunCount: (value) => value,
|
||||
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
|
||||
notifyStepComplete: (step, payload) => {
|
||||
events.notifyCompletions.push({ step, payload });
|
||||
notifyNodeComplete: (nodeId, payload) => {
|
||||
events.notifyCompletions.push({ step: getStepForNode(nodeId), nodeId, payload });
|
||||
},
|
||||
notifyStepError: (step, error) => {
|
||||
events.notifyErrors.push({ step, error });
|
||||
notifyNodeError: (nodeId, error) => {
|
||||
events.notifyErrors.push({ step: getStepForNode(nodeId), nodeId, error });
|
||||
},
|
||||
patchHotmailAccount: async () => {},
|
||||
registerTab: async () => {},
|
||||
@@ -142,11 +198,13 @@ function createRouter(overrides = {}) {
|
||||
setState: async (updates) => {
|
||||
events.stateUpdates.push(updates);
|
||||
},
|
||||
setStepStatus: async (step, status) => {
|
||||
setNodeStatus: async (nodeId, status) => {
|
||||
events.nodeStatuses.push({ nodeId, status });
|
||||
const step = getStepForNode(nodeId);
|
||||
events.stepStatuses.push({ step, status });
|
||||
},
|
||||
skipAutoRunCountdown: async () => false,
|
||||
skipStep: async () => {},
|
||||
skipNode: async () => {},
|
||||
startAutoRunLoop: async () => {},
|
||||
syncHotmailAccounts: async () => {},
|
||||
testHotmailAccountMailAccess: async () => {},
|
||||
@@ -366,10 +424,11 @@ test('message router finalizes step 3 before marking it completed', async () =>
|
||||
const { router, events } = createRouter();
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'STEP_COMPLETE',
|
||||
step: 3,
|
||||
type: 'NODE_COMPLETE',
|
||||
nodeId: 'fill-password',
|
||||
source: 'signup-page',
|
||||
payload: {
|
||||
nodeId: 'fill-password',
|
||||
email: 'user@example.com',
|
||||
signupVerificationRequestedAt: 123,
|
||||
},
|
||||
@@ -377,8 +436,10 @@ test('message router finalizes step 3 before marking it completed', async () =>
|
||||
|
||||
assert.deepStrictEqual(events.finalizePayloads, [
|
||||
{
|
||||
nodeId: 'fill-password',
|
||||
email: 'user@example.com',
|
||||
signupVerificationRequestedAt: 123,
|
||||
step: 3,
|
||||
},
|
||||
]);
|
||||
assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'completed' }]);
|
||||
@@ -386,9 +447,12 @@ test('message router finalizes step 3 before marking it completed', async () =>
|
||||
assert.deepStrictEqual(events.notifyCompletions, [
|
||||
{
|
||||
step: 3,
|
||||
nodeId: 'fill-password',
|
||||
payload: {
|
||||
nodeId: 'fill-password',
|
||||
email: 'user@example.com',
|
||||
signupVerificationRequestedAt: 123,
|
||||
step: 3,
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -436,7 +500,8 @@ test('message router finalizes pending phone activation on platform verify succe
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(events.phoneFinalizations, [state]);
|
||||
assert.equal(events.phoneFinalizations.length, 1);
|
||||
assert.deepStrictEqual(events.phoneFinalizations[0].pendingPhoneActivationConfirmation, state.pendingPhoneActivationConfirmation);
|
||||
});
|
||||
|
||||
test('message router does not finalize pending phone activation when icloud finalization fails', async () => {
|
||||
@@ -481,10 +546,11 @@ test('message router marks step 3 failed when post-submit finalize fails', async
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'STEP_COMPLETE',
|
||||
step: 3,
|
||||
type: 'NODE_COMPLETE',
|
||||
nodeId: 'fill-password',
|
||||
source: 'signup-page',
|
||||
payload: {
|
||||
nodeId: 'fill-password',
|
||||
email: 'user@example.com',
|
||||
},
|
||||
}, {});
|
||||
@@ -493,10 +559,11 @@ test('message router marks step 3 failed when post-submit finalize fails', async
|
||||
assert.deepStrictEqual(events.notifyErrors, [
|
||||
{
|
||||
step: 3,
|
||||
nodeId: 'fill-password',
|
||||
error: '步骤 3 提交后仍停留在密码页。',
|
||||
},
|
||||
]);
|
||||
assert.equal(events.logs.some(({ message, step }) => /失败:步骤 3 提交后仍停留在密码页。/.test(message) && step === 3), true);
|
||||
assert.equal(events.logs.some(({ message, nodeId }) => /失败:步骤 3 提交后仍停留在密码页。/.test(message) && nodeId === 'fill-password'), true);
|
||||
assert.deepStrictEqual(response, { ok: true, error: '步骤 3 提交后仍停留在密码页。' });
|
||||
});
|
||||
|
||||
@@ -512,10 +579,10 @@ test('message router does not duplicate step 3 mismatch failure log after finali
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'STEP_ERROR',
|
||||
step: 3,
|
||||
type: 'NODE_ERROR',
|
||||
nodeId: 'fill-password',
|
||||
source: 'signup-page',
|
||||
payload: {},
|
||||
payload: { nodeId: 'fill-password' },
|
||||
error: mismatchError,
|
||||
}, {});
|
||||
|
||||
@@ -524,6 +591,7 @@ test('message router does not duplicate step 3 mismatch failure log after finali
|
||||
assert.deepStrictEqual(events.notifyErrors, [
|
||||
{
|
||||
step: 3,
|
||||
nodeId: 'fill-password',
|
||||
error: mismatchError,
|
||||
},
|
||||
]);
|
||||
@@ -534,10 +602,10 @@ test('message router stops the flow and surfaces cloudflare security block error
|
||||
const { router, events } = createRouter();
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'STEP_ERROR',
|
||||
step: 7,
|
||||
type: 'NODE_ERROR',
|
||||
nodeId: 'oauth-login',
|
||||
source: 'signup-page',
|
||||
payload: {},
|
||||
payload: { nodeId: 'oauth-login' },
|
||||
error: 'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统',
|
||||
}, {});
|
||||
|
||||
@@ -545,6 +613,7 @@ test('message router stops the flow and surfaces cloudflare security block error
|
||||
assert.deepStrictEqual(events.notifyErrors, [
|
||||
{
|
||||
step: 7,
|
||||
nodeId: 'oauth-login',
|
||||
error: '流程已被用户停止。',
|
||||
},
|
||||
]);
|
||||
@@ -562,9 +631,10 @@ test('message router blocks manual step 4 execution when signup page tab is miss
|
||||
|
||||
await assert.rejects(
|
||||
() => router.handleMessage({
|
||||
type: 'EXECUTE_STEP',
|
||||
type: 'EXECUTE_NODE',
|
||||
source: 'sidepanel',
|
||||
payload: { step: 4 },
|
||||
nodeId: 'fetch-signup-code',
|
||||
payload: { nodeId: 'fetch-signup-code' },
|
||||
}, {}),
|
||||
/手动执行步骤 4 前,请先执行步骤 1 或步骤 2/
|
||||
);
|
||||
@@ -630,18 +700,19 @@ test('message router ignores stale step 2 errors while auto-run is already on a
|
||||
state: {
|
||||
autoRunning: true,
|
||||
autoRunPhase: 'running',
|
||||
currentStep: 6,
|
||||
stepStatuses: {
|
||||
2: 'completed',
|
||||
6: 'running',
|
||||
currentNodeId: 'wait-registration-success',
|
||||
nodeStatuses: {
|
||||
'submit-signup-email': 'completed',
|
||||
'wait-registration-success': 'running',
|
||||
},
|
||||
},
|
||||
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'STEP_ERROR',
|
||||
step: 2,
|
||||
type: 'NODE_ERROR',
|
||||
nodeId: 'submit-signup-email',
|
||||
payload: { nodeId: 'submit-signup-email' },
|
||||
error: '步骤 2:旧页面异步失败,不应覆盖当前第 6 步记录。',
|
||||
}, {});
|
||||
|
||||
@@ -649,7 +720,7 @@ test('message router ignores stale step 2 errors while auto-run is already on a
|
||||
assert.deepStrictEqual(events.stepStatuses, []);
|
||||
assert.deepStrictEqual(events.notifyErrors, []);
|
||||
assert.deepStrictEqual(events.accountRecords, []);
|
||||
assert.equal(events.logs.some(({ message }) => /忽略过期的步骤 2 失败消息/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /忽略过期的节点 submit-signup-email 失败消息/.test(message)), true);
|
||||
});
|
||||
|
||||
test('message router ignores stale step 2 completion while auto-run is already on a later step', async () => {
|
||||
@@ -657,19 +728,20 @@ test('message router ignores stale step 2 completion while auto-run is already o
|
||||
state: {
|
||||
autoRunning: true,
|
||||
autoRunPhase: 'running',
|
||||
currentStep: 6,
|
||||
stepStatuses: {
|
||||
2: 'completed',
|
||||
6: 'running',
|
||||
currentNodeId: 'wait-registration-success',
|
||||
nodeStatuses: {
|
||||
'submit-signup-email': 'completed',
|
||||
'wait-registration-success': 'running',
|
||||
},
|
||||
},
|
||||
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'STEP_COMPLETE',
|
||||
step: 2,
|
||||
type: 'NODE_COMPLETE',
|
||||
nodeId: 'submit-signup-email',
|
||||
payload: {
|
||||
nodeId: 'submit-signup-email',
|
||||
email: 'late@example.com',
|
||||
},
|
||||
}, {});
|
||||
@@ -678,5 +750,5 @@ test('message router ignores stale step 2 completion while auto-run is already o
|
||||
assert.deepStrictEqual(events.stepStatuses, []);
|
||||
assert.deepStrictEqual(events.notifyCompletions, []);
|
||||
assert.deepStrictEqual(events.emailStates, []);
|
||||
assert.equal(events.logs.some(({ message }) => /忽略过期的步骤 2 完成消息/.test(message)), true);
|
||||
assert.equal(events.logs.some(({ message }) => /忽略过期的节点 submit-signup-email 完成消息/.test(message)), true);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
@@ -35,7 +35,7 @@ test('platform verify module supports codex2api protocol callback exchange', asy
|
||||
},
|
||||
chrome: {},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completed.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
@@ -68,7 +68,7 @@ test('platform verify module supports codex2api protocol callback exchange', asy
|
||||
]);
|
||||
assert.deepStrictEqual(completed, [
|
||||
{
|
||||
step: 10,
|
||||
step: 'platform-verify',
|
||||
payload: {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
verifiedStatus: 'OAuth 账号 flow@example.com 添加成功',
|
||||
@@ -98,7 +98,7 @@ test('platform verify retries transient SUB2API oauth/token exchange errors befo
|
||||
},
|
||||
},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getPanelMode: () => 'sub2api',
|
||||
getTabId: async () => 12,
|
||||
@@ -166,7 +166,7 @@ test('platform verify retries transient SUB2API token_exchange_user_error before
|
||||
},
|
||||
},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getPanelMode: () => 'sub2api',
|
||||
getTabId: async () => 12,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
@@ -17,7 +17,7 @@ function createDeps(overrides = {}) {
|
||||
},
|
||||
},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completed.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
@@ -97,7 +97,7 @@ test('platform verify module submits CPA callback via management API first', asy
|
||||
assert.equal(uiCalled, false);
|
||||
assert.deepStrictEqual(completed, [
|
||||
{
|
||||
step: 10,
|
||||
step: 'platform-verify',
|
||||
payload: {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
verifiedStatus: 'CPA API 回调提交成功',
|
||||
@@ -317,7 +317,7 @@ test('platform verify module submits Plus visible step 13 to SUB2API via direct
|
||||
assert.equal(exchangeCall.body.state, 'oauth-state');
|
||||
assert.deepStrictEqual(createCall.body.group_ids, [5]);
|
||||
assert.deepStrictEqual(completed, [{
|
||||
step: 13,
|
||||
step: 'platform-verify',
|
||||
payload: {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
verifiedStatus: 'SUB2API 已创建账号 #11',
|
||||
|
||||
@@ -22,29 +22,22 @@ test('runtime-state module exposes a factory', () => {
|
||||
assert.equal(typeof api?.createRuntimeStateHelpers, 'function');
|
||||
});
|
||||
|
||||
test('runtime-state view derives canonical flow metadata from legacy step state', () => {
|
||||
test('runtime-state view preserves canonical flow metadata from node state', () => {
|
||||
const api = loadRuntimeStateApi();
|
||||
const helpers = api.createRuntimeStateHelpers({
|
||||
DEFAULT_ACTIVE_FLOW_ID: 'openai',
|
||||
defaultStepStatuses: {
|
||||
1: 'pending',
|
||||
2: 'pending',
|
||||
10: 'pending',
|
||||
},
|
||||
getStepDefinitionForState(step) {
|
||||
return {
|
||||
1: { id: 1, key: 'open-chatgpt' },
|
||||
2: { id: 2, key: 'submit-signup-email' },
|
||||
10: { id: 10, key: 'oauth-login' },
|
||||
}[Number(step)] || null;
|
||||
defaultNodeStatuses: {
|
||||
'open-chatgpt': 'pending',
|
||||
'submit-signup-email': 'pending',
|
||||
'oauth-login': 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
const view = helpers.buildStateView({
|
||||
currentStep: 2,
|
||||
stepStatuses: {
|
||||
1: 'completed',
|
||||
2: 'running',
|
||||
currentNodeId: 'submit-signup-email',
|
||||
nodeStatuses: {
|
||||
'open-chatgpt': 'completed',
|
||||
'submit-signup-email': 'running',
|
||||
},
|
||||
oauthUrl: 'https://auth.example.com/start',
|
||||
plusCheckoutTabId: 88,
|
||||
@@ -63,14 +56,9 @@ test('runtime-state view derives canonical flow metadata from legacy step state'
|
||||
|
||||
assert.equal(view.activeFlowId, 'openai');
|
||||
assert.equal(view.currentNodeId, 'submit-signup-email');
|
||||
assert.deepStrictEqual(view.legacyStepCompat, {
|
||||
currentStep: 2,
|
||||
stepStatuses: {
|
||||
1: 'completed',
|
||||
2: 'running',
|
||||
10: 'pending',
|
||||
},
|
||||
});
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(view, 'legacyStepCompat'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(view, 'currentStep'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(view, 'stepStatuses'), false);
|
||||
assert.deepStrictEqual(view.nodeStatuses, {
|
||||
'open-chatgpt': 'completed',
|
||||
'submit-signup-email': 'running',
|
||||
@@ -93,35 +81,33 @@ test('runtime-state view derives canonical flow metadata from legacy step state'
|
||||
});
|
||||
});
|
||||
|
||||
test('runtime-state patch accepts nested flow updates while keeping legacy compatibility fields in sync', () => {
|
||||
test('runtime-state patch accepts nested flow and node updates without legacy step state', () => {
|
||||
const api = loadRuntimeStateApi();
|
||||
const helpers = api.createRuntimeStateHelpers({
|
||||
DEFAULT_ACTIVE_FLOW_ID: 'openai',
|
||||
defaultStepStatuses: {
|
||||
1: 'pending',
|
||||
2: 'pending',
|
||||
10: 'pending',
|
||||
},
|
||||
getStepDefinitionForState(step) {
|
||||
return {
|
||||
1: { id: 1, key: 'open-chatgpt' },
|
||||
2: { id: 2, key: 'submit-signup-email' },
|
||||
10: { id: 10, key: 'oauth-login' },
|
||||
}[Number(step)] || null;
|
||||
defaultNodeStatuses: {
|
||||
'open-chatgpt': 'pending',
|
||||
'submit-signup-email': 'pending',
|
||||
'oauth-login': 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
const patch = helpers.buildSessionStatePatch({
|
||||
currentStep: 1,
|
||||
stepStatuses: {
|
||||
1: 'running',
|
||||
2: 'pending',
|
||||
10: 'pending',
|
||||
currentNodeId: 'open-chatgpt',
|
||||
nodeStatuses: {
|
||||
'open-chatgpt': 'running',
|
||||
'submit-signup-email': 'pending',
|
||||
'oauth-login': 'pending',
|
||||
},
|
||||
oauthUrl: 'https://old.example.com/start',
|
||||
}, {
|
||||
runtimeState: {
|
||||
activeRunId: 'run-001',
|
||||
currentNodeId: 'oauth-login',
|
||||
nodeStatuses: {
|
||||
'open-chatgpt': 'completed',
|
||||
'oauth-login': 'running',
|
||||
},
|
||||
flowState: {
|
||||
openai: {
|
||||
auth: {
|
||||
@@ -132,13 +118,6 @@ test('runtime-state patch accepts nested flow updates while keeping legacy compa
|
||||
},
|
||||
},
|
||||
},
|
||||
legacyStepCompat: {
|
||||
currentStep: 10,
|
||||
stepStatuses: {
|
||||
1: 'completed',
|
||||
10: 'running',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -147,12 +126,8 @@ test('runtime-state patch accepts nested flow updates while keeping legacy compa
|
||||
assert.equal(patch.currentNodeId, 'oauth-login');
|
||||
assert.equal(patch.oauthUrl, 'https://new.example.com/start');
|
||||
assert.equal(patch.plusCheckoutTabId, 99);
|
||||
assert.equal(patch.currentStep, 10);
|
||||
assert.deepStrictEqual(patch.stepStatuses, {
|
||||
1: 'completed',
|
||||
2: 'pending',
|
||||
10: 'running',
|
||||
});
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(patch, 'currentStep'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(patch, 'stepStatuses'), false);
|
||||
assert.deepStrictEqual(patch.nodeStatuses, {
|
||||
'open-chatgpt': 'completed',
|
||||
'submit-signup-email': 'pending',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
@@ -20,7 +20,7 @@ test('step 2 completes with password step skipped when landing on email verifica
|
||||
const executor = step2Api.createStep2Executor({
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completedPayloads.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
@@ -40,7 +40,7 @@ test('step 2 completes with password step skipped when landing on email verifica
|
||||
|
||||
assert.deepStrictEqual(completedPayloads, [
|
||||
{
|
||||
step: 2,
|
||||
step: 'submit-signup-email',
|
||||
payload: {
|
||||
email: 'user@example.com',
|
||||
accountIdentifierType: 'email',
|
||||
@@ -59,7 +59,7 @@ test('step 2 keeps password flow when landing on password page', async () => {
|
||||
const executor = step2Api.createStep2Executor({
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completedPayloads.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
@@ -79,7 +79,7 @@ test('step 2 keeps password flow when landing on password page', async () => {
|
||||
|
||||
assert.deepStrictEqual(completedPayloads, [
|
||||
{
|
||||
step: 2,
|
||||
step: 'submit-signup-email',
|
||||
payload: {
|
||||
email: 'user@example.com',
|
||||
accountIdentifierType: 'email',
|
||||
@@ -110,7 +110,7 @@ test('step 2 uses phone activation when resolved signup method is phone', async
|
||||
const executor = step2Api.createStep2Executor({
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completedPayloads.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
@@ -170,7 +170,7 @@ test('step 2 uses phone activation when resolved signup method is phone', async
|
||||
]);
|
||||
assert.deepStrictEqual(completedPayloads, [
|
||||
{
|
||||
step: 2,
|
||||
step: 'submit-signup-email',
|
||||
payload: {
|
||||
accountIdentifierType: 'phone',
|
||||
accountIdentifier: '66959916439',
|
||||
@@ -200,7 +200,7 @@ test('step 2 reuses existing signup phone activation without acquiring a new num
|
||||
const executor = step2Api.createStep2Executor({
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completedPayloads.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
@@ -263,7 +263,7 @@ test('step 2 submits manual signup phone without acquiring a number', async () =
|
||||
const executor = step2Api.createStep2Executor({
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completedPayloads.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
@@ -310,7 +310,7 @@ test('step 2 submits manual signup phone without acquiring a number', async () =
|
||||
]);
|
||||
assert.deepStrictEqual(completedPayloads, [
|
||||
{
|
||||
step: 2,
|
||||
step: 'submit-signup-email',
|
||||
payload: {
|
||||
accountIdentifierType: 'phone',
|
||||
accountIdentifier: '+446700000002',
|
||||
@@ -338,7 +338,7 @@ test('step 2 stops with an explicit error instead of silently skipping 3/4/5 on
|
||||
get: async () => ({ url: 'https://chatgpt.com/' }),
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completedPayloads.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
@@ -382,7 +382,7 @@ test('step 2 does not force auth-entry retry on logged-out chatgpt home when con
|
||||
get: async () => ({ url: 'https://chatgpt.com/' }),
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completedPayloads.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
@@ -414,7 +414,7 @@ test('step 2 does not force auth-entry retry on logged-out chatgpt home when con
|
||||
assert.deepStrictEqual(sentPayloads, [{ email: 'user@example.com' }]);
|
||||
assert.deepStrictEqual(completedPayloads, [
|
||||
{
|
||||
step: 2,
|
||||
step: 'submit-signup-email',
|
||||
payload: {
|
||||
email: 'user@example.com',
|
||||
accountIdentifierType: 'email',
|
||||
@@ -450,7 +450,7 @@ test('step 2 waits for the existing signup tab to settle before probing the entr
|
||||
},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completedPayloads.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {
|
||||
@@ -499,7 +499,7 @@ test('step 2 waits for the existing signup tab to settle before probing the entr
|
||||
assert.equal(logs.some((item) => item.meta.step === 2 && item.meta.stepKey === 'signup-entry'), true);
|
||||
assert.deepStrictEqual(completedPayloads, [
|
||||
{
|
||||
step: 2,
|
||||
step: 'submit-signup-email',
|
||||
payload: {
|
||||
email: 'user@example.com',
|
||||
accountIdentifierType: 'email',
|
||||
|
||||
@@ -4,6 +4,19 @@ const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
const OPENAI_NODE_IDS = [
|
||||
'open-chatgpt',
|
||||
'submit-signup-email',
|
||||
'fill-password',
|
||||
'fetch-signup-code',
|
||||
'fill-profile',
|
||||
'wait-registration-success',
|
||||
'oauth-login',
|
||||
'fetch-login-code',
|
||||
'confirm-oauth',
|
||||
'platform-verify',
|
||||
];
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
@@ -48,113 +61,107 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('skipStep cascades from step 1 to step 5 when downstream steps are pending', async () => {
|
||||
function createApi() {
|
||||
const bundle = [
|
||||
extractFunction('isStepDoneStatus'),
|
||||
extractFunction('skipStep'),
|
||||
extractFunction('skipNode'),
|
||||
].join('\n');
|
||||
|
||||
const statuses = {
|
||||
1: 'pending',
|
||||
2: 'pending',
|
||||
3: 'pending',
|
||||
4: 'pending',
|
||||
5: 'pending',
|
||||
6: 'pending',
|
||||
7: 'pending',
|
||||
8: 'pending',
|
||||
9: 'pending',
|
||||
10: 'pending',
|
||||
};
|
||||
return new Function(`
|
||||
const DEFAULT_STATE = { nodeStatuses: {} };
|
||||
function getNodeIdsForState() {
|
||||
return ${JSON.stringify(OPENAI_NODE_IDS)};
|
||||
}
|
||||
function normalizeStatusMapForNodes(statuses = {}) {
|
||||
return { ...statuses };
|
||||
}
|
||||
${bundle}
|
||||
return { skipNode };
|
||||
`)();
|
||||
}
|
||||
|
||||
test('skipNode cascades from open-chatgpt through signup profile when downstream nodes are pending', async () => {
|
||||
const statuses = Object.fromEntries(OPENAI_NODE_IDS.map((nodeId) => [nodeId, 'pending']));
|
||||
const events = {
|
||||
setStepStatusCalls: [],
|
||||
setNodeStatusCalls: [],
|
||||
logs: [],
|
||||
};
|
||||
|
||||
const api = new Function(`
|
||||
const STEP_IDS = [1,2,3,4,5,6,7,8,9,10];
|
||||
${bundle}
|
||||
return { skipStep };
|
||||
`)();
|
||||
const api = createApi();
|
||||
|
||||
globalThis.ensureManualInteractionAllowed = async () => ({
|
||||
stepStatuses: { ...statuses },
|
||||
nodeStatuses: { ...statuses },
|
||||
});
|
||||
globalThis.getState = async () => ({
|
||||
stepStatuses: { ...statuses },
|
||||
nodeStatuses: { ...statuses },
|
||||
});
|
||||
globalThis.setStepStatus = async (step, status) => {
|
||||
events.setStepStatusCalls.push({ step, status });
|
||||
statuses[step] = status;
|
||||
globalThis.setNodeStatus = async (nodeId, status) => {
|
||||
events.setNodeStatusCalls.push({ nodeId, status });
|
||||
statuses[nodeId] = status;
|
||||
};
|
||||
globalThis.addLog = async (message, level) => {
|
||||
events.logs.push({ message, level });
|
||||
};
|
||||
|
||||
const result = await api.skipStep(1);
|
||||
const result = await api.skipNode('open-chatgpt');
|
||||
|
||||
assert.deepStrictEqual(result, { ok: true, step: 1, status: 'skipped' });
|
||||
assert.deepStrictEqual(events.setStepStatusCalls, [
|
||||
{ step: 1, status: 'skipped' },
|
||||
{ step: 2, status: 'skipped' },
|
||||
{ step: 3, status: 'skipped' },
|
||||
{ step: 4, status: 'skipped' },
|
||||
{ step: 5, status: 'skipped' },
|
||||
assert.deepStrictEqual(result, { ok: true, nodeId: 'open-chatgpt', status: 'skipped' });
|
||||
assert.deepStrictEqual(events.setNodeStatusCalls, [
|
||||
{ nodeId: 'open-chatgpt', status: 'skipped' },
|
||||
{ nodeId: 'submit-signup-email', status: 'skipped' },
|
||||
{ nodeId: 'fill-password', status: 'skipped' },
|
||||
{ nodeId: 'fetch-signup-code', status: 'skipped' },
|
||||
{ nodeId: 'fill-profile', status: 'skipped' },
|
||||
]);
|
||||
assert.equal(events.logs[0]?.message, '步骤 1 已跳过');
|
||||
assert.equal(events.logs[1]?.message, '步骤 1 已跳过,步骤 2、3、4、5 也已同时跳过。');
|
||||
assert.equal(events.logs[0]?.message, '节点 open-chatgpt 已跳过');
|
||||
assert.equal(
|
||||
events.logs[1]?.message,
|
||||
'节点 open-chatgpt 已跳过,节点 submit-signup-email、fill-password、fetch-signup-code、fill-profile 也已同时跳过。'
|
||||
);
|
||||
});
|
||||
|
||||
test('skipStep from step 1 skips only unfinished steps up to step 5', async () => {
|
||||
const bundle = [
|
||||
extractFunction('isStepDoneStatus'),
|
||||
extractFunction('skipStep'),
|
||||
].join('\n');
|
||||
|
||||
test('skipNode from open-chatgpt skips only unfinished linked signup nodes', async () => {
|
||||
const statuses = {
|
||||
1: 'pending',
|
||||
2: 'completed',
|
||||
3: 'running',
|
||||
4: 'pending',
|
||||
5: 'manual_completed',
|
||||
6: 'pending',
|
||||
7: 'pending',
|
||||
8: 'pending',
|
||||
9: 'pending',
|
||||
10: 'pending',
|
||||
'open-chatgpt': 'pending',
|
||||
'submit-signup-email': 'completed',
|
||||
'fill-password': 'running',
|
||||
'fetch-signup-code': 'pending',
|
||||
'fill-profile': 'manual_completed',
|
||||
'wait-registration-success': 'pending',
|
||||
'oauth-login': 'pending',
|
||||
'fetch-login-code': 'pending',
|
||||
'confirm-oauth': 'pending',
|
||||
'platform-verify': 'pending',
|
||||
};
|
||||
|
||||
const events = {
|
||||
setStepStatusCalls: [],
|
||||
setNodeStatusCalls: [],
|
||||
logs: [],
|
||||
};
|
||||
|
||||
const api = new Function(`
|
||||
const STEP_IDS = [1,2,3,4,5,6,7,8,9,10];
|
||||
${bundle}
|
||||
return { skipStep };
|
||||
`)();
|
||||
const api = createApi();
|
||||
|
||||
globalThis.ensureManualInteractionAllowed = async () => ({
|
||||
stepStatuses: { ...statuses },
|
||||
nodeStatuses: { ...statuses },
|
||||
});
|
||||
globalThis.getState = async () => ({
|
||||
stepStatuses: { ...statuses },
|
||||
nodeStatuses: { ...statuses },
|
||||
});
|
||||
globalThis.setStepStatus = async (step, status) => {
|
||||
events.setStepStatusCalls.push({ step, status });
|
||||
statuses[step] = status;
|
||||
globalThis.setNodeStatus = async (nodeId, status) => {
|
||||
events.setNodeStatusCalls.push({ nodeId, status });
|
||||
statuses[nodeId] = status;
|
||||
};
|
||||
globalThis.addLog = async (message, level) => {
|
||||
events.logs.push({ message, level });
|
||||
};
|
||||
|
||||
await api.skipStep(1);
|
||||
await api.skipNode('open-chatgpt');
|
||||
|
||||
assert.deepStrictEqual(events.setStepStatusCalls, [
|
||||
{ step: 1, status: 'skipped' },
|
||||
{ step: 4, status: 'skipped' },
|
||||
assert.deepStrictEqual(events.setNodeStatusCalls, [
|
||||
{ nodeId: 'open-chatgpt', status: 'skipped' },
|
||||
{ nodeId: 'fetch-signup-code', status: 'skipped' },
|
||||
]);
|
||||
assert.equal(events.logs.some(({ message }) => message === '步骤 1 已跳过,步骤 4 也已同时跳过。'), true);
|
||||
assert.equal(
|
||||
events.logs.some(({ message }) => (
|
||||
message === '节点 open-chatgpt 已跳过,节点 fetch-signup-code 也已同时跳过。'
|
||||
)),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
@@ -51,57 +51,56 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
function createApi(events, lastStepId = 10) {
|
||||
return new Function('events', 'lastStepId', `
|
||||
function createApi(events, lastNodeId = 'platform-verify') {
|
||||
return new Function('events', 'lastNodeId', `
|
||||
let stopRequested = false;
|
||||
const LOG_PREFIX = '[test]';
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
const LAST_STEP_ID = 10;
|
||||
function getErrorMessage(error) {
|
||||
return error?.message || String(error || '');
|
||||
}
|
||||
async function getState() {
|
||||
events.push({ type: 'getState' });
|
||||
return { stepStatuses: {}, contributionMode: true };
|
||||
return { nodeStatuses: {}, contributionMode: true };
|
||||
}
|
||||
function getLastStepIdForState() {
|
||||
return lastStepId;
|
||||
function getLastNodeIdForState() {
|
||||
return lastNodeId;
|
||||
}
|
||||
async function setStepStatus(step, status) {
|
||||
events.push({ type: 'status', step, status });
|
||||
async function setNodeStatus(nodeId, status) {
|
||||
events.push({ type: 'status', nodeId, status });
|
||||
}
|
||||
async function addLog(message, level) {
|
||||
events.push({ type: 'log', message, level });
|
||||
async function addLog(message, level, options = {}) {
|
||||
events.push({ type: 'log', message, level, options });
|
||||
}
|
||||
async function appendManualAccountRunRecordIfNeeded() {
|
||||
events.push({ type: 'manual-record' });
|
||||
}
|
||||
function notifyStepError(step, error) {
|
||||
events.push({ type: 'error', step, error });
|
||||
function notifyNodeError(nodeId, error) {
|
||||
events.push({ type: 'error', nodeId, error });
|
||||
}
|
||||
function notifyStepComplete(step, payload) {
|
||||
events.push({ type: 'notify', step, payload });
|
||||
function notifyNodeComplete(nodeId, payload) {
|
||||
events.push({ type: 'notify', nodeId, payload });
|
||||
}
|
||||
async function handleStepData(step, payload) {
|
||||
events.push({ type: 'handle-start', step, payload });
|
||||
async function handleNodeData(nodeId, payload) {
|
||||
events.push({ type: 'handle-start', nodeId, payload });
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
events.push({ type: 'handle-done', step });
|
||||
events.push({ type: 'handle-done', nodeId });
|
||||
}
|
||||
async function appendAndBroadcastAccountRunRecord(status, state) {
|
||||
events.push({ type: 'record', status, state });
|
||||
}
|
||||
${extractFunction('runCompletedStepSideEffects')}
|
||||
${extractFunction('reportCompletedStepSideEffectError')}
|
||||
${extractFunction('completeStepFromBackground')}
|
||||
return { completeStepFromBackground };
|
||||
`)(events, lastStepId);
|
||||
${extractFunction('runCompletedNodeSideEffects')}
|
||||
${extractFunction('reportCompletedNodeSideEffectError')}
|
||||
${extractFunction('completeNodeFromBackground')}
|
||||
return { completeNodeFromBackground };
|
||||
`)(events, lastNodeId);
|
||||
}
|
||||
|
||||
test('completeStepFromBackground releases final step before slow post-completion side effects', async () => {
|
||||
test('completeNodeFromBackground releases final node before slow post-completion side effects', async () => {
|
||||
const events = [];
|
||||
const api = createApi(events, 10);
|
||||
const api = createApi(events, 'platform-verify');
|
||||
|
||||
await api.completeStepFromBackground(10, { localhostUrl: 'http://localhost:1455/auth/callback?code=ok' });
|
||||
await api.completeNodeFromBackground('platform-verify', { localhostUrl: 'http://localhost:1455/auth/callback?code=ok' });
|
||||
|
||||
const types = events.map((event) => event.type);
|
||||
assert.equal(types.indexOf('notify') < types.indexOf('handle-start'), true);
|
||||
@@ -115,11 +114,11 @@ test('completeStepFromBackground releases final step before slow post-completion
|
||||
assert.equal(settledTypes.includes('record'), true);
|
||||
});
|
||||
|
||||
test('completeStepFromBackground keeps non-final step data handling before completion signal', async () => {
|
||||
test('completeNodeFromBackground keeps non-final node data handling before completion signal', async () => {
|
||||
const events = [];
|
||||
const api = createApi(events, 10);
|
||||
const api = createApi(events, 'platform-verify');
|
||||
|
||||
await api.completeStepFromBackground(9, { localhostUrl: 'http://localhost:1455/auth/callback?code=ok' });
|
||||
await api.completeNodeFromBackground('confirm-oauth', { localhostUrl: 'http://localhost:1455/auth/callback?code=ok' });
|
||||
|
||||
const types = events.map((event) => event.type);
|
||||
assert.equal(types.indexOf('handle-done') < types.indexOf('notify'), true);
|
||||
|
||||
@@ -2,18 +2,20 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports step registry and shared step definitions', () => {
|
||||
test('background imports node registry and shared workflow definitions', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/steps\/registry\.js/);
|
||||
assert.match(source, /data\/step-definitions\.js/);
|
||||
assert.match(source, /MultiPageStepDefinitions\?\.getSteps/);
|
||||
assert.match(source, /background\/workflow-engine\.js/);
|
||||
assert.match(source, /MultiPageStepDefinitions\?\.getNodes/);
|
||||
assert.match(source, /getStepRegistryForState\(state\)/);
|
||||
assert.match(source, /buildNodeRegistry\(definitions/);
|
||||
assert.match(source, /PLUS_PAYPAL_STEP_DEFINITIONS/);
|
||||
assert.match(source, /PLUS_GOPAY_STEP_DEFINITIONS/);
|
||||
assert.match(source, /plusPayPalStepRegistry/);
|
||||
assert.match(source, /plusGoPayStepRegistry/);
|
||||
assert.match(source, /normalizePlusPaymentMethod\(state\?\.plusPaymentMethod\) === PLUS_PAYMENT_METHOD_GOPAY/);
|
||||
assert.match(source, /activeStepRegistry\.executeStep\(step,\s*\{/);
|
||||
assert.match(source, /activeStepRegistry\.executeNode\(normalizedNodeId,\s*\{/);
|
||||
assert.match(source, /background\/steps\/create-plus-checkout\.js/);
|
||||
assert.match(source, /background\/steps\/fill-plus-checkout\.js/);
|
||||
assert.match(source, /background\/steps\/gopay-manual-confirm\.js/);
|
||||
|
||||
@@ -39,7 +39,8 @@ test('step 3 reuses existing generated password when rerunning the same email fl
|
||||
assert.deepStrictEqual(events.passwordStates, ['Secret123!']);
|
||||
assert.deepStrictEqual(events.messages, [
|
||||
{
|
||||
type: 'EXECUTE_STEP',
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'fill-password',
|
||||
step: 3,
|
||||
source: 'background',
|
||||
payload: {
|
||||
@@ -109,7 +110,8 @@ test('step 3 supports phone-only signup identity when password page is present',
|
||||
]);
|
||||
assert.deepStrictEqual(events.messages, [
|
||||
{
|
||||
type: 'EXECUTE_STEP',
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'fill-password',
|
||||
step: 3,
|
||||
source: 'background',
|
||||
payload: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
@@ -23,7 +23,7 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
|
||||
},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureMail2925MailboxSession: async () => {
|
||||
ensureCalls += 1;
|
||||
@@ -84,7 +84,7 @@ test('step 4 does not request a fresh code first for Cloudflare temp mail', asyn
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureMail2925MailboxSession: async () => {},
|
||||
getMailConfig: () => ({
|
||||
@@ -135,7 +135,7 @@ test('step 4 checks iCloud session before polling iCloud mailbox', async () => {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureIcloudMailSession: async () => {
|
||||
icloudChecks += 1;
|
||||
@@ -183,7 +183,7 @@ test('step 4 forwards skipProfileStep when prepare stage already reached logged-
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completions.push({ step, payload });
|
||||
},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
@@ -224,7 +224,7 @@ test('step 4 forwards skipProfileStep when prepare stage already reached logged-
|
||||
|
||||
assert.deepStrictEqual(completions, [
|
||||
{
|
||||
step: 4,
|
||||
step: 'fetch-signup-code',
|
||||
payload: { skipProfileStep: true },
|
||||
},
|
||||
]);
|
||||
@@ -244,7 +244,7 @@ test('step 4 phone signup branch uses SMS helper and does not poll mailbox', asy
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completions.push({ step, payload });
|
||||
},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
@@ -296,7 +296,7 @@ test('step 4 phone signup branch uses SMS helper and does not poll mailbox', asy
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(phoneCalls[0].options, 'signupProfile'), true);
|
||||
assert.deepStrictEqual(completions, [
|
||||
{
|
||||
step: 4,
|
||||
step: 'fetch-signup-code',
|
||||
payload: {
|
||||
phoneVerification: true,
|
||||
code: '',
|
||||
@@ -321,7 +321,7 @@ test('step 4 phone signup email-verification handoff polls mailbox instead of co
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
completions.push({ step, payload });
|
||||
},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
@@ -403,7 +403,7 @@ test('step 4 prepare retries transport by recovering retry page without replayin
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureMail2925MailboxSession: async () => {},
|
||||
getMailConfig: () => ({
|
||||
|
||||
@@ -30,7 +30,8 @@ test('step 5 forwards generated profile data and relies on completion signal flo
|
||||
{
|
||||
source: 'signup-page',
|
||||
message: {
|
||||
type: 'EXECUTE_STEP',
|
||||
type: 'EXECUTE_NODE',
|
||||
nodeId: 'fill-profile',
|
||||
step: 5,
|
||||
source: 'background',
|
||||
payload: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
@@ -17,7 +17,7 @@ test('step 6 waits for registration success and completes from background', asyn
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.logs.push({ message, level });
|
||||
},
|
||||
completeStepFromBackground: async (step) => {
|
||||
completeNodeFromBackground: async (step) => {
|
||||
events.completedSteps.push(step);
|
||||
},
|
||||
sleepWithStop: async (ms) => {
|
||||
@@ -28,7 +28,7 @@ test('step 6 waits for registration success and completes from background', asyn
|
||||
await executor.executeStep6();
|
||||
|
||||
assert.deepStrictEqual(events.waits, [20000]);
|
||||
assert.deepStrictEqual(events.completedSteps, [6]);
|
||||
assert.deepStrictEqual(events.completedSteps, ['wait-registration-success']);
|
||||
assert.ok(events.logs.some(({ message }) => /等待 20 秒/.test(message)));
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ test('step 6 only clears cookies when cleanup switch is enabled', async () => {
|
||||
const executor = api.createStep6Executor({
|
||||
addLog: async () => {},
|
||||
chrome: chromeApi,
|
||||
completeStepFromBackground: async (step) => {
|
||||
completeNodeFromBackground: async (step) => {
|
||||
events.completedSteps.push(step);
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
@@ -77,7 +77,7 @@ test('step 6 only clears cookies when cleanup switch is enabled', async () => {
|
||||
|
||||
await executor.executeStep6({ step6CookieCleanupEnabled: true });
|
||||
|
||||
assert.deepStrictEqual(events.completedSteps, [6, 6]);
|
||||
assert.deepStrictEqual(events.completedSteps, ['wait-registration-success', 'wait-registration-success']);
|
||||
assert.deepStrictEqual(events.removedCookies, [
|
||||
{
|
||||
url: 'https://chatgpt.com/auth',
|
||||
@@ -102,7 +102,7 @@ test('step 7 retries up to configured limit and then fails', async () => {
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {
|
||||
completeNodeFromBackground: async () => {
|
||||
events.completed += 1;
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
@@ -153,7 +153,7 @@ test('step 7 exits internal retry loop immediately when add-phone is detected',
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.logs.push({ message, level });
|
||||
},
|
||||
completeStepFromBackground: async () => {
|
||||
completeNodeFromBackground: async () => {
|
||||
events.completed += 1;
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
@@ -201,7 +201,7 @@ test('step 7 hands direct add-phone to shared phone verification when enabled',
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
events.completions.push({ step, payload });
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
@@ -256,7 +256,7 @@ test('step 7 hands direct add-phone to shared phone verification when enabled',
|
||||
]);
|
||||
assert.deepStrictEqual(events.completions, [
|
||||
{
|
||||
step: 7,
|
||||
step: 'oauth-login',
|
||||
payload: {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
@@ -280,7 +280,7 @@ test('step 7 direct add-phone stays fatal when phone verification is disabled',
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {
|
||||
completeNodeFromBackground: async () => {
|
||||
events.completions += 1;
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
@@ -324,7 +324,7 @@ test('step 7 propagates fatal errors from shared add-phone verification', async
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {
|
||||
completeNodeFromBackground: async () => {
|
||||
events.completions += 1;
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
@@ -368,7 +368,7 @@ test('step 7 starts a new oauth timeout window for each refreshed oauth url', as
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs, options) => {
|
||||
@@ -419,7 +419,7 @@ test('step 7 forwards direct OAuth consent skip metadata when completing', async
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
events.completions.push({ step, payload });
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
@@ -447,7 +447,7 @@ test('step 7 forwards direct OAuth consent skip metadata when completing', async
|
||||
|
||||
assert.deepStrictEqual(events.completions, [
|
||||
{
|
||||
step: 10,
|
||||
step: 'oauth-login',
|
||||
payload: {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
@@ -469,7 +469,7 @@ test('step 7 forwards phone login identity payload when account identifier is ph
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
events.completions.push({ step, payload });
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
@@ -529,7 +529,7 @@ test('step 7 forwards phone login identity payload when account identifier is ph
|
||||
]);
|
||||
assert.deepStrictEqual(events.completions, [
|
||||
{
|
||||
step: 7,
|
||||
step: 'oauth-login',
|
||||
payload: {
|
||||
loginVerificationRequestedAt: 123456,
|
||||
accountIdentifierType: 'phone',
|
||||
@@ -558,7 +558,7 @@ test('step 7 keeps Plus email login even when phone sms runtime exists', async (
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({
|
||||
@@ -629,7 +629,7 @@ test('step 7 keeps phone login after step 8 stores an unbound email for phone si
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({ ...phoneSignupState }),
|
||||
@@ -668,7 +668,7 @@ test('step 7 can infer phone login from an available phone signup configuration
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({
|
||||
@@ -715,7 +715,7 @@ test('step 7 can start from a manually filled signup phone without completed ste
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
events.completions.push({ step, payload });
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
@@ -755,7 +755,7 @@ test('step 7 can start from a manually filled signup phone without completed ste
|
||||
assert.equal(events.payloads[0].password, '');
|
||||
assert.deepStrictEqual(events.completions, [
|
||||
{
|
||||
step: 7,
|
||||
step: 'oauth-login',
|
||||
payload: {
|
||||
loginVerificationRequestedAt: 987654,
|
||||
accountIdentifierType: 'phone',
|
||||
@@ -783,7 +783,7 @@ test('step 7 stops immediately when management secret is missing', async () => {
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.logs.push({ message, level });
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
@@ -827,7 +827,7 @@ test('step 7 stops immediately when management secret is invalid', async () => {
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.logs.push({ message, level });
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
@@ -112,7 +112,7 @@ test('step 8 keeps phone-registered accounts on email-code flow when page is ema
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
calls.completions.push({ step, payload });
|
||||
},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
@@ -190,7 +190,7 @@ test('step 8 routes only a real phone verification page through sms helper', asy
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
calls.completions.push({ step, payload });
|
||||
},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
@@ -250,7 +250,7 @@ test('step 8 routes only a real phone verification page through sms helper', asy
|
||||
]);
|
||||
assert.deepStrictEqual(calls.completions, [
|
||||
{
|
||||
step: 8,
|
||||
step: 'fetch-login-code',
|
||||
payload: {
|
||||
phoneVerification: true,
|
||||
loginPhoneVerification: true,
|
||||
@@ -770,7 +770,7 @@ test('step 8 completes directly when auth page is already on OAuth consent page'
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
events.completeCalls.push({ step, payload });
|
||||
},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
@@ -825,7 +825,7 @@ test('step 8 completes directly when auth page is already on OAuth consent page'
|
||||
]);
|
||||
assert.deepStrictEqual(events.completeCalls, [
|
||||
{
|
||||
step: 8,
|
||||
step: 'fetch-login-code',
|
||||
payload: {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
@@ -855,7 +855,7 @@ test('step 8 retries in-place when polling fails but auth page still stays on ve
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep8VerificationPageReady: async () => {
|
||||
events.ensureCalls += 1;
|
||||
@@ -922,7 +922,7 @@ test('step 8 keeps resend cooldown timestamp across in-place retries to avoid re
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: 'user@example.com' }),
|
||||
rerunStep7ForStep8Recovery: async () => {},
|
||||
@@ -997,7 +997,7 @@ test('step 8 completes when polling fails but recovery probe shows oauth consent
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
events.completeCalls.push({ step, payload });
|
||||
},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
@@ -1046,7 +1046,7 @@ test('step 8 completes when polling fails but recovery probe shows oauth consent
|
||||
assert.equal(events.rerunStep7, 0);
|
||||
assert.deepStrictEqual(events.completeCalls, [
|
||||
{
|
||||
step: 8,
|
||||
step: 'fetch-login-code',
|
||||
payload: {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
|
||||
@@ -48,11 +48,59 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
const NODE_COMPAT_HELPERS = `
|
||||
const workflowEngine = null;
|
||||
const STEP_NODE_IDS = {
|
||||
1: 'open-chatgpt',
|
||||
2: 'submit-signup-email',
|
||||
3: 'fill-password',
|
||||
4: 'fetch-signup-code',
|
||||
5: 'fill-profile',
|
||||
6: 'wait-registration-success',
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
9: 'confirm-oauth',
|
||||
10: 'platform-verify',
|
||||
};
|
||||
const NODE_STEP_IDS = Object.fromEntries(Object.entries(STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)]));
|
||||
function getNodeIdsForState() {
|
||||
return Object.values(STEP_NODE_IDS);
|
||||
}
|
||||
function getNodeIdByStepForState(step) {
|
||||
return STEP_NODE_IDS[Number(step)] || '';
|
||||
}
|
||||
function getStepIdByNodeIdForState(nodeId) {
|
||||
return NODE_STEP_IDS[String(nodeId || '').trim()] || null;
|
||||
}
|
||||
function projectStepStatusesToNodeStatuses(stepStatuses = {}) {
|
||||
const nodeStatuses = {};
|
||||
for (const [step, status] of Object.entries(stepStatuses || {})) {
|
||||
const nodeId = getNodeIdByStepForState(step);
|
||||
if (nodeId) nodeStatuses[nodeId] = status;
|
||||
}
|
||||
return nodeStatuses;
|
||||
}
|
||||
function normalizeStatusMapForNodes(statuses = {}, state = {}) {
|
||||
return {
|
||||
...DEFAULT_STATE.nodeStatuses,
|
||||
...projectStepStatusesToNodeStatuses(state?.stepStatuses || {}),
|
||||
...(state?.nodeStatuses || {}),
|
||||
...(statuses || {}),
|
||||
};
|
||||
}
|
||||
DEFAULT_STATE.nodeStatuses = projectStepStatusesToNodeStatuses(DEFAULT_STATE.stepStatuses || {});
|
||||
`;
|
||||
|
||||
test('generic stopped record resolves to next unfinished step during execution gap', async () => {
|
||||
const bundle = [
|
||||
NODE_COMPAT_HELPERS,
|
||||
extractFunction('isStepDoneStatus'),
|
||||
extractFunction('getRunningNodeIds'),
|
||||
extractFunction('getRunningSteps'),
|
||||
extractFunction('inferStoppedRecordNode'),
|
||||
extractFunction('inferStoppedRecordStep'),
|
||||
extractFunction('resolveAccountRunRecordStatusForStop'),
|
||||
extractFunction('extractStoppedNodeFromRecordStatus'),
|
||||
extractFunction('extractStoppedStepFromRecordStatus'),
|
||||
extractFunction('resolveAccountRunRecordReasonForStop'),
|
||||
extractFunction('appendAndBroadcastAccountRunRecord'),
|
||||
@@ -103,10 +151,9 @@ return {
|
||||
10: 'pending',
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(api.inferStoppedRecordStep(state), 7);
|
||||
assert.equal(api.resolveAccountRunRecordStatusForStop('stopped', state), 'step7_stopped');
|
||||
assert.equal(api.resolveAccountRunRecordReasonForStop('step7_stopped', '流程已被用户停止。'), '步骤 7 已被用户停止。');
|
||||
assert.equal(api.resolveAccountRunRecordStatusForStop('stopped', state), 'node:oauth-login:stopped');
|
||||
assert.equal(api.resolveAccountRunRecordReasonForStop('node:oauth-login:stopped', '流程已被用户停止。'), '节点 oauth-login 已被用户停止。');
|
||||
assert.equal(
|
||||
api.resolveAccountRunRecordReasonForStop('step2_stopped', '步骤 2 已使用邮箱,流程尚未完成。'),
|
||||
'步骤 2 已停止:邮箱已设置,流程尚未完成。'
|
||||
@@ -114,19 +161,23 @@ return {
|
||||
|
||||
await api.appendAndBroadcastAccountRunRecord('stopped', state, '流程已被用户停止。');
|
||||
assert.deepStrictEqual(api.getCaptured(), {
|
||||
status: 'step7_stopped',
|
||||
status: 'node:oauth-login:stopped',
|
||||
state,
|
||||
reason: '步骤 7 已被用户停止。',
|
||||
reason: '节点 oauth-login 已被用户停止。',
|
||||
});
|
||||
});
|
||||
|
||||
test('requestStop appends a stopped record for the next unfinished step when no step is running', async () => {
|
||||
const bundle = [
|
||||
NODE_COMPAT_HELPERS,
|
||||
extractFunction('normalizeAutoRunSessionId'),
|
||||
extractFunction('clearCurrentAutoRunSessionId'),
|
||||
extractFunction('cleanupStep8NavigationListeners'),
|
||||
extractFunction('rejectPendingStep8'),
|
||||
extractFunction('isStepDoneStatus'),
|
||||
extractFunction('getRunningNodeIds'),
|
||||
extractFunction('getRunningSteps'),
|
||||
extractFunction('inferStoppedRecordNode'),
|
||||
extractFunction('inferStoppedRecordStep'),
|
||||
extractFunction('requestStop'),
|
||||
].join('\n');
|
||||
@@ -148,6 +199,7 @@ const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
||||
const DEFAULT_STATE = {
|
||||
stepStatuses: Object.fromEntries([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((step) => [step, 'pending'])),
|
||||
};
|
||||
const nodeWaiters = new Map();
|
||||
const stepWaiters = new Map();
|
||||
const appended = [];
|
||||
const logs = [];
|
||||
@@ -176,6 +228,7 @@ async function addLog(message, level) {
|
||||
}
|
||||
async function broadcastStopToContentScripts() {}
|
||||
async function markRunningStepsStopped() {}
|
||||
async function markRunningNodesStopped() {}
|
||||
async function broadcastAutoRunStatus() {}
|
||||
async function appendAndBroadcastAccountRunRecord(status, state, reason) {
|
||||
appended.push({ status, state, reason });
|
||||
|
||||
@@ -121,13 +121,13 @@ test('GoPay approve waits and retries slowly on Hubungkan linking page', () => {
|
||||
});
|
||||
|
||||
|
||||
test('background auto-run routes GoPay restart sentinel back to step 6', () => {
|
||||
test('background auto-run routes GoPay restart sentinel back to checkout-create node', () => {
|
||||
const backgroundSource = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(backgroundSource, /isGoPayCheckoutRestartRequiredFailure/);
|
||||
assert.match(backgroundSource, /GOPAY_RESTART_FROM_STEP6::/);
|
||||
assert.match(backgroundSource, /step === 8 && isGoPayCheckoutRestartRequiredFailure\(err\)/);
|
||||
assert.match(backgroundSource, /step = 6/);
|
||||
assert.match(backgroundSource, /invalidateDownstreamAfterStepRestart\(5/);
|
||||
assert.match(backgroundSource, /nodeId === 'paypal-approve' && isGoPayCheckoutRestartRequiredFailure\(err\)/);
|
||||
assert.match(backgroundSource, /getNodeIndex\(await getState\(\), 'plus-checkout-create'\)/);
|
||||
assert.match(backgroundSource, /invalidateDownstreamAfterAutoRunNodeRestart\(getPreviousNodeId\('plus-checkout-create'/);
|
||||
});
|
||||
|
||||
test('GoPay approve gives PIN precedence over OTP on ambiguous second PIN pages', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
@@ -56,7 +56,7 @@ function createExecutor({
|
||||
},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
events.completed.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
@@ -259,7 +259,7 @@ test('PayPal approve keeps original combined email and password login path', asy
|
||||
});
|
||||
|
||||
assert.equal(events.submittedPayloads.length, 1);
|
||||
assert.deepEqual(events.completed.map((item) => item.step), [8]);
|
||||
assert.deepEqual(events.completed.map((item) => item.step), ['paypal-approve']);
|
||||
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), true);
|
||||
});
|
||||
|
||||
@@ -404,7 +404,7 @@ test('PayPal approve discovers an already open unregistered PayPal tab', async (
|
||||
|
||||
assert.deepEqual(events.updatedTabs, [{ tabId: 7, updateInfo: { active: true } }]);
|
||||
assert.equal(events.logs.some(({ message }) => /发现 PayPal 页面/.test(message)), true);
|
||||
assert.deepEqual(events.completed.map((item) => item.step), [8]);
|
||||
assert.deepEqual(events.completed.map((item) => item.step), ['paypal-approve']);
|
||||
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), true);
|
||||
});
|
||||
|
||||
@@ -440,7 +440,7 @@ test('PayPal approve discovers PayPal tabs through the locked automation window
|
||||
|
||||
assert.deepEqual(queries, [{}]);
|
||||
assert.deepEqual(events.updatedTabs, [{ tabId: 9, updateInfo: { active: true } }]);
|
||||
assert.deepEqual(events.completed.map((item) => item.step), [8]);
|
||||
assert.deepEqual(events.completed.map((item) => item.step), ['paypal-approve']);
|
||||
});
|
||||
|
||||
test('PayPal approve auto-detects split email then password pages', async () => {
|
||||
@@ -464,7 +464,7 @@ test('PayPal approve auto-detects split email then password pages', async () =>
|
||||
});
|
||||
|
||||
assert.equal(events.submittedPayloads.length, 2);
|
||||
assert.deepEqual(events.completed.map((item) => item.step), [8]);
|
||||
assert.deepEqual(events.completed.map((item) => item.step), ['paypal-approve']);
|
||||
assert.equal(events.logs.some(({ message }) => /识别到密码页/.test(message)), true);
|
||||
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), true);
|
||||
});
|
||||
@@ -490,6 +490,6 @@ test('PayPal approve finishes when login redirects away from PayPal', async () =
|
||||
});
|
||||
|
||||
assert.equal(events.submittedPayloads.length, 1);
|
||||
assert.deepEqual(events.completed.map((item) => item.step), [8]);
|
||||
assert.deepEqual(events.completed.map((item) => item.step), ['paypal-approve']);
|
||||
assert.equal(events.messages.includes('PAYPAL_CLICK_APPROVE'), false);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
@@ -153,7 +153,7 @@ function createExecutorHarness({
|
||||
getAllFrames: async () => frames,
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => events.completed.push({ step, payload }),
|
||||
completeNodeFromBackground: async (step, payload) => events.completed.push({ step, payload }),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId) => events.ensuredTabs.push({ source, tabId }),
|
||||
fetch: fetchImpl,
|
||||
generateRandomName: () => ({ firstName: 'Ada', lastName: 'Lovelace' }),
|
||||
@@ -237,7 +237,7 @@ test('Plus checkout billing uses the current checkout tab when step 6 did not re
|
||||
assert.equal(events.messages.some((entry) => entry.message.type === 'PLUS_CHECKOUT_SELECT_PAYPAL' && entry.frameId === 0), true);
|
||||
assert.equal(events.messages.some((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS' && entry.frameId === 0), true);
|
||||
assert.equal(events.messages.some((entry) => entry.message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE' && entry.frameId === 0), true);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
assert.equal(events.states.some((updates) => updates.plusCheckoutTabId === checkoutTab.id), true);
|
||||
assert.equal(events.logs.some((entry) => /当前已在 Plus Checkout 页面/.test(entry.message)), true);
|
||||
});
|
||||
@@ -297,7 +297,7 @@ test('Plus checkout billing sends the billing command to the iframe that contain
|
||||
assert.equal(fillMessage.frameId, 8);
|
||||
assert.equal(subscribeMessage.frameId, 0);
|
||||
assert.equal(events.logs.some((entry) => /checkout iframe/.test(entry.message)), true);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
});
|
||||
|
||||
test('Plus checkout billing uses proxy exit country for GoPay address when available', async () => {
|
||||
@@ -597,7 +597,7 @@ test('Plus checkout billing selects GoPay and waits for a GoPay redirect', async
|
||||
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'ID');
|
||||
assert.equal(subscribeMessage.message.payload.paymentMethod, 'gopay');
|
||||
assert.equal(checkoutTab.url, 'https://gopay.co.id/payment/session');
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
});
|
||||
|
||||
test('Plus checkout billing still inspects a frame when ping readiness is stale', async () => {
|
||||
@@ -625,7 +625,7 @@ test('Plus checkout billing still inspects a frame when ping readiness is stale'
|
||||
|
||||
const selectMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_SELECT_PAYPAL');
|
||||
assert.equal(selectMessage.frameId, 0);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
});
|
||||
|
||||
test('Plus checkout billing uses the autocomplete iframe for address suggestions when Stripe splits it out', async () => {
|
||||
@@ -655,7 +655,7 @@ test('Plus checkout billing uses the autocomplete iframe for address suggestions
|
||||
assert.equal(ensureAddressMessage.frameId, 8);
|
||||
assert.equal(combinedFillMessage, undefined);
|
||||
assert.equal(events.logs.some((entry) => /Google 地址推荐/.test(entry.message)), true);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
});
|
||||
|
||||
test('Plus checkout billing skips Google autocomplete when meiguodizhi returns a complete address', async () => {
|
||||
@@ -713,7 +713,7 @@ test('Plus checkout billing skips Google autocomplete when meiguodizhi returns a
|
||||
path: '/de-address',
|
||||
method: 'refresh',
|
||||
});
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
});
|
||||
|
||||
test('Plus checkout billing uses the detected checkout country before choosing an address seed', async () => {
|
||||
@@ -940,7 +940,7 @@ test('GPC billing polls queue task, submits WhatsApp OTP then PIN, and waits unt
|
||||
assert.equal(events.states.some((state) => state.gopayHelperTaskId === 'task_123' && state.gopayHelperTaskStatus === 'completed'), true);
|
||||
assert.equal(events.logs.some((entry) => entry.message === '步骤 7:GPC 任务状态:等待 WhatsApp OTP'), true);
|
||||
assert.equal(events.logs.some((entry) => /whatsapp_otp_wait/.test(entry.message)), false);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
|
||||
assert.ok(events.sleeps.includes(3000));
|
||||
});
|
||||
@@ -995,7 +995,7 @@ test('GPC billing auto mode only polls until completed without OTP or PIN submis
|
||||
assert.equal(events.logs.some((entry) => /auto_otp_wait/.test(entry.message)), false);
|
||||
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
|
||||
assert.equal(events.states.some((state) => state.gopayHelperTaskId === 'task_auto' && state.gopayHelperTaskStatus === 'completed'), true);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
|
||||
});
|
||||
|
||||
@@ -1266,7 +1266,7 @@ test('GPC billing reads SMS OTP from local helper for sms_otp_wait', async () =>
|
||||
assert.deepEqual(JSON.parse(fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_sms/otp')).options.body), {
|
||||
otp: '654321',
|
||||
});
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
});
|
||||
|
||||
test('GPC billing can read WhatsApp OTP from local helper when enabled', async () => {
|
||||
@@ -1346,7 +1346,7 @@ test('GPC billing can read WhatsApp OTP from local helper when enabled', async (
|
||||
assert.deepEqual(JSON.parse(fetchCalls.find((call) => call.url.endsWith('/api/gp/tasks/task_wa/otp')).options.body), {
|
||||
otp: '765432',
|
||||
});
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
});
|
||||
|
||||
|
||||
@@ -1481,7 +1481,7 @@ test('GPC billing helper mode requests newer OTP after invalid OTP error', async
|
||||
assert.equal(helperUrls[1].searchParams.get('consume'), '1');
|
||||
assert.ok(Number(helperUrls[1].searchParams.get('after_ms')) > 1710000000000);
|
||||
assert.equal(events.logs.some((entry) => /OTP 校验失败/.test(entry.message)), true);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
});
|
||||
|
||||
test('GPC billing manual OTP wrong input opens next dialog only after previous one closes', async () => {
|
||||
@@ -1558,7 +1558,7 @@ test('GPC billing manual OTP wrong input opens next dialog only after previous o
|
||||
.filter((call) => call.url.endsWith('/api/gp/tasks/task_manual_retry/otp'))
|
||||
.map((call) => JSON.parse(call.options.body));
|
||||
assert.deepEqual(otpBodies, [{ otp: '111111' }, { otp: '222222' }]);
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
assert.equal(events.completed[0].step, 'plus-checkout-billing');
|
||||
});
|
||||
|
||||
test('GPC billing manual OTP cancel stops task and ends current round', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
@@ -204,7 +204,7 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page'
|
||||
},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
events.push({ type: 'complete', step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {
|
||||
@@ -267,7 +267,7 @@ test('GoPay plus checkout create forwards gopay payment method to the checkout c
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
registerTab: async () => {},
|
||||
sendTabMessageUntilStopped: async (_tabId, _source, message) => {
|
||||
@@ -370,7 +370,7 @@ test('GPC manual checkout injects Plus script before reading ChatGPT session tok
|
||||
remove: async (tabId) => events.push({ type: 'tab-remove', tabId }),
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
|
||||
completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => events.push({ type: 'ready', source, tabId, options }),
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
@@ -438,7 +438,7 @@ test('GPC manual checkout injects Plus script before reading ChatGPT session tok
|
||||
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperRemoteStage, 'checkout_start');
|
||||
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperReferenceId, '');
|
||||
assert.ok(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperOrderCreatedAt > 0);
|
||||
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
|
||||
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
|
||||
assert.equal(events.find((event) => event.type === 'complete')?.payload?.plusCheckoutSource, 'gpc-helper');
|
||||
});
|
||||
|
||||
@@ -456,7 +456,7 @@ test('GPC auto checkout only sends access token and API Key', async () => {
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
|
||||
completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
@@ -507,7 +507,7 @@ test('GPC auto checkout only sends access token and API Key', async () => {
|
||||
assert.equal(statePayload.gopayHelperTaskId, 'task_auto');
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(statePayload, 'gopayHelperPhoneMode'), false);
|
||||
assert.equal(statePayload.gopayHelperTaskStatus, 'queued');
|
||||
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
|
||||
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
|
||||
});
|
||||
|
||||
test('GPC auto checkout keeps running when balance payload omits auto mode permission', async () => {
|
||||
@@ -523,7 +523,7 @@ test('GPC auto checkout keeps running when balance payload omits auto mode permi
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
|
||||
completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
@@ -564,7 +564,7 @@ test('GPC auto checkout keeps running when balance payload omits auto mode permi
|
||||
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
|
||||
assert.equal(statePayload.gopayHelperTaskId, 'task_auto_unknown_permission');
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(statePayload, 'gopayHelperPhoneMode'), false);
|
||||
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
|
||||
assert.equal(events.find((event) => event.type === 'complete')?.step, 'plus-checkout-create');
|
||||
});
|
||||
|
||||
test('GPC auto checkout blocks API Keys without auto mode permission', async () => {
|
||||
@@ -579,7 +579,7 @@ test('GPC auto checkout blocks API Keys without auto mode permission', async ()
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
@@ -623,7 +623,7 @@ test('GPC checkout blocks exhausted API Keys before creating task', async () =>
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
@@ -668,7 +668,7 @@ test('GPC checkout forwards selected SMS OTP channel', async () => {
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
@@ -720,7 +720,7 @@ test('GPC checkout surfaces unified queue API errors', async () => {
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
@@ -780,7 +780,7 @@ test('GPC checkout does not fall back to browser GoPay phone fields', async () =
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async () => {
|
||||
throw new Error('should not call helper API without helper phone');
|
||||
@@ -821,7 +821,7 @@ test('GPC checkout rejects missing API Key before calling helper API', async ()
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
fetch: async () => {
|
||||
throw new Error('should not call helper API without API Key');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
@@ -12,7 +12,7 @@ test('Plus return confirm waits a fixed 20 seconds after return URL is detected'
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.push({ type: 'log', message, level });
|
||||
},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completeNodeFromBackground: async (step, payload) => {
|
||||
events.push({ type: 'complete', step, payload });
|
||||
},
|
||||
getTabId: async (source) => (source === 'paypal-flow' ? 77 : null),
|
||||
@@ -46,7 +46,7 @@ test('Plus return confirm waits a fixed 20 seconds after return URL is detected'
|
||||
events.find((event) => event.type === 'complete'),
|
||||
{
|
||||
type: 'complete',
|
||||
step: 9,
|
||||
step: 'plus-checkout-return',
|
||||
payload: { plusReturnUrl: 'https://chatgpt.com/backend-api/payments/success' },
|
||||
}
|
||||
);
|
||||
|
||||
@@ -132,7 +132,10 @@ test('sidepanel settings refresh preserves rendered step progress', () => {
|
||||
|
||||
const bundle = [
|
||||
extractFunction('isDoneStatus'),
|
||||
extractFunction('getNodeStatuses'),
|
||||
extractFunction('getStepStatuses'),
|
||||
extractFunction('escapeCssValue'),
|
||||
extractFunction('renderSingleNodeStatus'),
|
||||
extractFunction('renderSingleStepStatus'),
|
||||
extractFunction('renderStepStatuses'),
|
||||
extractFunction('updateProgressCounter'),
|
||||
@@ -148,13 +151,24 @@ const STATUS_ICONS = {
|
||||
manual_completed: 'M',
|
||||
skipped: 'K',
|
||||
};
|
||||
let latestState = { stepStatuses: { 1: 'completed', 2: 'running', 3: 'pending' } };
|
||||
let latestState = { nodeStatuses: { 'open-chatgpt': 'completed', 'submit-signup-email': 'running', 'fill-password': 'pending' } };
|
||||
let NODE_IDS = ['open-chatgpt', 'submit-signup-email', 'fill-password'];
|
||||
let NODE_DEFAULT_STATUSES = { 'open-chatgpt': 'pending', 'submit-signup-email': 'pending', 'fill-password': 'pending' };
|
||||
let STEP_IDS = [1, 2, 3];
|
||||
let STEP_DEFAULT_STATUSES = { 1: 'pending', 2: 'pending', 3: 'pending' };
|
||||
function getStepIdByNodeIdForCurrentMode(nodeId) {
|
||||
return { 'open-chatgpt': 1, 'submit-signup-email': 2, 'fill-password': 3 }[nodeId] || 0;
|
||||
}
|
||||
const rows = new Map(STEP_IDS.map((step) => [step, { className: 'step-row' }]));
|
||||
const statusEls = new Map(STEP_IDS.map((step) => [step, { textContent: '' }]));
|
||||
const nodeRows = new Map(NODE_IDS.map((nodeId) => [nodeId, rows.get(getStepIdByNodeIdForCurrentMode(nodeId))]));
|
||||
const nodeStatusEls = new Map(NODE_IDS.map((nodeId) => [nodeId, statusEls.get(getStepIdByNodeIdForCurrentMode(nodeId))]));
|
||||
const document = {
|
||||
querySelector(selector) {
|
||||
const nodeMatch = selector.match(/data-node-id="([^"]+)"/);
|
||||
if (nodeMatch) {
|
||||
return selector.includes('step-status') ? nodeStatusEls.get(nodeMatch[1]) : nodeRows.get(nodeMatch[1]);
|
||||
}
|
||||
const match = selector.match(/data-step="(\\d+)"/);
|
||||
const step = match ? Number(match[1]) : 0;
|
||||
return selector.includes('step-status') ? statusEls.get(step) : rows.get(step);
|
||||
|
||||
@@ -200,7 +200,7 @@ test('sidepanel source wires runtime signup phone field to background sync messa
|
||||
assert.match(sidepanelSource, /final \? 'SAVE_SIGNUP_PHONE' : 'SET_SIGNUP_PHONE_STATE'/);
|
||||
assert.match(sidepanelSource, /message\.payload\.signupPhoneNumber !== undefined/);
|
||||
assert.match(sidepanelSource, /await persistSignupPhoneInputForAction\(\);\s*await saveSettings/);
|
||||
assert.match(sidepanelSource, /if \(shouldExecuteStep3WithSignupPhoneIdentity\(latestState\)\)[\s\S]*payload: \{ step \}/);
|
||||
assert.match(sidepanelSource, /if \(shouldExecuteStep3WithSignupPhoneIdentity\(latestState\)\)[\s\S]*type:\s*'EXECUTE_NODE'[\s\S]*payload: \{ nodeId \}/);
|
||||
assert.match(sidepanelSource, /async function handleSkipStep\(step\)[\s\S]*await persistCurrentSettingsForAction\(\);/);
|
||||
assert.match(sidepanelSource, /inputSignupPhone\.addEventListener\('input'[\s\S]*signupPhoneInputDirty = true/);
|
||||
});
|
||||
|
||||
@@ -53,4 +53,7 @@ test('shared source registry exposes canonical source, alias, detection, and rea
|
||||
assert.equal(registry.shouldReportReadyForFrame('mail-163', true), false);
|
||||
assert.equal(registry.shouldReportReadyForFrame('unknown-source', false), false);
|
||||
assert.equal(registry.getCleanupOwnerSource('oauth-localhost-callback'), 'openai-auth');
|
||||
assert.equal(registry.driverAcceptsCommand('openai-auth', 'submit-signup-email'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('content/platform-panel', 'platform-verify'), true);
|
||||
assert.equal(registry.driverAcceptsCommand('openai-auth', 'platform-verify'), false);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
@@ -240,7 +240,7 @@ test('step 8 escalates to rerun step 7 after too many local retry_without_step7
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep8VerificationPageReady: async () => {
|
||||
calls.ensureReady += 1;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const assert = require('assert');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
|
||||
const helperSource = fs.readFileSync('background.js', 'utf8');
|
||||
@@ -78,7 +78,7 @@ let autoRunAttemptRun = 4;
|
||||
let autoRunSessionId = 99;
|
||||
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
||||
const DEFAULT_STATE = {
|
||||
stepStatuses: Object.fromEntries([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((step) => [step, 'pending'])),
|
||||
nodeStatuses: {},
|
||||
};
|
||||
const STEP8_CLICK_RETRY_DELAY_MS = 500;
|
||||
const STEP8_MAX_ROUNDS = 5;
|
||||
@@ -134,6 +134,7 @@ const chrome = {
|
||||
},
|
||||
};
|
||||
|
||||
const nodeWaiters = new Map();
|
||||
const stepWaiters = new Map();
|
||||
let resumeWaiter = null;
|
||||
|
||||
@@ -141,6 +142,13 @@ function cancelPendingCommands() {}
|
||||
function abortActiveIcloudRequests() {}
|
||||
async function addLog() {}
|
||||
async function broadcastStopToContentScripts() {}
|
||||
function getRunningNodeIds() {
|
||||
return [];
|
||||
}
|
||||
function inferStoppedRecordNode() {
|
||||
return '';
|
||||
}
|
||||
async function markRunningNodesStopped() {}
|
||||
async function markRunningStepsStopped() {}
|
||||
async function broadcastAutoRunStatus() {}
|
||||
async function appendAndBroadcastAccountRunRecord() {}
|
||||
@@ -156,7 +164,7 @@ function isAutoRunScheduledState() {
|
||||
function getStep8CallbackUrlFromNavigation() { return ''; }
|
||||
function getStep8CallbackUrlFromTabUpdate() { return ''; }
|
||||
function getStep8EffectLabel() { return 'no_effect'; }
|
||||
async function completeStepFromBackground() {}
|
||||
async function completeNodeFromBackground() {}
|
||||
async function getTabId() {
|
||||
return await new Promise((resolve) => {
|
||||
resolveTabId = resolve;
|
||||
@@ -218,7 +226,7 @@ const executor = self.MultiPageBackgroundStep9.createStep9Executor({
|
||||
chrome,
|
||||
cleanupStep8NavigationListeners,
|
||||
clickWithDebugger,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
ensureStep8SignupPageReady,
|
||||
getStep8CallbackUrlFromNavigation,
|
||||
getStep8CallbackUrlFromTabUpdate,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
@@ -102,8 +102,8 @@ function getWebNavCommittedListener() { return webNavCommittedListener; }
|
||||
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
|
||||
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
|
||||
function setStep8PendingReject(handler) { step8PendingReject = handler; }
|
||||
async function completeStepFromBackground(step, payload) {
|
||||
completePayload = { step, payload };
|
||||
async function completeNodeFromBackground(nodeId, payload) {
|
||||
completePayload = { nodeId, payload };
|
||||
}
|
||||
|
||||
const STEP8_CLICK_RETRY_DELAY_MS = 1;
|
||||
@@ -120,7 +120,7 @@ const executor = self.MultiPageBackgroundStep9.createStep9Executor({
|
||||
chrome,
|
||||
cleanupStep8NavigationListeners,
|
||||
clickWithDebugger,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
ensureStep8SignupPageReady,
|
||||
getOAuthFlowRemainingMs,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
@@ -176,7 +176,7 @@ return {
|
||||
assert.equal(snapshot.cleanupCalls >= 1, true);
|
||||
assert.equal(snapshot.hasPendingReject, false);
|
||||
assert.deepEqual(snapshot.completePayload, {
|
||||
step: 9,
|
||||
nodeId: 'confirm-oauth',
|
||||
payload: {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
|
||||
},
|
||||
@@ -279,8 +279,8 @@ function getWebNavCommittedListener() { return webNavCommittedListener; }
|
||||
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
|
||||
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
|
||||
function setStep8PendingReject() {}
|
||||
async function completeStepFromBackground(step, payload) {
|
||||
completePayload = { step, payload };
|
||||
async function completeNodeFromBackground(nodeId, payload) {
|
||||
completePayload = { nodeId, payload };
|
||||
}
|
||||
|
||||
const STEP8_CLICK_RETRY_DELAY_MS = 1;
|
||||
@@ -297,7 +297,7 @@ const executor = self.MultiPageBackgroundStep9.createStep9Executor({
|
||||
chrome,
|
||||
cleanupStep8NavigationListeners,
|
||||
clickWithDebugger,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
ensureStep8SignupPageReady,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getStep8CallbackUrlFromNavigation,
|
||||
@@ -348,7 +348,7 @@ return {
|
||||
assert.equal(snapshot.deferCalls >= 1, true);
|
||||
assert.equal(snapshot.cleanupCalls >= 1, true);
|
||||
assert.deepEqual(snapshot.completePayload, {
|
||||
step: 12,
|
||||
nodeId: 'confirm-oauth',
|
||||
payload: {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const assert = require('assert');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
|
||||
const step9ModuleSource = fs.readFileSync('background/steps/confirm-oauth.js', 'utf8');
|
||||
@@ -148,8 +148,8 @@ function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listen
|
||||
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
|
||||
function setStep8PendingReject(handler) { step8PendingReject = handler; }
|
||||
|
||||
async function completeStepFromBackground(step, payload) {
|
||||
completePayload = { step, payload };
|
||||
async function completeNodeFromBackground(nodeId, payload) {
|
||||
completePayload = { nodeId, payload };
|
||||
}
|
||||
|
||||
const STEP8_CLICK_RETRY_DELAY_MS = 200;
|
||||
@@ -167,7 +167,7 @@ const executor = self.MultiPageBackgroundStep9.createStep9Executor({
|
||||
chrome,
|
||||
cleanupStep8NavigationListeners,
|
||||
clickWithDebugger,
|
||||
completeStepFromBackground,
|
||||
completeNodeFromBackground,
|
||||
ensureStep8SignupPageReady,
|
||||
getOAuthFlowRemainingMs,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
@@ -226,7 +226,7 @@ return {
|
||||
assert.strictEqual(snapshot.cleanupCalls >= 1, true, 'step9 should cleanup navigation listeners');
|
||||
assert.strictEqual(snapshot.hasPendingReject, false, 'step9 should clear pending reject after completion');
|
||||
assert.deepStrictEqual(snapshot.completePayload, {
|
||||
step: 9,
|
||||
nodeId: 'confirm-oauth',
|
||||
payload: {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
|
||||
},
|
||||
|
||||
@@ -51,8 +51,8 @@ function createSub2ApiPanelContext(fetchCalls = []) {
|
||||
removeItem(key) { storage.delete(`session:${key}`); },
|
||||
},
|
||||
log() {},
|
||||
reportComplete(step, payload) {
|
||||
context.completed.push({ step, payload });
|
||||
reportComplete(nodeId, payload) {
|
||||
context.completed.push({ nodeId, payload });
|
||||
},
|
||||
reportReady() {},
|
||||
reportError() {},
|
||||
@@ -177,7 +177,8 @@ test('SUB2API step 10 uses the same proxy for code exchange and account creation
|
||||
assert.equal(exchangeCall.body.proxy_id, 7);
|
||||
assert.equal(createCall.body.proxy_id, 7);
|
||||
assert.equal(createCall.body.group_ids[0], 5);
|
||||
assert.equal(context.completed[0].step, 10);
|
||||
assert.equal(context.completed[0].nodeId, 'platform-verify');
|
||||
assert.equal(context.completed[0].payload.visibleStep, 10);
|
||||
});
|
||||
|
||||
test('SUB2API panel accepts Plus platform verify step 13', async () => {
|
||||
@@ -202,7 +203,8 @@ test('SUB2API panel accepts Plus platform verify step 13', async () => {
|
||||
|
||||
assert.equal(exchangeCall.body.code, 'callback-code');
|
||||
assert.equal(createCall.body.group_ids[0], 5);
|
||||
assert.equal(context.completed[0].step, 13);
|
||||
assert.equal(context.completed[0].nodeId, 'platform-verify');
|
||||
assert.equal(context.completed[0].payload.visibleStep, 13);
|
||||
});
|
||||
|
||||
test('SUB2API step 1 omits proxy_id when default proxy is empty', async () => {
|
||||
|
||||
@@ -1,13 +1,58 @@
|
||||
const test = require('node:test');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background/verification-flow.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundVerificationFlow;`)(globalScope);
|
||||
const rawCreateVerificationFlowHelpers = api.createVerificationFlowHelpers.bind(api);
|
||||
|
||||
const TEST_STEP_NODE_IDS = Object.freeze({
|
||||
4: 'fetch-signup-code',
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
11: 'fetch-login-code',
|
||||
});
|
||||
|
||||
const TEST_NODE_STEP_IDS = Object.fromEntries(
|
||||
Object.entries(TEST_STEP_NODE_IDS).map(([step, nodeId]) => [nodeId, Number(step)])
|
||||
);
|
||||
|
||||
function getTestNodeIdByStepForState(step) {
|
||||
return TEST_STEP_NODE_IDS[Number(step)] || '';
|
||||
}
|
||||
|
||||
function getTestStepIdByNodeId(nodeId) {
|
||||
return TEST_NODE_STEP_IDS[String(nodeId || '').trim()] || null;
|
||||
}
|
||||
|
||||
function normalizeVerificationFlowTestOverrides(overrides = {}) {
|
||||
const normalized = { ...overrides };
|
||||
if (
|
||||
typeof normalized.completeNodeFromBackground !== 'function'
|
||||
&& typeof normalized.completeStepFromBackground === 'function'
|
||||
) {
|
||||
const completeStepFromBackground = normalized.completeStepFromBackground;
|
||||
normalized.completeNodeFromBackground = async (nodeId, payload) => (
|
||||
completeStepFromBackground(getTestStepIdByNodeId(nodeId), payload)
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof normalized.setNodeStatus !== 'function'
|
||||
&& typeof normalized.setStepStatus === 'function'
|
||||
) {
|
||||
const setStepStatus = normalized.setStepStatus;
|
||||
normalized.setNodeStatus = async (nodeId, status) => (
|
||||
setStepStatus(getTestStepIdByNodeId(nodeId), status)
|
||||
);
|
||||
}
|
||||
delete normalized.completeStepFromBackground;
|
||||
delete normalized.setStepStatus;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function createVerificationFlowTestHelpers(overrides = {}) {
|
||||
return api.createVerificationFlowHelpers({
|
||||
return rawCreateVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
buildVerificationPollPayload: null,
|
||||
chrome: {
|
||||
@@ -18,8 +63,9 @@ function createVerificationFlowTestHelpers(overrides = {}) {
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
CLOUD_MAIL_PROVIDER: 'cloudmail',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getNodeIdByStepForState: getTestNodeIdByStepForState,
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
getState: async () => ({}),
|
||||
@@ -36,14 +82,16 @@ function createVerificationFlowTestHelpers(overrides = {}) {
|
||||
sendToContentScript: async () => ({}),
|
||||
sendToMailContentScriptResilient: async () => ({}),
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
setNodeStatus: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
...overrides,
|
||||
...normalizeVerificationFlowTestOverrides(overrides),
|
||||
});
|
||||
}
|
||||
|
||||
api.createVerificationFlowHelpers = createVerificationFlowTestHelpers;
|
||||
|
||||
test('verification flow prefers injected verification poll payload builder when provided', () => {
|
||||
const helpers = createVerificationFlowTestHelpers({
|
||||
buildVerificationPollPayload: (step, state, overrides = {}) => ({
|
||||
@@ -85,7 +133,7 @@ test('verification flow keeps 2925 polling cadence in the default payload', () =
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -184,7 +232,7 @@ test('verification flow only enables 2925 target email matching in receive mode'
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -233,7 +281,7 @@ test('verification flow runs beforeSubmit hook before filling the code', async (
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (_step, payload) => {
|
||||
completeNodeFromBackground: async (_step, payload) => {
|
||||
events.push(['complete', payload.code]);
|
||||
},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
@@ -299,7 +347,7 @@ test('verification flow skips 2925 mailbox preclear when using a fixed login mai
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -360,7 +408,7 @@ test('verification flow skips 2925 mailbox preclear when using a fixed signup ma
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -436,7 +484,7 @@ test('verification flow closes the tracked iCloud mail tab after a successful ve
|
||||
throw new Error('should not use family cleanup when tracked tab exists');
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -498,7 +546,7 @@ test('verification flow completes step 8 and flags phone verification when add-p
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (_step, payload) => {
|
||||
completeNodeFromBackground: async (_step, payload) => {
|
||||
events.push(['complete', payload.code]);
|
||||
},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
@@ -568,7 +616,7 @@ test('verification flow keeps step 8 successful when code submit transport fails
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (_step, payload) => {
|
||||
completeNodeFromBackground: async (_step, payload) => {
|
||||
events.push(['complete', payload.code]);
|
||||
},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
@@ -642,7 +690,7 @@ test('verification flow treats manual step 8 add-phone confirmation as the same
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {
|
||||
completeNodeFromBackground: async () => {
|
||||
throw new Error('should not complete step 8');
|
||||
},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({
|
||||
@@ -689,7 +737,7 @@ test('verification flow caps mail polling timeout to the remaining oauth budget'
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -753,7 +801,7 @@ test('verification flow keeps mail polling response timeout above minimum floor'
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -816,7 +864,7 @@ test('verification flow keeps 2925 mailbox polling at 15 refresh attempts even w
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -881,7 +929,7 @@ test('verification flow can run a 2/3/15 2925 resend polling plan', async () =>
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -953,7 +1001,7 @@ test('verification flow uses full 2925 polling window after a rejected login cod
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -1026,7 +1074,7 @@ test('verification flow keeps Hotmail request timestamp filtering on the first p
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 87654,
|
||||
@@ -1081,7 +1129,7 @@ test('verification flow keeps fixed filter timestamp after step 4 resend', async
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: (_step, state) => Math.max(0, Number(state.signupVerificationRequestedAt || 0) - 15000),
|
||||
@@ -1147,7 +1195,7 @@ test('verification flow uses configured signup resend count for step 4', async (
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -1206,7 +1254,7 @@ test('verification flow uses configured login resend count for step 8', async ()
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -1265,8 +1313,8 @@ test('verification flow can complete Plus visible login-code step with shared st
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completed.push({ step, payload });
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completed.push({ nodeId, payload });
|
||||
},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
@@ -1310,7 +1358,7 @@ test('verification flow can complete Plus visible login-code step with shared st
|
||||
assert.deepStrictEqual(fillMessages.map((message) => message.step), [8]);
|
||||
assert.deepStrictEqual(completed, [
|
||||
{
|
||||
step: 11,
|
||||
nodeId: 'fetch-login-code',
|
||||
payload: {
|
||||
emailTimestamp: 456,
|
||||
code: '654321',
|
||||
@@ -1328,7 +1376,7 @@ test('verification flow waits during resend cooldown instead of tight-looping',
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -1389,7 +1437,7 @@ test('verification flow clicks resend before waiting for the next LuckMail /code
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -1459,7 +1507,7 @@ test('verification flow notifies onResendRequestedAt when resend is triggered',
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -1532,7 +1580,7 @@ test('verification flow uses resilient signup-page transport when submitting ver
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -1582,7 +1630,7 @@ test('verification flow does not replay step 8 code submit after transient auth-
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -1647,7 +1695,7 @@ test('verification flow requests a new code immediately after Cloudflare Temp Em
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (_step, payload) => {
|
||||
completeNodeFromBackground: async (_step, payload) => {
|
||||
completed.push(payload);
|
||||
},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
@@ -1726,7 +1774,7 @@ test('verification flow forwards optional signup profile payload when submitting
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -1787,8 +1835,8 @@ test('verification flow keeps combined signup profile skip reason when completin
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completed.push({ step, payload });
|
||||
completeNodeFromBackground: async (nodeId, payload) => {
|
||||
completed.push({ nodeId, payload });
|
||||
},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
@@ -1843,7 +1891,7 @@ test('verification flow keeps combined signup profile skip reason when completin
|
||||
assert.equal(resilientCalls[0].payload.signupProfile.firstName, 'Ada');
|
||||
assert.deepStrictEqual(completed, [
|
||||
{
|
||||
step: 4,
|
||||
nodeId: 'fetch-signup-code',
|
||||
payload: {
|
||||
emailTimestamp: 123,
|
||||
code: '654321',
|
||||
@@ -1869,7 +1917,7 @@ test('verification flow treats retryable submit transport failure as success whe
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -1919,7 +1967,7 @@ test('verification flow avoids resend storms when iCloud polling keeps hitting t
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -1982,7 +2030,7 @@ test('verification flow stops iCloud poll-only loop after repeated no-code round
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
@@ -2043,7 +2091,7 @@ test('verification flow derives iCloud polling response timeout from the configu
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const assert = require('assert');
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
|
||||
const helperSource = fs.readFileSync('background.js', 'utf8');
|
||||
@@ -97,7 +97,7 @@ const helpers = self.MultiPageBackgroundVerificationFlow.createVerificationFlowH
|
||||
addLog,
|
||||
chrome: {},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig,
|
||||
getHotmailVerificationRequestTimestamp: () => 123,
|
||||
@@ -164,7 +164,7 @@ const helpers = self.MultiPageBackgroundVerificationFlow.createVerificationFlowH
|
||||
addLog,
|
||||
chrome,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
completeStepFromBackground: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 123,
|
||||
|
||||
Reference in New Issue
Block a user