feat: 添加停止记录推断逻辑,支持归一化步骤状态并更新相关测试
This commit is contained in:
+83
-1
@@ -4064,6 +4064,79 @@ function getRunningSteps(statuses = {}) {
|
|||||||
.sort((a, b) => a - b);
|
.sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inferStoppedRecordStep(state = {}) {
|
||||||
|
const statuses = { ...DEFAULT_STATE.stepStatuses, ...(state?.stepStatuses || {}) };
|
||||||
|
const stepIds = Object.keys(statuses)
|
||||||
|
.map((step) => Number(step))
|
||||||
|
.filter(Number.isFinite)
|
||||||
|
.sort((left, right) => left - right);
|
||||||
|
|
||||||
|
const runningSteps = stepIds.filter((step) => statuses[step] === 'running');
|
||||||
|
if (runningSteps.length) {
|
||||||
|
return runningSteps[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasProgress = stepIds.some((step) => statuses[step] !== 'pending');
|
||||||
|
if (!hasProgress) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const step of stepIds) {
|
||||||
|
const status = statuses[step] || 'pending';
|
||||||
|
if (!(status === 'completed' || status === 'manual_completed' || status === 'skipped')) {
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAccountRunRecordStatusForStop(status, state = {}) {
|
||||||
|
const normalizedStatus = String(status || '').trim().toLowerCase();
|
||||||
|
if (normalizedStatus === 'stopped') {
|
||||||
|
const inferredStep = inferStoppedRecordStep(state);
|
||||||
|
if (Number.isInteger(inferredStep) && inferredStep > 0) {
|
||||||
|
return `step${inferredStep}_stopped`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractStoppedStepFromRecordStatus(status = '') {
|
||||||
|
const match = String(status || '').trim().toLowerCase().match(/^step(\d+)_stopped$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const step = Number(match[1]);
|
||||||
|
return Number.isInteger(step) && step > 0 ? step : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAccountRunRecordReasonForStop(status, reason = '') {
|
||||||
|
const text = String(reason || '').trim();
|
||||||
|
const stoppedStep = extractStoppedStepFromRecordStatus(status);
|
||||||
|
|
||||||
|
if (!stoppedStep) {
|
||||||
|
if (!text || text === STOP_ERROR_MESSAGE || /^流程已被用户停止。?$/.test(text)) {
|
||||||
|
return '流程已停止。';
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text || text === STOP_ERROR_MESSAGE || /^流程已被用户停止。?$/.test(text)) {
|
||||||
|
return `步骤 ${stoppedStep} 已被用户停止。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/流程尚未完成/.test(text) || /已使用邮箱/.test(text)) {
|
||||||
|
return `步骤 ${stoppedStep} 已停止:邮箱已设置,流程尚未完成。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/步骤\s*\d+\s*已(?:被用户)?停止/.test(text)) {
|
||||||
|
return text.replace(/步骤\s*\d+/, `步骤 ${stoppedStep}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
function getAutoRunStatusPayload(phase, payload = {}) {
|
function getAutoRunStatusPayload(phase, payload = {}) {
|
||||||
const normalizedPayload = {
|
const normalizedPayload = {
|
||||||
...payload,
|
...payload,
|
||||||
@@ -4932,6 +5005,8 @@ async function markRunningStepsStopped() {
|
|||||||
async function requestStop(options = {}) {
|
async function requestStop(options = {}) {
|
||||||
const { logMessage = '已收到停止请求,正在取消当前操作...' } = options;
|
const { logMessage = '已收到停止请求,正在取消当前操作...' } = options;
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
|
const runningSteps = getRunningSteps(state.stepStatuses);
|
||||||
|
const inferredStopStep = inferStoppedRecordStep(state);
|
||||||
const timerPlan = getPendingAutoRunTimerPlan(state);
|
const timerPlan = getPendingAutoRunTimerPlan(state);
|
||||||
|
|
||||||
if (timerPlan?.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START && !autoRunActive) {
|
if (timerPlan?.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START && !autoRunActive) {
|
||||||
@@ -4979,6 +5054,10 @@ async function requestStop(options = {}) {
|
|||||||
await addLog(logMessage, 'warn');
|
await addLog(logMessage, 'warn');
|
||||||
await broadcastStopToContentScripts();
|
await broadcastStopToContentScripts();
|
||||||
|
|
||||||
|
if (!runningSteps.length && Number.isInteger(inferredStopStep) && inferredStopStep > 0) {
|
||||||
|
await appendAndBroadcastAccountRunRecord('stopped', state, STOP_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
for (const waiter of stepWaiters.values()) {
|
for (const waiter of stepWaiters.values()) {
|
||||||
waiter.reject(new Error(STOP_ERROR_MESSAGE));
|
waiter.reject(new Error(STOP_ERROR_MESSAGE));
|
||||||
}
|
}
|
||||||
@@ -5208,7 +5287,10 @@ async function appendAndBroadcastAccountRunRecord(status, stateOverride = null,
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = await accountRunHistoryHelpers.appendAccountRunRecord(status, stateOverride, reason);
|
const state = stateOverride || await getState();
|
||||||
|
const resolvedStatus = resolveAccountRunRecordStatusForStop(status, state);
|
||||||
|
const resolvedReason = resolveAccountRunRecordReasonForStop(resolvedStatus, reason);
|
||||||
|
const record = await accountRunHistoryHelpers.appendAccountRunRecord(resolvedStatus, state, resolvedReason);
|
||||||
if (!record) {
|
if (!record) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,9 @@
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFailedStep(status = '', detail = '') {
|
function extractRecordStep(status = '', detail = '') {
|
||||||
const normalizedStatus = String(status || '').trim().toLowerCase();
|
const normalizedStatus = String(status || '').trim().toLowerCase();
|
||||||
const statusMatch = normalizedStatus.match(/^step(\d+)_failed$/);
|
const statusMatch = normalizedStatus.match(/^step(\d+)_(?:failed|stopped)$/);
|
||||||
if (statusMatch) {
|
if (statusMatch) {
|
||||||
const step = Number(statusMatch[1]);
|
const step = Number(statusMatch[1]);
|
||||||
return Number.isInteger(step) && step > 0 ? step : null;
|
return Number.isInteger(step) && step > 0 ? step : null;
|
||||||
@@ -73,6 +73,9 @@
|
|||||||
return '流程完成';
|
return '流程完成';
|
||||||
}
|
}
|
||||||
if (finalStatus === 'stopped') {
|
if (finalStatus === 'stopped') {
|
||||||
|
if (Number.isInteger(failedStep) && failedStep > 0) {
|
||||||
|
return `步骤 ${failedStep} 停止`;
|
||||||
|
}
|
||||||
return '流程已停止';
|
return '流程已停止';
|
||||||
}
|
}
|
||||||
if (finalStatus !== 'failed') {
|
if (finalStatus !== 'failed') {
|
||||||
@@ -152,7 +155,7 @@
|
|||||||
const failedStepCandidate = Number(record.failedStep);
|
const failedStepCandidate = Number(record.failedStep);
|
||||||
const failedStep = Number.isInteger(failedStepCandidate) && failedStepCandidate > 0
|
const failedStep = Number.isInteger(failedStepCandidate) && failedStepCandidate > 0
|
||||||
? failedStepCandidate
|
? failedStepCandidate
|
||||||
: extractFailedStep(record.finalStatus || record.status || '', failureDetail);
|
: extractRecordStep(record.finalStatus || record.status || '', failureDetail);
|
||||||
const autoRunContext = normalizeAutoRunContext(record.autoRunContext);
|
const autoRunContext = normalizeAutoRunContext(record.autoRunContext);
|
||||||
const retryCount = normalizeRetryCount(
|
const retryCount = normalizeRetryCount(
|
||||||
record.retryCount !== undefined
|
record.retryCount !== undefined
|
||||||
@@ -160,6 +163,8 @@
|
|||||||
: ((autoRunContext?.attemptRun || 0) > 1 ? autoRunContext.attemptRun - 1 : 0)
|
: ((autoRunContext?.attemptRun || 0) > 1 ? autoRunContext.attemptRun - 1 : 0)
|
||||||
);
|
);
|
||||||
const source = normalizeSource(record.source || (autoRunContext ? 'auto' : 'manual'));
|
const source = normalizeSource(record.source || (autoRunContext ? 'auto' : 'manual'));
|
||||||
|
const computedFailureLabel = buildFailureLabel(finalStatus, failedStep, failureDetail);
|
||||||
|
const rawFailureLabel = String(record.failureLabel || '').trim();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
recordId: String(record.recordId || '').trim() || buildRecordId(email),
|
recordId: String(record.recordId || '').trim() || buildRecordId(email),
|
||||||
@@ -168,7 +173,9 @@
|
|||||||
finalStatus,
|
finalStatus,
|
||||||
finishedAt,
|
finishedAt,
|
||||||
retryCount,
|
retryCount,
|
||||||
failureLabel: String(record.failureLabel || '').trim() || buildFailureLabel(finalStatus, failedStep, failureDetail),
|
failureLabel: finalStatus === 'stopped'
|
||||||
|
? computedFailureLabel
|
||||||
|
: (rawFailureLabel || computedFailureLabel),
|
||||||
failureDetail,
|
failureDetail,
|
||||||
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
|
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
|
||||||
source,
|
source,
|
||||||
@@ -215,7 +222,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const failureDetail = finalStatus === 'failed' || finalStatus === 'stopped' ? String(reason || '').trim() : '';
|
const failureDetail = finalStatus === 'failed' || finalStatus === 'stopped' ? String(reason || '').trim() : '';
|
||||||
const failedStep = finalStatus === 'failed' ? extractFailedStep(status, failureDetail) : null;
|
const failedStep = finalStatus === 'failed' || finalStatus === 'stopped'
|
||||||
|
? extractRecordStep(status, failureDetail)
|
||||||
|
: null;
|
||||||
const source = Boolean(state.autoRunning) ? 'auto' : 'manual';
|
const source = Boolean(state.autoRunning) ? 'auto' : 'manual';
|
||||||
const autoRunContext = source === 'auto' ? buildAutoRunContextFromState(state) : null;
|
const autoRunContext = source === 'auto' ? buildAutoRunContextFromState(state) : null;
|
||||||
const retryCount = source === 'auto' ? getRetryCountFromState(state) : 0;
|
const retryCount = source === 'auto' ? getRetryCountFromState(state) : 0;
|
||||||
|
|||||||
@@ -100,18 +100,42 @@ test('account run history helper upgrades old records, keeps stopped items and s
|
|||||||
assert.equal(fetchCalled, false);
|
assert.equal(fetchCalled, false);
|
||||||
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: false, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), false);
|
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: false, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), false);
|
||||||
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: true, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), true);
|
assert.equal(helpers.shouldAppendAccountRunTextFile({ accountRunHistoryTextEnabled: true, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373' }), true);
|
||||||
const stoppedRecord = helpers.buildAccountRunHistoryRecord({ email: 'a@b.com', password: 'x' }, 'stopped', 'stop');
|
const stoppedRecord = helpers.buildAccountRunHistoryRecord(
|
||||||
|
{ email: 'a@b.com', password: 'x' },
|
||||||
|
'step7_stopped',
|
||||||
|
'步骤 7 已被用户停止'
|
||||||
|
);
|
||||||
assert.equal(stoppedRecord.recordId, 'a@b.com');
|
assert.equal(stoppedRecord.recordId, 'a@b.com');
|
||||||
assert.equal(stoppedRecord.email, 'a@b.com');
|
assert.equal(stoppedRecord.email, 'a@b.com');
|
||||||
assert.equal(stoppedRecord.password, 'x');
|
assert.equal(stoppedRecord.password, 'x');
|
||||||
assert.equal(stoppedRecord.finalStatus, 'stopped');
|
assert.equal(stoppedRecord.finalStatus, 'stopped');
|
||||||
assert.equal(stoppedRecord.retryCount, 0);
|
assert.equal(stoppedRecord.retryCount, 0);
|
||||||
assert.equal(stoppedRecord.failureLabel, '流程已停止');
|
assert.equal(stoppedRecord.failureLabel, '步骤 7 停止');
|
||||||
assert.equal(stoppedRecord.failureDetail, 'stop');
|
assert.equal(stoppedRecord.failureDetail, '步骤 7 已被用户停止');
|
||||||
assert.equal(stoppedRecord.failedStep, null);
|
assert.equal(stoppedRecord.failedStep, 7);
|
||||||
assert.equal(stoppedRecord.source, 'manual');
|
assert.equal(stoppedRecord.source, 'manual');
|
||||||
assert.equal(stoppedRecord.autoRunContext, null);
|
assert.equal(stoppedRecord.autoRunContext, null);
|
||||||
assert.ok(stoppedRecord.finishedAt);
|
assert.ok(stoppedRecord.finishedAt);
|
||||||
|
|
||||||
|
const genericStoppedRecord = helpers.buildAccountRunHistoryRecord({ email: 'stop@b.com', password: 'y' }, 'stopped', 'stop');
|
||||||
|
assert.equal(genericStoppedRecord.failureLabel, '流程已停止');
|
||||||
|
assert.equal(genericStoppedRecord.failedStep, null);
|
||||||
|
|
||||||
|
const normalizedStoppedRecord = helpers.normalizeAccountRunHistoryRecord({
|
||||||
|
recordId: 'legacy-stop@example.com',
|
||||||
|
email: 'legacy-stop@example.com',
|
||||||
|
password: 'secret',
|
||||||
|
finalStatus: 'stopped',
|
||||||
|
finishedAt: '2026-04-17T00:12:00.000Z',
|
||||||
|
retryCount: 0,
|
||||||
|
failureLabel: '流程已停止',
|
||||||
|
failureDetail: '步骤 7 已被用户停止。',
|
||||||
|
failedStep: 7,
|
||||||
|
source: 'manual',
|
||||||
|
autoRunContext: null,
|
||||||
|
});
|
||||||
|
assert.equal(normalizedStoppedRecord.failureLabel, '步骤 7 停止');
|
||||||
|
assert.equal(normalizedStoppedRecord.failedStep, 7);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('account run history helper clears persisted records and syncs full snapshot payload to local helper', async () => {
|
test('account run history helper clears persisted records and syncs full snapshot payload to local helper', async () => {
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const source = fs.readFileSync('background.js', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
|
const start = markers
|
||||||
|
.map((marker) => source.indexOf(marker))
|
||||||
|
.find((index) => index >= 0);
|
||||||
|
if (start < 0) {
|
||||||
|
throw new Error(`missing function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parenDepth = 0;
|
||||||
|
let signatureEnded = false;
|
||||||
|
let braceStart = -1;
|
||||||
|
for (let i = start; i < source.length; i += 1) {
|
||||||
|
const ch = source[i];
|
||||||
|
if (ch === '(') {
|
||||||
|
parenDepth += 1;
|
||||||
|
} else if (ch === ')') {
|
||||||
|
parenDepth -= 1;
|
||||||
|
if (parenDepth === 0) {
|
||||||
|
signatureEnded = true;
|
||||||
|
}
|
||||||
|
} else if (ch === '{' && signatureEnded) {
|
||||||
|
braceStart = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let end = braceStart;
|
||||||
|
for (; end < source.length; end += 1) {
|
||||||
|
const ch = source[end];
|
||||||
|
if (ch === '{') depth += 1;
|
||||||
|
if (ch === '}') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
end += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return source.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('generic stopped record resolves to next unfinished step during execution gap', async () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('getRunningSteps'),
|
||||||
|
extractFunction('inferStoppedRecordStep'),
|
||||||
|
extractFunction('resolveAccountRunRecordStatusForStop'),
|
||||||
|
extractFunction('extractStoppedStepFromRecordStatus'),
|
||||||
|
extractFunction('resolveAccountRunRecordReasonForStop'),
|
||||||
|
extractFunction('appendAndBroadcastAccountRunRecord'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
const STEP_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
|
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
stepStatuses: Object.fromEntries(STEP_IDS.map((step) => [step, 'pending'])),
|
||||||
|
};
|
||||||
|
let captured = null;
|
||||||
|
const accountRunHistoryHelpers = {
|
||||||
|
appendAccountRunRecord: async (status, state, reason) => {
|
||||||
|
captured = { status, state, reason };
|
||||||
|
return { status, state, reason };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
async function broadcastAccountRunHistoryUpdate() {}
|
||||||
|
async function getState() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
${bundle}
|
||||||
|
return {
|
||||||
|
inferStoppedRecordStep,
|
||||||
|
resolveAccountRunRecordStatusForStop,
|
||||||
|
resolveAccountRunRecordReasonForStop,
|
||||||
|
appendAndBroadcastAccountRunRecord,
|
||||||
|
getCaptured() {
|
||||||
|
return captured;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'secret',
|
||||||
|
stepStatuses: {
|
||||||
|
1: 'completed',
|
||||||
|
2: 'completed',
|
||||||
|
3: 'completed',
|
||||||
|
4: 'completed',
|
||||||
|
5: 'completed',
|
||||||
|
6: 'completed',
|
||||||
|
7: 'pending',
|
||||||
|
8: 'pending',
|
||||||
|
9: 'pending',
|
||||||
|
10: 'pending',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.equal(api.inferStoppedRecordStep(state), 7);
|
||||||
|
assert.equal(api.resolveAccountRunRecordStatusForStop('stopped', state), 'step7_stopped');
|
||||||
|
assert.equal(api.resolveAccountRunRecordReasonForStop('step7_stopped', '流程已被用户停止。'), '步骤 7 已被用户停止。');
|
||||||
|
assert.equal(
|
||||||
|
api.resolveAccountRunRecordReasonForStop('step2_stopped', '步骤 2 已使用邮箱,流程尚未完成。'),
|
||||||
|
'步骤 2 已停止:邮箱已设置,流程尚未完成。'
|
||||||
|
);
|
||||||
|
|
||||||
|
await api.appendAndBroadcastAccountRunRecord('stopped', state, '流程已被用户停止。');
|
||||||
|
assert.deepStrictEqual(api.getCaptured(), {
|
||||||
|
status: 'step7_stopped',
|
||||||
|
state,
|
||||||
|
reason: '步骤 7 已被用户停止。',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requestStop appends a stopped record for the next unfinished step when no step is running', async () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeAutoRunSessionId'),
|
||||||
|
extractFunction('clearCurrentAutoRunSessionId'),
|
||||||
|
extractFunction('cleanupStep8NavigationListeners'),
|
||||||
|
extractFunction('rejectPendingStep8'),
|
||||||
|
extractFunction('getRunningSteps'),
|
||||||
|
extractFunction('inferStoppedRecordStep'),
|
||||||
|
extractFunction('requestStop'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
let stopRequested = false;
|
||||||
|
let autoRunActive = false;
|
||||||
|
let autoRunCurrentRun = 0;
|
||||||
|
let autoRunTotalRuns = 1;
|
||||||
|
let autoRunAttemptRun = 0;
|
||||||
|
let autoRunSessionId = 99;
|
||||||
|
let webNavListener = null;
|
||||||
|
let webNavCommittedListener = null;
|
||||||
|
let step8TabUpdatedListener = null;
|
||||||
|
let step8PendingReject = null;
|
||||||
|
let resumeWaiter = null;
|
||||||
|
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||||
|
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
stepStatuses: Object.fromEntries([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((step) => [step, 'pending'])),
|
||||||
|
};
|
||||||
|
const stepWaiters = new Map();
|
||||||
|
const appended = [];
|
||||||
|
const logs = [];
|
||||||
|
const chrome = {
|
||||||
|
webNavigation: {
|
||||||
|
onBeforeNavigate: { removeListener() {} },
|
||||||
|
onCommitted: { removeListener() {} },
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
onUpdated: { removeListener() {} },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function cancelPendingCommands() {}
|
||||||
|
function getPendingAutoRunTimerPlan() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async function cancelScheduledAutoRun() {}
|
||||||
|
async function clearAutoRunTimerAlarm() {}
|
||||||
|
function clearStopRequest() {
|
||||||
|
stopRequested = false;
|
||||||
|
}
|
||||||
|
async function addLog(message, level) {
|
||||||
|
logs.push({ message, level });
|
||||||
|
}
|
||||||
|
async function broadcastStopToContentScripts() {}
|
||||||
|
async function markRunningStepsStopped() {}
|
||||||
|
async function broadcastAutoRunStatus() {}
|
||||||
|
async function appendAndBroadcastAccountRunRecord(status, state, reason) {
|
||||||
|
appended.push({ status, state, reason });
|
||||||
|
return { status, state, reason };
|
||||||
|
}
|
||||||
|
async function getState() {
|
||||||
|
return {
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'secret',
|
||||||
|
stepStatuses: {
|
||||||
|
1: 'completed',
|
||||||
|
2: 'completed',
|
||||||
|
3: 'completed',
|
||||||
|
4: 'completed',
|
||||||
|
5: 'completed',
|
||||||
|
6: 'completed',
|
||||||
|
7: 'pending',
|
||||||
|
8: 'pending',
|
||||||
|
9: 'pending',
|
||||||
|
10: 'pending',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
${bundle}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestStop,
|
||||||
|
snapshot() {
|
||||||
|
return { appended, logs, stopRequested, autoRunSessionId };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
await api.requestStop();
|
||||||
|
const state = api.snapshot();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(state.appended, [{
|
||||||
|
status: 'stopped',
|
||||||
|
state: {
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'secret',
|
||||||
|
stepStatuses: {
|
||||||
|
1: 'completed',
|
||||||
|
2: 'completed',
|
||||||
|
3: 'completed',
|
||||||
|
4: 'completed',
|
||||||
|
5: 'completed',
|
||||||
|
6: 'completed',
|
||||||
|
7: 'pending',
|
||||||
|
8: 'pending',
|
||||||
|
9: 'pending',
|
||||||
|
10: 'pending',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reason: '流程已被用户停止。',
|
||||||
|
}]);
|
||||||
|
assert.equal(state.autoRunSessionId, 0);
|
||||||
|
assert.equal(state.stopRequested, true);
|
||||||
|
});
|
||||||
@@ -207,7 +207,7 @@ test('account records manager supports filter chips and partial multi-select del
|
|||||||
finalStatus: 'stopped',
|
finalStatus: 'stopped',
|
||||||
finishedAt: '2026-04-17T04:28:00.000Z',
|
finishedAt: '2026-04-17T04:28:00.000Z',
|
||||||
retryCount: 1,
|
retryCount: 1,
|
||||||
failureLabel: '流程已停止',
|
failureLabel: '步骤 7 停止',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -303,6 +303,7 @@ test('account records manager supports filter chips and partial multi-select del
|
|||||||
assert.doesNotMatch(list.innerHTML, /success@example\.com/);
|
assert.doesNotMatch(list.innerHTML, /success@example\.com/);
|
||||||
assert.match(list.innerHTML, /failed@example\.com/);
|
assert.match(list.innerHTML, /failed@example\.com/);
|
||||||
assert.match(list.innerHTML, /stopped@example\.com/);
|
assert.match(list.innerHTML, /stopped@example\.com/);
|
||||||
|
assert.match(list.innerHTML, /步骤 7 停止/);
|
||||||
|
|
||||||
btnToggleAccountRecordsSelection.listeners.click();
|
btnToggleAccountRecordsSelection.listeners.click();
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ const helperBundle = [
|
|||||||
extractFunction(helperSource, 'cleanupStep8NavigationListeners'),
|
extractFunction(helperSource, 'cleanupStep8NavigationListeners'),
|
||||||
extractFunction(helperSource, 'rejectPendingStep8'),
|
extractFunction(helperSource, 'rejectPendingStep8'),
|
||||||
extractFunction(helperSource, 'throwIfStep8SettledOrStopped'),
|
extractFunction(helperSource, 'throwIfStep8SettledOrStopped'),
|
||||||
|
extractFunction(helperSource, 'getRunningSteps'),
|
||||||
|
extractFunction(helperSource, 'inferStoppedRecordStep'),
|
||||||
extractFunction(helperSource, 'requestStop'),
|
extractFunction(helperSource, 'requestStop'),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
@@ -75,6 +77,9 @@ let autoRunTotalRuns = 3;
|
|||||||
let autoRunAttemptRun = 4;
|
let autoRunAttemptRun = 4;
|
||||||
let autoRunSessionId = 99;
|
let autoRunSessionId = 99;
|
||||||
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
const AUTO_RUN_TIMER_KIND_SCHEDULED_START = 'scheduled_start';
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
stepStatuses: Object.fromEntries([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((step) => [step, 'pending'])),
|
||||||
|
};
|
||||||
const STEP8_CLICK_RETRY_DELAY_MS = 500;
|
const STEP8_CLICK_RETRY_DELAY_MS = 500;
|
||||||
const STEP8_MAX_ROUNDS = 5;
|
const STEP8_MAX_ROUNDS = 5;
|
||||||
const STEP8_READY_WAIT_TIMEOUT_MS = 30000;
|
const STEP8_READY_WAIT_TIMEOUT_MS = 30000;
|
||||||
@@ -137,6 +142,7 @@ async function addLog() {}
|
|||||||
async function broadcastStopToContentScripts() {}
|
async function broadcastStopToContentScripts() {}
|
||||||
async function markRunningStepsStopped() {}
|
async function markRunningStepsStopped() {}
|
||||||
async function broadcastAutoRunStatus() {}
|
async function broadcastAutoRunStatus() {}
|
||||||
|
async function appendAndBroadcastAccountRunRecord() {}
|
||||||
async function getState() {
|
async function getState() {
|
||||||
return { autoRunning: false };
|
return { autoRunning: false };
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -38,7 +38,7 @@
|
|||||||
- 接收后台广播并更新 UI
|
- 接收后台广播并更新 UI
|
||||||
- 动态渲染步骤列表
|
- 动态渲染步骤列表
|
||||||
- 顶部提供一个临时 `贡献` 按钮,用于直接打开账号贡献上传页
|
- 顶部提供一个临时 `贡献` 按钮,用于直接打开账号贡献上传页
|
||||||
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表
|
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计、按状态筛选、分页列表与多选删除操作
|
||||||
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Pro` 与 legacy `v` 两个版本族,排序时优先保持版本族语义一致,同时会在读取缓存后重新排序,避免旧缓存把 `v` 版本误显示为比 `Pro` 更新
|
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Pro` 与 legacy `v` 两个版本族,排序时优先保持版本族语义一致,同时会在读取缓存后重新排序,避免旧缓存把 `v` 版本误显示为比 `Pro` 更新
|
||||||
|
|
||||||
### 2.2 Background Service Worker
|
### 2.2 Background Service Worker
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
- iCloud 相关偏好
|
- iCloud 相关偏好
|
||||||
- LuckMail API 配置
|
- LuckMail API 配置
|
||||||
- 自动运行默认配置
|
- 自动运行默认配置
|
||||||
- 账号运行历史 `accountRunHistory`(以邮箱为主键,保存该邮箱最近一次状态:成功/失败/停止)
|
- 账号运行历史 `accountRunHistory`(以邮箱为主键,保存该邮箱最近一次状态:成功/失败/停止;停止记录会优先归一到具体步骤,如“步骤 7 停止”)
|
||||||
- 账号运行历史本地同步开关与 helper 地址
|
- 账号运行历史本地同步开关与 helper 地址
|
||||||
|
|
||||||
当启用了独立的账号运行历史本地同步配置时,账号运行历史会通过 [scripts/hotmail_helper.py](c:/Users/projectf/Downloads/codex注册扩展/scripts/hotmail_helper.py) 整体同步写入 `data/account-run-history.json` 快照文件,便于开发者直接查看完整记录。
|
当启用了独立的账号运行历史本地同步配置时,账号运行历史会通过 [scripts/hotmail_helper.py](c:/Users/projectf/Downloads/codex注册扩展/scripts/hotmail_helper.py) 整体同步写入 `data/account-run-history.json` 快照文件,便于开发者直接查看完整记录。
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
1. 解析本轮应使用的邮箱
|
1. 解析本轮应使用的邮箱
|
||||||
2. 打开或复用注册页
|
2. 打开或复用注册页
|
||||||
3. 点击注册入口并提交邮箱
|
3. 点击注册入口并提交邮箱
|
||||||
4. 以当前邮箱先写入一条“停止(流程尚未完成)”的记录占位
|
4. 以当前邮箱先写入一条“停止(流程尚未完成)”的记录占位;该占位记录后续会随真实停止点归一成“步骤 X 停止”或被成功/失败状态覆盖
|
||||||
5. 等待邮箱提交后的真实落地页
|
5. 等待邮箱提交后的真实落地页
|
||||||
6. 如果进入密码页,则继续执行 Step 3
|
6. 如果进入密码页,则继续执行 Step 3
|
||||||
7. 如果直接进入邮箱验证码页,则自动跳过 Step 3 并进入 Step 4
|
7. 如果直接进入邮箱验证码页,则自动跳过 Step 3 并进入 Step 4
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
- 当前轮重试
|
- 当前轮重试
|
||||||
- 下一轮继续
|
- 下一轮继续
|
||||||
7. 当前轮最终失败时,写入邮箱记录,并在自动重试链路中累计重试次数
|
7. 当前轮最终失败时,写入邮箱记录,并在自动重试链路中累计重试次数
|
||||||
- 手动停止与自动停止会写入“停止”状态(若后续同邮箱成功/失败,会被覆盖为最新状态)
|
- 手动停止与自动停止会写入“停止”状态;如果能确定当前运行步骤或下一未完成步骤,则会优先归一成“步骤 X 停止”(若后续同邮箱成功/失败,会被覆盖为最新状态)
|
||||||
8. 如果配置了线程间隔,则挂计时计划;计时计划会带上当前 `autoRunSessionId`
|
8. 如果配置了线程间隔,则挂计时计划;计时计划会带上当前 `autoRunSessionId`
|
||||||
9. 旧 timer / 旧 alarm / 旧恢复入口只有在 session 仍有效时才允许恢复执行;Stop 会立即使当前 session 失效,防止“停止后旧倒计时又把流程重新拉起”
|
9. 旧 timer / 旧 alarm / 旧恢复入口只有在 session 仍有效时才允许恢复执行;Stop 会立即使当前 session 失效,防止“停止后旧倒计时又把流程重新拉起”
|
||||||
10. 所有轮次结束后输出汇总,并清空当前 session 标识
|
10. 所有轮次结束后输出汇总,并清空当前 session 标识
|
||||||
|
|||||||
+5
-4
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
## `background/`
|
## `background/`
|
||||||
|
|
||||||
- `background/account-run-history.js`:邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,支持清理记录,并在启用独立本地同步配置后把完整快照同步到本地 helper。
|
- `background/account-run-history.js`:邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,停止记录会尽量推断并归一到具体步骤标签(如“步骤 7 停止”),支持清理记录与按选中记录局部删除,并在启用独立本地同步配置后把完整快照同步到本地 helper。
|
||||||
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 `gmailBaseEmail` 与 `mail2925BaseEmail`,避免自动流程重置后丢失别名基邮箱配置。
|
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 `gmailBaseEmail` 与 `mail2925BaseEmail`,避免自动流程重置后丢失别名基邮箱配置。
|
||||||
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口。
|
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口。
|
||||||
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。
|
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
- `sidepanel/hotmail-manager.js`:侧边栏 Hotmail 账号池管理器,负责列表、导入、验证、测试收信和批量操作。
|
- `sidepanel/hotmail-manager.js`:侧边栏 Hotmail 账号池管理器,负责列表、导入、验证、测试收信和批量操作。
|
||||||
- `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。
|
- `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。
|
||||||
- `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
|
- `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
|
||||||
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。
|
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要、按状态筛选、多选删除、清理确认,以及多层弹窗下的工具栏交互状态管理。
|
||||||
- `sidepanel/sidepanel.css`:侧边栏样式文件。
|
- `sidepanel/sidepanel.css`:侧边栏样式文件。
|
||||||
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增临时 `贡献` 按钮用于直接打开账号贡献上传页,同时继续加载 `managed-alias-utils.js`,把旧的“邮箱前缀”字段语义改为“别名基邮箱”。
|
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增临时 `贡献` 按钮用于直接打开账号贡献上传页,同时继续加载 `managed-alias-utils.js`,把旧的“邮箱前缀”字段语义改为“别名基邮箱”。
|
||||||
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / iCloud / LuckMail / 邮箱记录面板 manager;当前顶部 `贡献` 按钮会在新标签页中直接打开账号贡献上传页。
|
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / iCloud / LuckMail / 邮箱记录面板 manager;当前顶部 `贡献` 按钮会在新标签页中直接打开账号贡献上传页。
|
||||||
@@ -128,7 +128,8 @@
|
|||||||
- `tests/auto-run-step6-restart.test.js`:测试自动运行在后半段授权链路遇错时会回到步骤 7 重开,并在命中 add-phone 或泛化手机号页 fatal 错误时停止重开。
|
- `tests/auto-run-step6-restart.test.js`:测试自动运行在后半段授权链路遇错时会回到步骤 7 重开,并在命中 add-phone 或泛化手机号页 fatal 错误时停止重开。
|
||||||
- `tests/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。
|
- `tests/auto-step-random-delay.test.js`:测试自动运行步间延迟与旧配置键兼容解析。
|
||||||
- `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。
|
- `tests/background-auto-run-module.test.js`:测试自动运行控制器模块已接入且导出工厂。
|
||||||
- `tests/background-account-run-history-module.test.js`:测试邮箱记录模块已接入、导出工厂,能够保留并归一化停止记录、归一化失败标签、计算自动重试次数,并支持清理后同步完整快照。
|
- `tests/background-account-run-history-module.test.js`:测试邮箱记录模块已接入、导出工厂,能够保留并归一化停止记录、归一化失败/停止标签、计算自动重试次数,并支持清理后同步完整快照。
|
||||||
|
- `tests/background-stop-gap-record.test.js`:测试通用 stopped 记录在步骤空档期会归一到“下一未完成步骤停止”,并校验停止原因文案归一化。
|
||||||
- `tests/background-account-history-settings.test.js`:测试账号运行历史的独立配置项归一化,不再复用 Hotmail 模式作为启停条件。
|
- `tests/background-account-history-settings.test.js`:测试账号运行历史的独立配置项归一化,不再复用 Hotmail 模式作为启停条件。
|
||||||
- `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂。
|
- `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂。
|
||||||
- `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。
|
- `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。
|
||||||
@@ -162,7 +163,7 @@
|
|||||||
- `tests/microsoft-email.test.js`:测试 Microsoft 邮件拉取与验证码提取逻辑。
|
- `tests/microsoft-email.test.js`:测试 Microsoft 邮件拉取与验证码提取逻辑。
|
||||||
- `tests/sidepanel-hotmail-manager.test.js`:测试侧边栏 Hotmail 管理器模块接线与空态渲染。
|
- `tests/sidepanel-hotmail-manager.test.js`:测试侧边栏 Hotmail 管理器模块接线与空态渲染。
|
||||||
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线,以及直接打开账号贡献上传页的最小行为。
|
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线,以及直接打开账号贡献上传页的最小行为。
|
||||||
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏邮箱记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
|
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏邮箱记录覆盖层的 HTML 接入、helper 地址归一化、筛选/多选删除交互,以及确认弹窗层级高于记录覆盖层。
|
||||||
- `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。
|
- `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。
|
||||||
- `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析逻辑。
|
- `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析逻辑。
|
||||||
- `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
|
- `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
|
||||||
|
|||||||
Reference in New Issue
Block a user