feat: 添加停止记录推断逻辑,支持归一化步骤状态并更新相关测试

This commit is contained in:
QLHazyCoder
2026-04-19 16:54:15 +08:00
parent 2fe04f6f22
commit 17e1c672f1
8 changed files with 379 additions and 19 deletions
+83 -1
View File
@@ -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;
} }
+14 -5
View File
@@ -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 () => {
+237
View File
@@ -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();
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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 管理器模块接线与空态渲染。