Refactor workflow auto-run to node graph
This commit is contained in:
@@ -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();
|
||||
|
||||
+53
-100
@@ -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,47 +274,64 @@
|
||||
...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,
|
||||
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, 'activeRunId')
|
||||
? state.activeRunId
|
||||
: baseRuntimeState.activeRunId
|
||||
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,
|
||||
nodeStatuses,
|
||||
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,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user