475 lines
16 KiB
JavaScript
475 lines
16 KiB
JavaScript
(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 buildRecordId(email = '') {
|
||
return String(email || '').trim().toLowerCase();
|
||
}
|
||
|
||
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 email = String(record.email || '').trim();
|
||
const password = String(record.password || '').trim();
|
||
const finalStatus = normalizeFinalStatus(record.finalStatus || record.status || '');
|
||
|
||
if (!email || !password || !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(email),
|
||
email,
|
||
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,
|
||
};
|
||
}
|
||
|
||
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 email = String(state.email || '').trim();
|
||
const password = String(state.password || state.customPassword || '').trim() || '无';
|
||
const finalStatus = normalizeFinalStatus(status);
|
||
|
||
if (!email || !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(email),
|
||
email,
|
||
password,
|
||
finalStatus,
|
||
finishedAt,
|
||
retryCount,
|
||
failureLabel: buildFailureLabel(finalStatus, failedStep, failureDetail),
|
||
failureDetail,
|
||
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
|
||
source,
|
||
autoRunContext,
|
||
};
|
||
}
|
||
|
||
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 nextHistory = normalizedHistory.filter((item) => {
|
||
const itemRecordId = String(item.recordId || '').trim();
|
||
const itemEmailKey = String(item.email || '').trim().toLowerCase();
|
||
return itemRecordId !== recordId && itemEmailKey !== emailKey;
|
||
});
|
||
|
||
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.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;
|
||
}
|
||
if (!Boolean(state.accountRunHistoryTextEnabled)) {
|
||
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) {
|
||
throw new Error(`账号记录快照同步失败:无法连接本地 helper(${getErrorMessage(err)})`);
|
||
}
|
||
|
||
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,
|
||
};
|
||
});
|