feat: 添加停止记录推断逻辑,支持归一化步骤状态并更新相关测试
This commit is contained in:
+83
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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',
|
||||
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();
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
+4
-4
@@ -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 标识
|
||||
|
||||
+5
-4
@@ -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 管理器模块接线与空态渲染。
|
||||
|
||||
Reference in New Issue
Block a user