Files
FlowPilot/background/account-run-history.js
T

604 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function attachBackgroundAccountRunHistory(root, factory) {
root.MultiPageBackgroundAccountRunHistory = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundAccountRunHistoryModule() {
function createAccountRunHistoryHelpers(deps = {}) {
const {
ACCOUNT_RUN_HISTORY_STORAGE_KEY = 'accountRunHistory',
addLog,
buildLocalHelperEndpoint,
chrome,
getErrorMessage,
getState,
normalizeAccountRunHistoryHelperBaseUrl,
} = deps;
function normalizeTimestamp(value) {
const timestamp = Date.parse(String(value || ''));
return Number.isFinite(timestamp) ? timestamp : 0;
}
function normalizeRetryCount(value) {
const count = Math.floor(Number(value) || 0);
return count > 0 ? count : 0;
}
function normalizeFinalStatus(status = '') {
const normalized = String(status || '').trim().toLowerCase();
if (!normalized) {
return '';
}
if (normalized === 'success') {
return 'success';
}
if (normalized === 'failed' || /_failed$/.test(normalized)) {
return 'failed';
}
if (normalized === 'stopped' || /_stopped$/.test(normalized)) {
return 'stopped';
}
return '';
}
function extractRecordStep(status = '', detail = '') {
const normalizedStatus = String(status || '').trim().toLowerCase();
const statusMatch = normalizedStatus.match(/^step(\d+)_(?:failed|stopped)$/);
if (statusMatch) {
const step = Number(statusMatch[1]);
return Number.isInteger(step) && step > 0 ? step : null;
}
const text = String(detail || '').trim();
const detailMatch = text.match(/(?:Step\s+(\d+)|步骤\s*(\d+))/i);
if (!detailMatch) {
return null;
}
const step = Number(detailMatch[1] || detailMatch[2]);
return Number.isInteger(step) && step > 0 ? step : null;
}
function isPhoneVerificationFailure(detail = '') {
const text = String(detail || '').trim();
if (!text) {
return false;
}
return /add[_\s-]?phone/i.test(text)
|| /手机号(?:验证|页面|页)|手机(?:号)?页面|出现手机号验证/.test(text)
|| /进入了手机号页面/.test(text);
}
function buildFailureLabel(finalStatus, failedStep, failureDetail = '') {
if (finalStatus === 'success') {
return '流程完成';
}
if (finalStatus === 'stopped') {
if (Number.isInteger(failedStep) && failedStep > 0) {
return `步骤 ${failedStep} 停止`;
}
return '流程已停止';
}
if (finalStatus !== 'failed') {
return '无';
}
if (isPhoneVerificationFailure(failureDetail)) {
return '出现手机号验证';
}
if (Number.isInteger(failedStep) && failedStep > 0) {
return `步骤 ${failedStep} 失败`;
}
return '流程失败';
}
function normalizeAccountIdentifierType(value = '') {
return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
}
function normalizeAccountIdentifierValue(value = '', identifierType = 'email') {
const normalizedValue = String(value || '').trim();
if (!normalizedValue) {
return '';
}
return normalizeAccountIdentifierType(identifierType) === 'phone'
? normalizedValue
: normalizedValue.toLowerCase();
}
function getActivationPhoneNumber(activation = null) {
if (!activation || typeof activation !== 'object' || Array.isArray(activation)) {
return '';
}
return String(
activation.phoneNumber
?? activation.number
?? activation.phone
?? ''
).trim();
}
function resolveStatePhoneNumber(state = {}) {
const identifierType = String(state?.accountIdentifierType || '').trim().toLowerCase();
const accountIdentifierPhone = identifierType === 'phone'
? String(state?.accountIdentifier || '').trim()
: '';
return String(
state?.phoneNumber
|| state?.signupPhoneNumber
|| accountIdentifierPhone
|| getActivationPhoneNumber(state?.signupPhoneCompletedActivation)
|| getActivationPhoneNumber(state?.signupPhoneActivation)
|| getActivationPhoneNumber(state?.currentPhoneActivation)
|| ''
).trim();
}
function normalizePhoneRecordKey(value = '') {
const rawValue = String(value || '').trim();
const digits = rawValue.replace(/\D+/g, '');
return digits || rawValue.toLowerCase();
}
function resolveRecordIdentity(record = {}) {
const rawEmail = String(record.email || '').trim().toLowerCase();
const rawPhoneNumber = String(record.phoneNumber ?? record.phone ?? record.number ?? '').trim();
const rawIdentifierType = String(record.accountIdentifierType || '').trim().toLowerCase();
const inferredIdentifierType = rawIdentifierType === 'phone'
? 'phone'
: (rawIdentifierType === 'email'
? 'email'
: ((!rawEmail && rawPhoneNumber) ? 'phone' : 'email'));
const rawAccountIdentifier = String(
record.accountIdentifier
|| (inferredIdentifierType === 'phone' ? rawPhoneNumber : rawEmail)
|| ''
).trim();
const accountIdentifierType = rawAccountIdentifier
? normalizeAccountIdentifierType(inferredIdentifierType)
: (rawEmail ? 'email' : (rawPhoneNumber ? 'phone' : ''));
const accountIdentifier = normalizeAccountIdentifierValue(
rawAccountIdentifier || (accountIdentifierType === 'phone' ? rawPhoneNumber : rawEmail),
accountIdentifierType || inferredIdentifierType
);
const email = rawEmail || (accountIdentifierType === 'email' ? accountIdentifier : '');
const phoneNumber = rawPhoneNumber || (accountIdentifierType === 'phone' ? accountIdentifier : '');
return {
email,
phoneNumber,
accountIdentifierType,
accountIdentifier,
};
}
function buildRecordId(identifier = '', identifierType = 'email') {
const normalizedIdentifierType = normalizeAccountIdentifierType(identifierType);
const normalizedIdentifier = normalizeAccountIdentifierValue(identifier, normalizedIdentifierType);
if (!normalizedIdentifier) {
return '';
}
if (normalizedIdentifierType === 'phone' && /^phone:/i.test(normalizedIdentifier)) {
return normalizedIdentifier.toLowerCase();
}
return normalizedIdentifierType === 'phone'
? `phone:${normalizedIdentifier.toLowerCase()}`
: normalizedIdentifier;
}
function normalizeSource(value = '') {
return String(value || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual';
}
function normalizeAutoRunContext(context) {
if (!context || typeof context !== 'object') {
return null;
}
const currentRun = Math.max(0, Math.floor(Number(context.currentRun) || 0));
const totalRuns = Math.max(0, Math.floor(Number(context.totalRuns) || 0));
const attemptRun = Math.max(0, Math.floor(Number(context.attemptRun) || 0));
if (!currentRun && !totalRuns && !attemptRun) {
return null;
}
return {
currentRun,
totalRuns,
attemptRun,
};
}
function buildAutoRunContextFromState(state = {}) {
return normalizeAutoRunContext({
currentRun: state.autoRunCurrentRun,
totalRuns: state.autoRunTotalRuns,
attemptRun: state.autoRunAttemptRun,
});
}
function getRetryCountFromState(state = {}) {
if (!Boolean(state.autoRunning)) {
return 0;
}
const attemptRun = Math.max(0, Math.floor(Number(state.autoRunAttemptRun) || 0));
return attemptRun > 1 ? attemptRun - 1 : 0;
}
function normalizeAccountRunHistoryRecord(record) {
if (!record || typeof record !== 'object') {
return null;
}
const identity = resolveRecordIdentity(record);
const email = identity.email;
const phoneNumber = identity.phoneNumber;
const accountIdentifierType = identity.accountIdentifierType;
const accountIdentifier = identity.accountIdentifier;
const password = String(record.password ?? '').trim();
const finalStatus = normalizeFinalStatus(record.finalStatus || record.status || '');
if (!accountIdentifier || !finalStatus) {
return null;
}
const finishedAt = String(record.finishedAt || record.recordedAt || '').trim();
const failureDetail = finalStatus === 'failed' || finalStatus === 'stopped'
? String(record.failureDetail || record.reason || '').trim()
: '';
const failedStepCandidate = Number(record.failedStep);
const failedStep = Number.isInteger(failedStepCandidate) && failedStepCandidate > 0
? failedStepCandidate
: extractRecordStep(record.finalStatus || record.status || '', failureDetail);
const autoRunContext = normalizeAutoRunContext(record.autoRunContext);
const retryCount = normalizeRetryCount(
record.retryCount !== undefined
? record.retryCount
: ((autoRunContext?.attemptRun || 0) > 1 ? autoRunContext.attemptRun - 1 : 0)
);
const source = normalizeSource(record.source || (autoRunContext ? 'auto' : 'manual'));
const computedFailureLabel = buildFailureLabel(finalStatus, failedStep, failureDetail);
const rawFailureLabel = String(record.failureLabel || '').trim();
return {
recordId: String(record.recordId || '').trim() || buildRecordId(accountIdentifier, accountIdentifierType),
accountIdentifierType,
accountIdentifier,
email,
phoneNumber,
password,
finalStatus,
finishedAt,
retryCount,
failureLabel: finalStatus === 'stopped'
? computedFailureLabel
: (rawFailureLabel || computedFailureLabel),
failureDetail,
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
source,
autoRunContext: source === 'auto' ? autoRunContext : null,
plusModeEnabled: Boolean(record.plusModeEnabled),
contributionMode: Boolean(record.contributionMode),
};
}
function normalizeAccountRunHistory(records) {
if (!Array.isArray(records)) {
return [];
}
return records
.map((item) => normalizeAccountRunHistoryRecord(item))
.filter(Boolean)
.sort((left, right) => normalizeTimestamp(right.finishedAt) - normalizeTimestamp(left.finishedAt));
}
async function getPersistedAccountRunHistory() {
try {
const stored = await chrome.storage.local.get(ACCOUNT_RUN_HISTORY_STORAGE_KEY);
return normalizeAccountRunHistory(stored[ACCOUNT_RUN_HISTORY_STORAGE_KEY]);
} catch (err) {
console.warn('[MultiPage:account-run-history] Failed to read account run history:', err?.message || err);
return [];
}
}
async function setPersistedAccountRunHistory(records) {
const normalizedHistory = normalizeAccountRunHistory(records);
await chrome.storage.local.set({
[ACCOUNT_RUN_HISTORY_STORAGE_KEY]: normalizedHistory,
});
return normalizedHistory;
}
function buildAccountRunHistoryRecord(state = {}, status = '', reason = '') {
const identity = resolveRecordIdentity({
accountIdentifierType: state.accountIdentifierType,
accountIdentifier: state.accountIdentifier,
email: state.email,
phoneNumber: resolveStatePhoneNumber(state),
});
const email = identity.email;
const phoneNumber = identity.phoneNumber;
const accountIdentifierType = identity.accountIdentifierType;
const accountIdentifier = identity.accountIdentifier;
const password = String(state.password || state.customPassword || '').trim();
const finalStatus = normalizeFinalStatus(status);
if (!accountIdentifier || !finalStatus) {
return null;
}
const failureDetail = finalStatus === 'failed' || finalStatus === 'stopped' ? String(reason || '').trim() : '';
const failedStep = finalStatus === 'failed' || finalStatus === 'stopped'
? extractRecordStep(status, failureDetail)
: null;
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();
return {
recordId: buildRecordId(accountIdentifier, accountIdentifierType),
accountIdentifierType,
accountIdentifier,
email,
phoneNumber,
password,
finalStatus,
finishedAt,
retryCount,
failureLabel: buildFailureLabel(finalStatus, failedStep, failureDetail),
failureDetail,
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
source,
autoRunContext,
plusModeEnabled: Boolean(state.plusModeEnabled),
contributionMode: Boolean(state.contributionMode),
};
}
function upsertAccountRunHistoryRecord(history, record) {
const normalizedHistory = normalizeAccountRunHistory(history);
if (!record) {
return normalizedHistory;
}
const recordId = String(record.recordId || '').trim();
const emailKey = String(record.email || '').trim().toLowerCase();
const phoneKey = normalizePhoneRecordKey(record.phoneNumber);
const identifierKey = buildRecordId(
record.accountIdentifier || record.email || record.phoneNumber,
record.accountIdentifierType || (phoneKey && !emailKey ? 'phone' : 'email')
);
const nextHistory = normalizedHistory.filter((item) => {
const itemRecordId = String(item.recordId || '').trim();
const itemEmailKey = String(item.email || '').trim().toLowerCase();
const itemPhoneKey = normalizePhoneRecordKey(item.phoneNumber);
const itemIdentifierKey = buildRecordId(
item.accountIdentifier || item.email || item.phoneNumber,
item.accountIdentifierType || (itemPhoneKey && !itemEmailKey ? 'phone' : 'email')
);
return itemRecordId !== recordId
&& itemIdentifierKey !== identifierKey
&& (!emailKey || itemEmailKey !== emailKey)
&& (!phoneKey || itemPhoneKey !== phoneKey);
});
nextHistory.unshift(record);
return normalizeAccountRunHistory(nextHistory);
}
function deleteAccountRunHistoryEntries(history, recordIds = []) {
const normalizedHistory = normalizeAccountRunHistory(history);
const normalizedIds = Array.isArray(recordIds)
? recordIds
.map((value) => String(value || '').trim().toLowerCase())
.filter(Boolean)
: [];
if (!normalizedIds.length) {
return {
deletedCount: 0,
nextHistory: normalizedHistory,
};
}
const selectedIds = new Set(normalizedIds);
const nextHistory = normalizedHistory.filter((record) => !selectedIds.has(buildRecordId(
record.recordId || record.accountIdentifier || record.email || record.phoneNumber,
String(record.recordId || '').startsWith('phone:') || String(record.accountIdentifierType || '').trim().toLowerCase() === 'phone'
? 'phone'
: 'email'
)));
return {
deletedCount: normalizedHistory.length - nextHistory.length,
nextHistory: normalizeAccountRunHistory(nextHistory),
};
}
async function appendAccountRunHistoryRecord(status, stateOverride = null, reason = '') {
const state = stateOverride || await getState();
const record = buildAccountRunHistoryRecord(state, status, reason);
if (!record) {
return null;
}
const history = await getPersistedAccountRunHistory();
const nextHistory = upsertAccountRunHistoryRecord(history, record);
await setPersistedAccountRunHistory(nextHistory);
return record;
}
function summarizeAccountRunHistory(records = []) {
return normalizeAccountRunHistory(records).reduce((summary, record) => {
summary.total += 1;
if (record.finalStatus === 'success') {
summary.success += 1;
} else if (record.finalStatus === 'failed') {
summary.failed += 1;
} else if (record.finalStatus === 'stopped') {
summary.stopped += 1;
}
summary.retryTotal += normalizeRetryCount(record.retryCount);
return summary;
}, {
total: 0,
success: 0,
failed: 0,
stopped: 0,
retryTotal: 0,
});
}
function buildAccountRunHistorySnapshotPayload(records = []) {
const normalizedHistory = normalizeAccountRunHistory(records);
return {
generatedAt: new Date().toISOString(),
summary: summarizeAccountRunHistory(normalizedHistory),
records: normalizedHistory,
};
}
function shouldSyncAccountRunHistorySnapshot(state = {}) {
if (Boolean(state.contributionMode)) {
return false;
}
const helperBaseUrl = normalizeAccountRunHistoryHelperBaseUrl(state.accountRunHistoryHelperBaseUrl);
return Boolean(helperBaseUrl);
}
function shouldAppendAccountRunTextFile(state = {}) {
return shouldSyncAccountRunHistorySnapshot(state);
}
async function syncAccountRunHistorySnapshot(records, stateOverride = null) {
const state = stateOverride || await getState();
if (!shouldSyncAccountRunHistorySnapshot(state)) {
return '';
}
const helperBaseUrl = normalizeAccountRunHistoryHelperBaseUrl(state.accountRunHistoryHelperBaseUrl);
let response;
try {
response = await fetch(buildLocalHelperEndpoint(helperBaseUrl, '/sync-account-run-records'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(buildAccountRunHistorySnapshotPayload(records)),
});
} catch (err) {
return '';
}
let payload = null;
try {
payload = await response.json();
} catch (err) {
throw new Error(`账号记录快照同步失败:本地 helper 返回了无法解析的响应(${getErrorMessage(err)}`);
}
if (!response.ok || payload?.ok === false) {
throw new Error(`账号记录快照同步失败:${payload?.error || `HTTP ${response.status}`}`);
}
return payload?.filePath || '';
}
async function appendAccountRunRecord(status, stateOverride = null, reason = '') {
const state = stateOverride || await getState();
const record = await appendAccountRunHistoryRecord(status, state, reason);
if (!record) {
return null;
}
try {
const history = await getPersistedAccountRunHistory();
const filePath = await syncAccountRunHistorySnapshot(history, state);
if (filePath) {
await addLog(`账号记录快照已同步到本地:${filePath}`, 'info');
}
} catch (err) {
await addLog(getErrorMessage(err), 'warn');
}
return record;
}
async function clearAccountRunHistory(stateOverride = null) {
const state = stateOverride || await getState();
const history = await getPersistedAccountRunHistory();
await setPersistedAccountRunHistory([]);
try {
const filePath = await syncAccountRunHistorySnapshot([], state);
if (filePath) {
await addLog(`账号记录快照已同步到本地:${filePath}`, 'info');
}
} catch (err) {
await addLog(getErrorMessage(err), 'warn');
}
return {
clearedCount: history.length,
};
}
async function deleteAccountRunHistoryRecords(recordIds = [], stateOverride = null) {
const state = stateOverride || await getState();
const history = await getPersistedAccountRunHistory();
const { deletedCount, nextHistory } = deleteAccountRunHistoryEntries(history, recordIds);
if (!deletedCount) {
return {
deletedCount: 0,
remainingCount: history.length,
};
}
await setPersistedAccountRunHistory(nextHistory);
try {
const filePath = await syncAccountRunHistorySnapshot(nextHistory, state);
if (filePath) {
await addLog(`账号记录快照已同步到本地:${filePath}`, 'info');
}
} catch (err) {
await addLog(getErrorMessage(err), 'warn');
}
return {
deletedCount,
remainingCount: nextHistory.length,
};
}
return {
appendAccountRunRecord,
appendAccountRunHistoryRecord,
buildAccountRunHistoryRecord,
buildAccountRunHistorySnapshotPayload,
clearAccountRunHistory,
deleteAccountRunHistoryRecords,
getPersistedAccountRunHistory,
normalizeAccountRunHistory,
normalizeAccountRunHistoryRecord,
normalizeFinalStatus,
setPersistedAccountRunHistory,
shouldAppendAccountRunTextFile,
shouldSyncAccountRunHistorySnapshot,
summarizeAccountRunHistory,
syncAccountRunHistorySnapshot,
};
}
return {
createAccountRunHistoryHelpers,
};
});