From 17e1c672f1c78ccfbdd2ad8b5e89c3236f6dc7f3 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 19 Apr 2026 16:54:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=81=9C=E6=AD=A2?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=8E=A8=E6=96=AD=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=BD=92=E4=B8=80=E5=8C=96=E6=AD=A5=E9=AA=A4?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=B9=B6=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 84 ++++++- background/account-run-history.js | 19 +- ...kground-account-run-history-module.test.js | 32 ++- tests/background-stop-gap-record.test.js | 237 ++++++++++++++++++ .../sidepanel-account-records-manager.test.js | 3 +- tests/step8-stop-cleanup.test.js | 6 + 项目完整链路说明.md | 8 +- 项目文件结构说明.md | 9 +- 8 files changed, 379 insertions(+), 19 deletions(-) create mode 100644 tests/background-stop-gap-record.test.js diff --git a/background.js b/background.js index 03e822f..b614309 100644 --- a/background.js +++ b/background.js @@ -4064,6 +4064,79 @@ function getRunningSteps(statuses = {}) { .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 = {}) { const normalizedPayload = { ...payload, @@ -4932,6 +5005,8 @@ async function markRunningStepsStopped() { async function requestStop(options = {}) { const { logMessage = '已收到停止请求,正在取消当前操作...' } = options; const state = await getState(); + const runningSteps = getRunningSteps(state.stepStatuses); + const inferredStopStep = inferStoppedRecordStep(state); const timerPlan = getPendingAutoRunTimerPlan(state); if (timerPlan?.kind === AUTO_RUN_TIMER_KIND_SCHEDULED_START && !autoRunActive) { @@ -4979,6 +5054,10 @@ async function requestStop(options = {}) { await addLog(logMessage, 'warn'); await broadcastStopToContentScripts(); + if (!runningSteps.length && Number.isInteger(inferredStopStep) && inferredStopStep > 0) { + await appendAndBroadcastAccountRunRecord('stopped', state, STOP_ERROR_MESSAGE); + } + for (const waiter of stepWaiters.values()) { waiter.reject(new Error(STOP_ERROR_MESSAGE)); } @@ -5208,7 +5287,10 @@ async function appendAndBroadcastAccountRunRecord(status, stateOverride = 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) { return null; } diff --git a/background/account-run-history.js b/background/account-run-history.js index a855907..415764d 100644 --- a/background/account-run-history.js +++ b/background/account-run-history.js @@ -39,9 +39,9 @@ return ''; } - function extractFailedStep(status = '', detail = '') { + function extractRecordStep(status = '', detail = '') { const normalizedStatus = String(status || '').trim().toLowerCase(); - const statusMatch = normalizedStatus.match(/^step(\d+)_failed$/); + const statusMatch = normalizedStatus.match(/^step(\d+)_(?:failed|stopped)$/); if (statusMatch) { const step = Number(statusMatch[1]); return Number.isInteger(step) && step > 0 ? step : null; @@ -73,6 +73,9 @@ return '流程完成'; } if (finalStatus === 'stopped') { + if (Number.isInteger(failedStep) && failedStep > 0) { + return `步骤 ${failedStep} 停止`; + } return '流程已停止'; } if (finalStatus !== 'failed') { @@ -152,7 +155,7 @@ const failedStepCandidate = Number(record.failedStep); const failedStep = Number.isInteger(failedStepCandidate) && failedStepCandidate > 0 ? failedStepCandidate - : extractFailedStep(record.finalStatus || record.status || '', failureDetail); + : extractRecordStep(record.finalStatus || record.status || '', failureDetail); const autoRunContext = normalizeAutoRunContext(record.autoRunContext); const retryCount = normalizeRetryCount( record.retryCount !== undefined @@ -160,6 +163,8 @@ : ((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), @@ -168,7 +173,9 @@ finalStatus, finishedAt, retryCount, - failureLabel: String(record.failureLabel || '').trim() || buildFailureLabel(finalStatus, failedStep, failureDetail), + failureLabel: finalStatus === 'stopped' + ? computedFailureLabel + : (rawFailureLabel || computedFailureLabel), failureDetail, failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null, source, @@ -215,7 +222,9 @@ } 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 autoRunContext = source === 'auto' ? buildAutoRunContextFromState(state) : null; const retryCount = source === 'auto' ? getRetryCountFromState(state) : 0; diff --git a/tests/background-account-run-history-module.test.js b/tests/background-account-run-history-module.test.js index 1b428c1..4345d6a 100644 --- a/tests/background-account-run-history-module.test.js +++ b/tests/background-account-run-history-module.test.js @@ -100,18 +100,42 @@ test('account run history helper upgrades old records, keeps stopped items and s assert.equal(fetchCalled, 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); - 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.email, 'a@b.com'); assert.equal(stoppedRecord.password, 'x'); assert.equal(stoppedRecord.finalStatus, 'stopped'); assert.equal(stoppedRecord.retryCount, 0); - assert.equal(stoppedRecord.failureLabel, '流程已停止'); - assert.equal(stoppedRecord.failureDetail, 'stop'); - assert.equal(stoppedRecord.failedStep, null); + assert.equal(stoppedRecord.failureLabel, '步骤 7 停止'); + assert.equal(stoppedRecord.failureDetail, '步骤 7 已被用户停止'); + assert.equal(stoppedRecord.failedStep, 7); assert.equal(stoppedRecord.source, 'manual'); assert.equal(stoppedRecord.autoRunContext, null); 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 () => { diff --git a/tests/background-stop-gap-record.test.js b/tests/background-stop-gap-record.test.js new file mode 100644 index 0000000..2a10935 --- /dev/null +++ b/tests/background-stop-gap-record.test.js @@ -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); +}); diff --git a/tests/sidepanel-account-records-manager.test.js b/tests/sidepanel-account-records-manager.test.js index 4d884d7..49a56d6 100644 --- a/tests/sidepanel-account-records-manager.test.js +++ b/tests/sidepanel-account-records-manager.test.js @@ -207,7 +207,7 @@ test('account records manager supports filter chips and partial multi-select del finalStatus: 'stopped', finishedAt: '2026-04-17T04:28:00.000Z', 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.match(list.innerHTML, /failed@example\.com/); assert.match(list.innerHTML, /stopped@example\.com/); + assert.match(list.innerHTML, /步骤 7 停止/); btnToggleAccountRecordsSelection.listeners.click(); diff --git a/tests/step8-stop-cleanup.test.js b/tests/step8-stop-cleanup.test.js index e49ca4e..587d973 100644 --- a/tests/step8-stop-cleanup.test.js +++ b/tests/step8-stop-cleanup.test.js @@ -58,6 +58,8 @@ const helperBundle = [ extractFunction(helperSource, 'cleanupStep8NavigationListeners'), extractFunction(helperSource, 'rejectPendingStep8'), extractFunction(helperSource, 'throwIfStep8SettledOrStopped'), + extractFunction(helperSource, 'getRunningSteps'), + extractFunction(helperSource, 'inferStoppedRecordStep'), extractFunction(helperSource, 'requestStop'), ].join('\n'); @@ -75,6 +77,9 @@ let autoRunTotalRuns = 3; let autoRunAttemptRun = 4; let autoRunSessionId = 99; 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_MAX_ROUNDS = 5; const STEP8_READY_WAIT_TIMEOUT_MS = 30000; @@ -137,6 +142,7 @@ async function addLog() {} async function broadcastStopToContentScripts() {} async function markRunningStepsStopped() {} async function broadcastAutoRunStatus() {} +async function appendAndBroadcastAccountRunRecord() {} async function getState() { return { autoRunning: false }; } diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 90385bf..985c603 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -38,7 +38,7 @@ - 接收后台广播并更新 UI - 动态渲染步骤列表 - 顶部提供一个临时 `贡献` 按钮,用于直接打开账号贡献上传页 -- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表 +- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计、按状态筛选、分页列表与多选删除操作 - 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Pro` 与 legacy `v` 两个版本族,排序时优先保持版本族语义一致,同时会在读取缓存后重新排序,避免旧缓存把 `v` 版本误显示为比 `Pro` 更新 ### 2.2 Background Service Worker @@ -144,7 +144,7 @@ - iCloud 相关偏好 - LuckMail API 配置 - 自动运行默认配置 -- 账号运行历史 `accountRunHistory`(以邮箱为主键,保存该邮箱最近一次状态:成功/失败/停止) +- 账号运行历史 `accountRunHistory`(以邮箱为主键,保存该邮箱最近一次状态:成功/失败/停止;停止记录会优先归一到具体步骤,如“步骤 7 停止”) - 账号运行历史本地同步开关与 helper 地址 当启用了独立的账号运行历史本地同步配置时,账号运行历史会通过 [scripts/hotmail_helper.py](c:/Users/projectf/Downloads/codex注册扩展/scripts/hotmail_helper.py) 整体同步写入 `data/account-run-history.json` 快照文件,便于开发者直接查看完整记录。 @@ -238,7 +238,7 @@ 1. 解析本轮应使用的邮箱 2. 打开或复用注册页 3. 点击注册入口并提交邮箱 -4. 以当前邮箱先写入一条“停止(流程尚未完成)”的记录占位 +4. 以当前邮箱先写入一条“停止(流程尚未完成)”的记录占位;该占位记录后续会随真实停止点归一成“步骤 X 停止”或被成功/失败状态覆盖 5. 等待邮箱提交后的真实落地页 6. 如果进入密码页,则继续执行 Step 3 7. 如果直接进入邮箱验证码页,则自动跳过 Step 3 并进入 Step 4 @@ -495,7 +495,7 @@ - 当前轮重试 - 下一轮继续 7. 当前轮最终失败时,写入邮箱记录,并在自动重试链路中累计重试次数 - - 手动停止与自动停止会写入“停止”状态(若后续同邮箱成功/失败,会被覆盖为最新状态) + - 手动停止与自动停止会写入“停止”状态;如果能确定当前运行步骤或下一未完成步骤,则会优先归一成“步骤 X 停止”(若后续同邮箱成功/失败,会被覆盖为最新状态) 8. 如果配置了线程间隔,则挂计时计划;计时计划会带上当前 `autoRunSessionId` 9. 旧 timer / 旧 alarm / 旧恢复入口只有在 session 仍有效时才允许恢复执行;Stop 会立即使当前 session 失效,防止“停止后旧倒计时又把流程重新拉起” 10. 所有轮次结束后输出汇总,并清空当前 session 标识 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 4d90214..9aab010 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -40,7 +40,7 @@ ## `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/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口。 - `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。 @@ -112,7 +112,7 @@ - `sidepanel/hotmail-manager.js`:侧边栏 Hotmail 账号池管理器,负责列表、导入、验证、测试收信和批量操作。 - `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。 - `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。 -- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。 +- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要、按状态筛选、多选删除、清理确认,以及多层弹窗下的工具栏交互状态管理。 - `sidepanel/sidepanel.css`:侧边栏样式文件。 - `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增临时 `贡献` 按钮用于直接打开账号贡献上传页,同时继续加载 `managed-alias-utils.js`,把旧的“邮箱前缀”字段语义改为“别名基邮箱”。 - `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-step-random-delay.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-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂。 - `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。 @@ -162,7 +163,7 @@ - `tests/microsoft-email.test.js`:测试 Microsoft 邮件拉取与验证码提取逻辑。 - `tests/sidepanel-hotmail-manager.test.js`:测试侧边栏 Hotmail 管理器模块接线与空态渲染。 - `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-provider.test.js`:测试侧边栏 iCloud 登录地址解析逻辑。 - `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。