feat: 添加自动运行功能及相关界面,支持启动前倒计时和计划管理

This commit is contained in:
QLHazyCoder
2026-04-12 12:25:24 +08:00
parent 3de2fd2c6f
commit 609dd26f08
5 changed files with 632 additions and 18 deletions
+332 -7
View File
@@ -8,6 +8,9 @@ const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const HUMAN_STEP_DELAY_MIN = 700;
const HUMAN_STEP_DELAY_MAX = 2200;
const STEP7_RESTART_MAX_ROUNDS = 8;
const AUTO_RUN_ALARM_NAME = 'scheduled-auto-run';
const AUTO_RUN_DELAY_MIN_MINUTES = 1;
const AUTO_RUN_DELAY_MAX_MINUTES = 1440;
initializeSessionStorageAccess();
@@ -20,6 +23,8 @@ const PERSISTED_SETTING_DEFAULTS = {
vpsPassword: '', // VPS 面板登录密码,可手动填写。
customPassword: '', // 自定义账号密码;留空时由程序自动生成随机密码。
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。
mailProvider: '163', // 验证码邮箱来源,当前支持 163 / inbucket。
inbucketHost: '', // 仅当 mailProvider 为 inbucket 时填写 Inbucket 地址,其他情况保持为空。
inbucketMailbox: '', // 仅当 mailProvider 为 inbucket 时填写邮箱名,其他情况保持为空。
@@ -51,14 +56,49 @@ const DEFAULT_STATE = {
autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。
autoRunTotalRuns: 1, // 自动运行计划总轮数。
autoRunAttemptRun: 0, // 当前轮次的重试序号。
scheduledAutoRunAt: null, // 自动运行计划启动时间戳。
scheduledAutoRunPlan: null, // 自动运行计划参数快照。
};
function normalizeAutoRunDelayMinutes(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return PERSISTED_SETTING_DEFAULTS.autoRunDelayMinutes;
}
return Math.min(
AUTO_RUN_DELAY_MAX_MINUTES,
Math.max(AUTO_RUN_DELAY_MIN_MINUTES, Math.floor(numeric))
);
}
function normalizeRunCount(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return 1;
}
return Math.min(50, Math.max(1, Math.floor(numeric)));
}
function normalizeScheduledAutoRunPlan(plan) {
if (!plan || typeof plan !== 'object') {
return null;
}
return {
totalRuns: normalizeRunCount(plan.totalRuns),
autoRunSkipFailures: Boolean(plan.autoRunSkipFailures),
mode: plan.mode === 'continue' ? 'continue' : 'restart',
};
}
async function getPersistedSettings() {
const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS);
return {
...PERSISTED_SETTING_DEFAULTS,
...stored,
autoRunSkipFailures: Boolean(stored.autoRunSkipFailures ?? PERSISTED_SETTING_DEFAULTS.autoRunSkipFailures),
autoRunDelayEnabled: Boolean(stored.autoRunDelayEnabled ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayEnabled),
autoRunDelayMinutes: normalizeAutoRunDelayMinutes(stored.autoRunDelayMinutes ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayMinutes),
};
}
@@ -92,9 +132,13 @@ async function setPersistentSettings(updates) {
const persistedUpdates = {};
for (const key of PERSISTED_SETTING_KEYS) {
if (updates[key] !== undefined) {
persistedUpdates[key] = key === 'autoRunSkipFailures'
? Boolean(updates[key])
: updates[key];
if (key === 'autoRunSkipFailures' || key === 'autoRunDelayEnabled') {
persistedUpdates[key] = Boolean(updates[key]);
} else if (key === 'autoRunDelayMinutes') {
persistedUpdates[key] = normalizeAutoRunDelayMinutes(updates[key]);
} else {
persistedUpdates[key] = updates[key];
}
}
}
@@ -1074,7 +1118,11 @@ function getAutoRunStatusPayload(phase, payload = {}) {
const currentRun = payload.currentRun ?? autoRunCurrentRun;
const totalRuns = payload.totalRuns ?? autoRunTotalRuns;
const attemptRun = payload.attemptRun ?? autoRunAttemptRun;
const autoRunning = phase === 'running' || phase === 'waiting_email' || phase === 'retrying';
const rawScheduledAt = phase === 'scheduled'
? (payload.scheduledAt ?? payload.scheduledAutoRunAt ?? null)
: null;
const scheduledAt = rawScheduledAt === null ? null : Number(rawScheduledAt);
const autoRunning = phase === 'scheduled' || phase === 'running' || phase === 'waiting_email' || phase === 'retrying';
return {
autoRunning,
@@ -1082,18 +1130,26 @@ function getAutoRunStatusPayload(phase, payload = {}) {
autoRunCurrentRun: currentRun,
autoRunTotalRuns: totalRuns,
autoRunAttemptRun: attemptRun,
scheduledAutoRunAt: Number.isFinite(scheduledAt) ? scheduledAt : null,
};
}
async function broadcastAutoRunStatus(phase, payload = {}) {
async function broadcastAutoRunStatus(phase, payload = {}, extraState = {}) {
const rawScheduledAt = phase === 'scheduled'
? (payload.scheduledAt ?? payload.scheduledAutoRunAt ?? null)
: null;
const statusPayload = {
phase,
currentRun: payload.currentRun ?? autoRunCurrentRun,
totalRuns: payload.totalRuns ?? autoRunTotalRuns,
attemptRun: payload.attemptRun ?? autoRunAttemptRun,
scheduledAt: rawScheduledAt === null ? null : Number(rawScheduledAt),
};
await setState(getAutoRunStatusPayload(phase, statusPayload));
await setState({
...extraState,
...getAutoRunStatusPayload(phase, statusPayload),
});
chrome.runtime.sendMessage({
type: 'AUTO_RUN_STATUS',
payload: statusPayload,
@@ -1108,6 +1164,202 @@ function isAutoRunPausedState(state) {
return Boolean(state.autoRunning) && state.autoRunPhase === 'waiting_email';
}
function isAutoRunScheduledState(state) {
const scheduledAt = state.scheduledAutoRunAt === null ? null : Number(state.scheduledAutoRunAt);
return Boolean(state.autoRunning)
&& state.autoRunPhase === 'scheduled'
&& Number.isFinite(scheduledAt)
&& Boolean(normalizeScheduledAutoRunPlan(state.scheduledAutoRunPlan));
}
function formatAutoRunScheduleTime(timestamp) {
return new Date(timestamp).toLocaleString('zh-CN', {
hour12: false,
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
}
async function ensureScheduledAutoRunAlarm(scheduledAt) {
if (!Number.isFinite(scheduledAt) || scheduledAt <= Date.now()) {
return false;
}
const existingAlarm = await chrome.alarms.get(AUTO_RUN_ALARM_NAME);
if (!existingAlarm || Math.abs((existingAlarm.scheduledTime || 0) - scheduledAt) > 1000) {
await chrome.alarms.clear(AUTO_RUN_ALARM_NAME);
await chrome.alarms.create(AUTO_RUN_ALARM_NAME, { when: scheduledAt });
}
return true;
}
async function clearScheduledAutoRunAlarm() {
await chrome.alarms.clear(AUTO_RUN_ALARM_NAME);
}
async function scheduleAutoRun(totalRuns, options = {}) {
const state = await getState();
if (isAutoRunLockedState(state) || isAutoRunPausedState(state) || autoRunActive) {
throw new Error('自动运行已在进行中,请先停止后再重新计划。');
}
if (isAutoRunScheduledState(state)) {
throw new Error('已有自动运行倒计时计划,请先取消或立即开始。');
}
const delayMinutes = normalizeAutoRunDelayMinutes(options.delayMinutes);
const plan = normalizeScheduledAutoRunPlan({
totalRuns,
autoRunSkipFailures: options.autoRunSkipFailures,
mode: options.mode,
});
const scheduledAt = Date.now() + delayMinutes * 60 * 1000;
autoRunCurrentRun = 0;
autoRunTotalRuns = plan.totalRuns;
autoRunAttemptRun = 0;
await ensureScheduledAutoRunAlarm(scheduledAt);
await broadcastAutoRunStatus(
'scheduled',
{
currentRun: 0,
totalRuns: plan.totalRuns,
attemptRun: 0,
scheduledAt,
},
{
autoRunSkipFailures: plan.autoRunSkipFailures,
scheduledAutoRunPlan: plan,
}
);
await addLog(
`自动运行已计划:${delayMinutes} 分钟后启动(${formatAutoRunScheduleTime(scheduledAt)}),目标 ${plan.totalRuns} 轮。`,
'info'
);
return { ok: true, scheduledAt };
}
let scheduledAutoRunLaunching = false;
async function launchScheduledAutoRun(trigger = 'alarm') {
if (scheduledAutoRunLaunching) {
return false;
}
scheduledAutoRunLaunching = true;
try {
const state = await getState();
if (!isAutoRunScheduledState(state)) {
return false;
}
if (autoRunActive) {
return false;
}
const plan = normalizeScheduledAutoRunPlan(state.scheduledAutoRunPlan);
if (!plan) {
await clearScheduledAutoRunAlarm();
await broadcastAutoRunStatus('idle', {
currentRun: 0,
totalRuns: 1,
attemptRun: 0,
}, {
scheduledAutoRunPlan: null,
});
return false;
}
await clearScheduledAutoRunAlarm();
await broadcastAutoRunStatus(
'running',
{
currentRun: 0,
totalRuns: plan.totalRuns,
attemptRun: 0,
},
{
autoRunSkipFailures: plan.autoRunSkipFailures,
scheduledAutoRunPlan: null,
}
);
clearStopRequest();
await addLog(
trigger === 'manual'
? '已手动跳过倒计时,自动运行立即开始。'
: '倒计时结束,自动运行开始执行。',
'info'
);
autoRunLoop(plan.totalRuns, {
autoRunSkipFailures: plan.autoRunSkipFailures,
mode: plan.mode,
});
return true;
} finally {
scheduledAutoRunLaunching = false;
}
}
async function cancelScheduledAutoRun(options = {}) {
const state = await getState();
if (!isAutoRunScheduledState(state)) {
return false;
}
const plan = normalizeScheduledAutoRunPlan(state.scheduledAutoRunPlan);
await clearScheduledAutoRunAlarm();
autoRunCurrentRun = 0;
autoRunTotalRuns = plan?.totalRuns || 1;
autoRunAttemptRun = 0;
await broadcastAutoRunStatus(
'idle',
{
currentRun: 0,
totalRuns: plan?.totalRuns || 1,
attemptRun: 0,
},
{
scheduledAutoRunPlan: null,
}
);
if (options.logMessage !== false) {
await addLog(options.logMessage || '已取消自动运行倒计时计划。', 'warn');
}
return true;
}
async function restoreScheduledAutoRunIfNeeded() {
const state = await getState();
if (state.autoRunPhase !== 'scheduled') {
return;
}
const plan = normalizeScheduledAutoRunPlan(state.scheduledAutoRunPlan);
const scheduledAt = state.scheduledAutoRunAt === null ? null : Number(state.scheduledAutoRunAt);
if (!plan || !Number.isFinite(scheduledAt)) {
await clearScheduledAutoRunAlarm();
await broadcastAutoRunStatus('idle', {
currentRun: 0,
totalRuns: 1,
attemptRun: 0,
}, {
scheduledAutoRunPlan: null,
});
return;
}
if (scheduledAt <= Date.now()) {
await launchScheduledAutoRun('restore');
return;
}
await ensureScheduledAutoRunAlarm(scheduledAt);
}
async function ensureManualInteractionAllowed(actionLabel) {
const state = await getState();
@@ -1117,6 +1369,9 @@ async function ensureManualInteractionAllowed(actionLabel) {
if (isAutoRunPausedState(state)) {
throw new Error(`自动流程当前已暂停。请点击“继续”,或先确认接管自动流程后再${actionLabel}`);
}
if (isAutoRunScheduledState(state)) {
throw new Error(`自动流程已计划启动。请先取消计划,或立即开始后再${actionLabel}`);
}
return state;
}
@@ -1313,6 +1568,7 @@ async function handleMessage(message, sender) {
case 'RESET': {
clearStopRequest();
await clearScheduledAutoRunAlarm();
await resetState();
await addLog('流程已重置', 'info');
return { ok: true };
@@ -1337,7 +1593,11 @@ async function handleMessage(message, sender) {
case 'AUTO_RUN': {
clearStopRequest();
const totalRuns = message.payload?.totalRuns || 1;
const state = await getState();
if (isAutoRunScheduledState(state)) {
throw new Error('已有自动运行倒计时计划,请先取消或立即开始。');
}
const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1);
const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures);
const mode = message.payload?.mode === 'continue' ? 'continue' : 'restart';
await setState({ autoRunSkipFailures });
@@ -1345,6 +1605,33 @@ async function handleMessage(message, sender) {
return { ok: true };
}
case 'SCHEDULE_AUTO_RUN': {
clearStopRequest();
const totalRuns = normalizeRunCount(message.payload?.totalRuns || 1);
return await scheduleAutoRun(totalRuns, {
delayMinutes: message.payload?.delayMinutes,
autoRunSkipFailures: Boolean(message.payload?.autoRunSkipFailures),
mode: message.payload?.mode,
});
}
case 'START_SCHEDULED_AUTO_RUN_NOW': {
clearStopRequest();
const started = await launchScheduledAutoRun('manual');
if (!started) {
throw new Error('当前没有可立即开始的倒计时计划。');
}
return { ok: true };
}
case 'CANCEL_SCHEDULED_AUTO_RUN': {
const cancelled = await cancelScheduledAutoRun();
if (!cancelled) {
throw new Error('当前没有可取消的倒计时计划。');
}
return { ok: true };
}
case 'RESUME_AUTO_RUN': {
clearStopRequest();
if (message.payload.email) {
@@ -1371,6 +1658,8 @@ async function handleMessage(message, sender) {
if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword;
if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures);
if (message.payload.autoRunDelayEnabled !== undefined) updates.autoRunDelayEnabled = Boolean(message.payload.autoRunDelayEnabled);
if (message.payload.autoRunDelayMinutes !== undefined) updates.autoRunDelayMinutes = normalizeAutoRunDelayMinutes(message.payload.autoRunDelayMinutes);
if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
@@ -1516,6 +1805,17 @@ async function markRunningStepsStopped() {
async function requestStop(options = {}) {
const { logMessage = '已收到停止请求,正在取消当前操作...' } = options;
const state = await getState();
if (isAutoRunScheduledState(state) && !autoRunActive) {
await cancelScheduledAutoRun({
logMessage: options.logMessage === false
? false
: (options.logMessage || '已取消自动运行倒计时计划。'),
});
return;
}
if (stopRequested) return;
stopRequested = true;
@@ -3004,3 +3304,28 @@ async function executeStep9(state) {
// ============================================================
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name !== AUTO_RUN_ALARM_NAME) {
return;
}
launchScheduledAutoRun('alarm').catch((err) => {
console.error(LOG_PREFIX, 'Failed to launch scheduled auto run from alarm:', err);
});
});
chrome.runtime.onStartup.addListener(() => {
restoreScheduledAutoRunIfNeeded().catch((err) => {
console.error(LOG_PREFIX, 'Failed to restore scheduled auto run on startup:', err);
});
});
chrome.runtime.onInstalled.addListener(() => {
restoreScheduledAutoRunIfNeeded().catch((err) => {
console.error(LOG_PREFIX, 'Failed to restore scheduled auto run on install/update:', err);
});
});
restoreScheduledAutoRunIfNeeded().catch((err) => {
console.error(LOG_PREFIX, 'Failed to restore scheduled auto run:', err);
});