feat: 添加自动运行功能及相关界面,支持启动前倒计时和计划管理
This commit is contained in:
+332
-7
@@ -8,6 +8,9 @@ const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
|||||||
const HUMAN_STEP_DELAY_MIN = 700;
|
const HUMAN_STEP_DELAY_MIN = 700;
|
||||||
const HUMAN_STEP_DELAY_MAX = 2200;
|
const HUMAN_STEP_DELAY_MAX = 2200;
|
||||||
const STEP7_RESTART_MAX_ROUNDS = 8;
|
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();
|
initializeSessionStorageAccess();
|
||||||
|
|
||||||
@@ -20,6 +23,8 @@ const PERSISTED_SETTING_DEFAULTS = {
|
|||||||
vpsPassword: '', // VPS 面板登录密码,可手动填写。
|
vpsPassword: '', // VPS 面板登录密码,可手动填写。
|
||||||
customPassword: '', // 自定义账号密码;留空时由程序自动生成随机密码。
|
customPassword: '', // 自定义账号密码;留空时由程序自动生成随机密码。
|
||||||
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
|
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
|
||||||
|
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
|
||||||
|
autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。
|
||||||
mailProvider: '163', // 验证码邮箱来源,当前支持 163 / inbucket。
|
mailProvider: '163', // 验证码邮箱来源,当前支持 163 / inbucket。
|
||||||
inbucketHost: '', // 仅当 mailProvider 为 inbucket 时填写 Inbucket 地址,其他情况保持为空。
|
inbucketHost: '', // 仅当 mailProvider 为 inbucket 时填写 Inbucket 地址,其他情况保持为空。
|
||||||
inbucketMailbox: '', // 仅当 mailProvider 为 inbucket 时填写邮箱名,其他情况保持为空。
|
inbucketMailbox: '', // 仅当 mailProvider 为 inbucket 时填写邮箱名,其他情况保持为空。
|
||||||
@@ -51,14 +56,49 @@ const DEFAULT_STATE = {
|
|||||||
autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。
|
autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。
|
||||||
autoRunTotalRuns: 1, // 自动运行计划总轮数。
|
autoRunTotalRuns: 1, // 自动运行计划总轮数。
|
||||||
autoRunAttemptRun: 0, // 当前轮次的重试序号。
|
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() {
|
async function getPersistedSettings() {
|
||||||
const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS);
|
const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS);
|
||||||
return {
|
return {
|
||||||
...PERSISTED_SETTING_DEFAULTS,
|
...PERSISTED_SETTING_DEFAULTS,
|
||||||
...stored,
|
...stored,
|
||||||
autoRunSkipFailures: Boolean(stored.autoRunSkipFailures ?? PERSISTED_SETTING_DEFAULTS.autoRunSkipFailures),
|
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 = {};
|
const persistedUpdates = {};
|
||||||
for (const key of PERSISTED_SETTING_KEYS) {
|
for (const key of PERSISTED_SETTING_KEYS) {
|
||||||
if (updates[key] !== undefined) {
|
if (updates[key] !== undefined) {
|
||||||
persistedUpdates[key] = key === 'autoRunSkipFailures'
|
if (key === 'autoRunSkipFailures' || key === 'autoRunDelayEnabled') {
|
||||||
? Boolean(updates[key])
|
persistedUpdates[key] = Boolean(updates[key]);
|
||||||
: 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 currentRun = payload.currentRun ?? autoRunCurrentRun;
|
||||||
const totalRuns = payload.totalRuns ?? autoRunTotalRuns;
|
const totalRuns = payload.totalRuns ?? autoRunTotalRuns;
|
||||||
const attemptRun = payload.attemptRun ?? autoRunAttemptRun;
|
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 {
|
return {
|
||||||
autoRunning,
|
autoRunning,
|
||||||
@@ -1082,18 +1130,26 @@ function getAutoRunStatusPayload(phase, payload = {}) {
|
|||||||
autoRunCurrentRun: currentRun,
|
autoRunCurrentRun: currentRun,
|
||||||
autoRunTotalRuns: totalRuns,
|
autoRunTotalRuns: totalRuns,
|
||||||
autoRunAttemptRun: attemptRun,
|
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 = {
|
const statusPayload = {
|
||||||
phase,
|
phase,
|
||||||
currentRun: payload.currentRun ?? autoRunCurrentRun,
|
currentRun: payload.currentRun ?? autoRunCurrentRun,
|
||||||
totalRuns: payload.totalRuns ?? autoRunTotalRuns,
|
totalRuns: payload.totalRuns ?? autoRunTotalRuns,
|
||||||
attemptRun: payload.attemptRun ?? autoRunAttemptRun,
|
attemptRun: payload.attemptRun ?? autoRunAttemptRun,
|
||||||
|
scheduledAt: rawScheduledAt === null ? null : Number(rawScheduledAt),
|
||||||
};
|
};
|
||||||
|
|
||||||
await setState(getAutoRunStatusPayload(phase, statusPayload));
|
await setState({
|
||||||
|
...extraState,
|
||||||
|
...getAutoRunStatusPayload(phase, statusPayload),
|
||||||
|
});
|
||||||
chrome.runtime.sendMessage({
|
chrome.runtime.sendMessage({
|
||||||
type: 'AUTO_RUN_STATUS',
|
type: 'AUTO_RUN_STATUS',
|
||||||
payload: statusPayload,
|
payload: statusPayload,
|
||||||
@@ -1108,6 +1164,202 @@ function isAutoRunPausedState(state) {
|
|||||||
return Boolean(state.autoRunning) && state.autoRunPhase === 'waiting_email';
|
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) {
|
async function ensureManualInteractionAllowed(actionLabel) {
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
|
|
||||||
@@ -1117,6 +1369,9 @@ async function ensureManualInteractionAllowed(actionLabel) {
|
|||||||
if (isAutoRunPausedState(state)) {
|
if (isAutoRunPausedState(state)) {
|
||||||
throw new Error(`自动流程当前已暂停。请点击“继续”,或先确认接管自动流程后再${actionLabel}。`);
|
throw new Error(`自动流程当前已暂停。请点击“继续”,或先确认接管自动流程后再${actionLabel}。`);
|
||||||
}
|
}
|
||||||
|
if (isAutoRunScheduledState(state)) {
|
||||||
|
throw new Error(`自动流程已计划启动。请先取消计划,或立即开始后再${actionLabel}。`);
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -1313,6 +1568,7 @@ async function handleMessage(message, sender) {
|
|||||||
|
|
||||||
case 'RESET': {
|
case 'RESET': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
|
await clearScheduledAutoRunAlarm();
|
||||||
await resetState();
|
await resetState();
|
||||||
await addLog('流程已重置', 'info');
|
await addLog('流程已重置', 'info');
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
@@ -1337,7 +1593,11 @@ async function handleMessage(message, sender) {
|
|||||||
|
|
||||||
case 'AUTO_RUN': {
|
case 'AUTO_RUN': {
|
||||||
clearStopRequest();
|
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 autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures);
|
||||||
const mode = message.payload?.mode === 'continue' ? 'continue' : 'restart';
|
const mode = message.payload?.mode === 'continue' ? 'continue' : 'restart';
|
||||||
await setState({ autoRunSkipFailures });
|
await setState({ autoRunSkipFailures });
|
||||||
@@ -1345,6 +1605,33 @@ async function handleMessage(message, sender) {
|
|||||||
return { ok: true };
|
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': {
|
case 'RESUME_AUTO_RUN': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
if (message.payload.email) {
|
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.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword;
|
||||||
if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
|
if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
|
||||||
if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures);
|
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.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
|
||||||
if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
|
if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
|
||||||
if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
|
if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
|
||||||
@@ -1516,6 +1805,17 @@ async function markRunningStepsStopped() {
|
|||||||
|
|
||||||
async function requestStop(options = {}) {
|
async function requestStop(options = {}) {
|
||||||
const { logMessage = '已收到停止请求,正在取消当前操作...' } = 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;
|
if (stopRequested) return;
|
||||||
|
|
||||||
stopRequested = true;
|
stopRequested = true;
|
||||||
@@ -3004,3 +3304,28 @@ async function executeStep9(state) {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"sidePanel",
|
"sidePanel",
|
||||||
|
"alarms",
|
||||||
"tabs",
|
"tabs",
|
||||||
"webNavigation",
|
"webNavigation",
|
||||||
"debugger",
|
"debugger",
|
||||||
|
|||||||
@@ -376,6 +376,35 @@ header {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-delay-inline {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-delay-check {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-delay-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-delay-input {
|
||||||
|
width: 72px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-unit {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Status Bar */
|
/* Status Bar */
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -422,6 +451,12 @@ header {
|
|||||||
}
|
}
|
||||||
.status-bar.paused { color: var(--orange); }
|
.status-bar.paused { color: var(--orange); }
|
||||||
|
|
||||||
|
.status-bar.scheduled .status-dot {
|
||||||
|
background: var(--blue);
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.status-bar.scheduled { color: var(--blue); }
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
50% { opacity: 0.4; transform: scale(0.85); }
|
50% { opacity: 0.4; transform: scale(0.85); }
|
||||||
@@ -441,6 +476,51 @@ header {
|
|||||||
.auto-continue-bar svg { color: var(--orange); flex-shrink: 0; }
|
.auto-continue-bar svg { color: var(--orange); flex-shrink: 0; }
|
||||||
.auto-hint { font-size: 13px; color: var(--orange); flex: 1; font-weight: 500; }
|
.auto-hint { font-size: 13px; color: var(--orange); flex: 1; font-weight: 500; }
|
||||||
|
|
||||||
|
.auto-schedule-bar {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--blue-soft);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-schedule-bar svg {
|
||||||
|
color: var(--blue);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-schedule-copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-schedule-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-schedule-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-schedule-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Steps Section
|
Steps Section
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|||||||
@@ -96,6 +96,28 @@
|
|||||||
<span>出现错误无法继续时,直接丢弃当前线程并新开一轮,直到补足目标运行次数</span>
|
<span>出现错误无法继续时,直接丢弃当前线程并新开一轮,直到补足目标运行次数</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="data-row">
|
||||||
|
<span class="data-label">延迟</span>
|
||||||
|
<div class="data-inline auto-delay-inline">
|
||||||
|
<label class="data-check auto-delay-check" for="input-auto-delay-enabled">
|
||||||
|
<input type="checkbox" id="input-auto-delay-enabled" />
|
||||||
|
<span>启动前倒计时</span>
|
||||||
|
</label>
|
||||||
|
<div class="auto-delay-controls">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="input-auto-delay-minutes"
|
||||||
|
class="data-input auto-delay-input"
|
||||||
|
value="30"
|
||||||
|
min="1"
|
||||||
|
max="1440"
|
||||||
|
step="1"
|
||||||
|
title="启动前倒计时分钟数"
|
||||||
|
/>
|
||||||
|
<span class="data-unit">分钟</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">OAuth</span>
|
<span class="data-label">OAuth</span>
|
||||||
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
|
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
|
||||||
@@ -114,6 +136,20 @@
|
|||||||
<span class="auto-hint">先自动获取 Duck 邮箱,或手动粘贴邮箱后再继续</span>
|
<span class="auto-hint">先自动获取 Duck 邮箱,或手动粘贴邮箱后再继续</span>
|
||||||
<button id="btn-auto-continue" class="btn btn-primary btn-sm">继续</button>
|
<button id="btn-auto-continue" class="btn btn-primary btn-sm">继续</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="auto-schedule-bar" class="auto-schedule-bar" style="display:none;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<polyline points="12 6 12 12 16 14"/>
|
||||||
|
</svg>
|
||||||
|
<div class="auto-schedule-copy">
|
||||||
|
<span id="auto-schedule-title" class="auto-schedule-title">已计划自动运行</span>
|
||||||
|
<span id="auto-schedule-meta" class="auto-schedule-meta">等待倒计时开始...</span>
|
||||||
|
</div>
|
||||||
|
<div class="auto-schedule-actions">
|
||||||
|
<button id="btn-auto-run-now" class="btn btn-primary btn-sm" type="button">立即开始</button>
|
||||||
|
<button id="btn-auto-cancel-schedule" class="btn btn-outline btn-sm" type="button">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="steps-section">
|
<section id="steps-section">
|
||||||
|
|||||||
+183
-11
@@ -27,6 +27,11 @@ const stepsProgress = document.getElementById('steps-progress');
|
|||||||
const btnAutoRun = document.getElementById('btn-auto-run');
|
const btnAutoRun = document.getElementById('btn-auto-run');
|
||||||
const btnAutoContinue = document.getElementById('btn-auto-continue');
|
const btnAutoContinue = document.getElementById('btn-auto-continue');
|
||||||
const autoContinueBar = document.getElementById('auto-continue-bar');
|
const autoContinueBar = document.getElementById('auto-continue-bar');
|
||||||
|
const autoScheduleBar = document.getElementById('auto-schedule-bar');
|
||||||
|
const autoScheduleTitle = document.getElementById('auto-schedule-title');
|
||||||
|
const autoScheduleMeta = document.getElementById('auto-schedule-meta');
|
||||||
|
const btnAutoRunNow = document.getElementById('btn-auto-run-now');
|
||||||
|
const btnAutoCancelSchedule = document.getElementById('btn-auto-cancel-schedule');
|
||||||
const btnClearLog = document.getElementById('btn-clear-log');
|
const btnClearLog = document.getElementById('btn-clear-log');
|
||||||
const inputVpsUrl = document.getElementById('input-vps-url');
|
const inputVpsUrl = document.getElementById('input-vps-url');
|
||||||
const inputVpsPassword = document.getElementById('input-vps-password');
|
const inputVpsPassword = document.getElementById('input-vps-password');
|
||||||
@@ -37,6 +42,8 @@ const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
|
|||||||
const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
|
const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
|
||||||
const inputRunCount = document.getElementById('input-run-count');
|
const inputRunCount = document.getElementById('input-run-count');
|
||||||
const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
|
const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
|
||||||
|
const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
|
||||||
|
const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes');
|
||||||
const autoStartModal = document.getElementById('auto-start-modal');
|
const autoStartModal = document.getElementById('auto-start-modal');
|
||||||
const autoStartTitle = autoStartModal?.querySelector('.modal-title');
|
const autoStartTitle = autoStartModal?.querySelector('.modal-title');
|
||||||
const autoStartMessage = document.getElementById('auto-start-message');
|
const autoStartMessage = document.getElementById('auto-start-message');
|
||||||
@@ -56,6 +63,9 @@ const STEP_DEFAULT_STATUSES = {
|
|||||||
9: 'pending',
|
9: 'pending',
|
||||||
};
|
};
|
||||||
const SKIPPABLE_STEPS = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
const SKIPPABLE_STEPS = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||||
|
const AUTO_DELAY_MIN_MINUTES = 1;
|
||||||
|
const AUTO_DELAY_MAX_MINUTES = 1440;
|
||||||
|
const AUTO_DELAY_DEFAULT_MINUTES = 30;
|
||||||
|
|
||||||
let latestState = null;
|
let latestState = null;
|
||||||
let currentAutoRun = {
|
let currentAutoRun = {
|
||||||
@@ -64,12 +74,14 @@ let currentAutoRun = {
|
|||||||
currentRun: 0,
|
currentRun: 0,
|
||||||
totalRuns: 1,
|
totalRuns: 1,
|
||||||
attemptRun: 0,
|
attemptRun: 0,
|
||||||
|
scheduledAt: null,
|
||||||
};
|
};
|
||||||
let settingsDirty = false;
|
let settingsDirty = false;
|
||||||
let settingsSaveInFlight = false;
|
let settingsSaveInFlight = false;
|
||||||
let settingsAutoSaveTimer = null;
|
let settingsAutoSaveTimer = null;
|
||||||
let modalChoiceResolver = null;
|
let modalChoiceResolver = null;
|
||||||
let currentModalActions = [];
|
let currentModalActions = [];
|
||||||
|
let scheduledCountdownTimer = null;
|
||||||
|
|
||||||
const EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
|
const EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
|
||||||
const EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
|
const EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
|
||||||
@@ -240,7 +252,7 @@ function syncAutoRunState(source = {}) {
|
|||||||
const autoRunning = source.autoRunning !== undefined
|
const autoRunning = source.autoRunning !== undefined
|
||||||
? Boolean(source.autoRunning)
|
? Boolean(source.autoRunning)
|
||||||
: (source.autoRunPhase !== undefined || source.phase !== undefined
|
: (source.autoRunPhase !== undefined || source.phase !== undefined
|
||||||
? ['running', 'waiting_email', 'retrying'].includes(phase)
|
? ['scheduled', 'running', 'waiting_email', 'retrying'].includes(phase)
|
||||||
: currentAutoRun.autoRunning);
|
: currentAutoRun.autoRunning);
|
||||||
|
|
||||||
currentAutoRun = {
|
currentAutoRun = {
|
||||||
@@ -249,6 +261,7 @@ function syncAutoRunState(source = {}) {
|
|||||||
currentRun: source.autoRunCurrentRun ?? source.currentRun ?? currentAutoRun.currentRun,
|
currentRun: source.autoRunCurrentRun ?? source.currentRun ?? currentAutoRun.currentRun,
|
||||||
totalRuns: source.autoRunTotalRuns ?? source.totalRuns ?? currentAutoRun.totalRuns,
|
totalRuns: source.autoRunTotalRuns ?? source.totalRuns ?? currentAutoRun.totalRuns,
|
||||||
attemptRun: source.autoRunAttemptRun ?? source.attemptRun ?? currentAutoRun.attemptRun,
|
attemptRun: source.autoRunAttemptRun ?? source.attemptRun ?? currentAutoRun.attemptRun,
|
||||||
|
scheduledAt: source.scheduledAutoRunAt ?? source.scheduledAt ?? currentAutoRun.scheduledAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +273,14 @@ function isAutoRunPausedPhase() {
|
|||||||
return currentAutoRun.phase === 'waiting_email';
|
return currentAutoRun.phase === 'waiting_email';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAutoRunScheduledPhase() {
|
||||||
|
return currentAutoRun.phase === 'scheduled';
|
||||||
|
}
|
||||||
|
|
||||||
function getAutoRunLabel(payload = currentAutoRun) {
|
function getAutoRunLabel(payload = currentAutoRun) {
|
||||||
|
if ((payload.phase ?? currentAutoRun.phase) === 'scheduled') {
|
||||||
|
return (payload.totalRuns || 1) > 1 ? ` (${payload.totalRuns}轮)` : '';
|
||||||
|
}
|
||||||
const attemptLabel = payload.attemptRun ? ` · 尝试${payload.attemptRun}` : '';
|
const attemptLabel = payload.attemptRun ? ` · 尝试${payload.attemptRun}` : '';
|
||||||
if ((payload.totalRuns || 1) > 1) {
|
if ((payload.totalRuns || 1) > 1) {
|
||||||
return ` (${payload.currentRun}/${payload.totalRuns}${attemptLabel})`;
|
return ` (${payload.currentRun}/${payload.totalRuns}${attemptLabel})`;
|
||||||
@@ -268,6 +288,79 @@ function getAutoRunLabel(payload = currentAutoRun) {
|
|||||||
return attemptLabel ? ` (${attemptLabel.slice(3)})` : '';
|
return attemptLabel ? ` (${attemptLabel.slice(3)})` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAutoDelayMinutes(value) {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isFinite(numeric)) {
|
||||||
|
return AUTO_DELAY_DEFAULT_MINUTES;
|
||||||
|
}
|
||||||
|
return Math.min(AUTO_DELAY_MAX_MINUTES, Math.max(AUTO_DELAY_MIN_MINUTES, Math.floor(numeric)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAutoDelayInputState() {
|
||||||
|
const scheduled = isAutoRunScheduledPhase();
|
||||||
|
inputAutoDelayEnabled.disabled = scheduled;
|
||||||
|
inputAutoDelayMinutes.disabled = scheduled || !inputAutoDelayEnabled.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCountdown(remainingMs) {
|
||||||
|
const totalSeconds = Math.max(0, Math.ceil(remainingMs / 1000));
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatScheduleTime(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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopScheduledCountdownTicker() {
|
||||||
|
clearInterval(scheduledCountdownTimer);
|
||||||
|
scheduledCountdownTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderScheduledAutoRunInfo() {
|
||||||
|
if (!autoScheduleBar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAutoRunScheduledPhase() || !Number.isFinite(currentAutoRun.scheduledAt)) {
|
||||||
|
autoScheduleBar.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingMs = currentAutoRun.scheduledAt - Date.now();
|
||||||
|
autoScheduleBar.style.display = 'flex';
|
||||||
|
autoScheduleTitle.textContent = '已计划自动运行';
|
||||||
|
autoScheduleMeta.textContent = remainingMs > 0
|
||||||
|
? `计划于 ${formatScheduleTime(currentAutoRun.scheduledAt)} 开始,剩余 ${formatCountdown(remainingMs)}`
|
||||||
|
: '倒计时即将结束,正在准备启动...';
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncScheduledCountdownTicker() {
|
||||||
|
renderScheduledAutoRunInfo();
|
||||||
|
if (!isAutoRunScheduledPhase() || !Number.isFinite(currentAutoRun.scheduledAt)) {
|
||||||
|
stopScheduledCountdownTicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduledCountdownTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledCountdownTimer = setInterval(() => {
|
||||||
|
renderScheduledAutoRunInfo();
|
||||||
|
updateStatusDisplay(latestState);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
function setDefaultAutoRunButton() {
|
function setDefaultAutoRunButton() {
|
||||||
btnAutoRun.disabled = false;
|
btnAutoRun.disabled = false;
|
||||||
inputRunCount.disabled = false;
|
inputRunCount.disabled = false;
|
||||||
@@ -283,6 +376,8 @@ function collectSettingsPayload() {
|
|||||||
inbucketHost: inputInbucketHost.value.trim(),
|
inbucketHost: inputInbucketHost.value.trim(),
|
||||||
inbucketMailbox: inputInbucketMailbox.value.trim(),
|
inbucketMailbox: inputInbucketMailbox.value.trim(),
|
||||||
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
||||||
|
autoRunDelayEnabled: inputAutoDelayEnabled.checked,
|
||||||
|
autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,13 +445,23 @@ function applyAutoRunStatus(payload = currentAutoRun) {
|
|||||||
const runLabel = getAutoRunLabel(currentAutoRun);
|
const runLabel = getAutoRunLabel(currentAutoRun);
|
||||||
const locked = isAutoRunLockedPhase();
|
const locked = isAutoRunLockedPhase();
|
||||||
const paused = isAutoRunPausedPhase();
|
const paused = isAutoRunPausedPhase();
|
||||||
|
const scheduled = isAutoRunScheduledPhase();
|
||||||
|
|
||||||
inputRunCount.disabled = currentAutoRun.autoRunning;
|
inputRunCount.disabled = currentAutoRun.autoRunning;
|
||||||
btnAutoRun.disabled = currentAutoRun.autoRunning;
|
btnAutoRun.disabled = currentAutoRun.autoRunning;
|
||||||
btnFetchEmail.disabled = locked;
|
btnFetchEmail.disabled = locked;
|
||||||
inputEmail.disabled = locked;
|
inputEmail.disabled = locked;
|
||||||
|
inputAutoSkipFailures.disabled = scheduled;
|
||||||
|
|
||||||
|
if (currentAutoRun.totalRuns > 0) {
|
||||||
|
inputRunCount.value = String(currentAutoRun.totalRuns);
|
||||||
|
}
|
||||||
|
|
||||||
switch (currentAutoRun.phase) {
|
switch (currentAutoRun.phase) {
|
||||||
|
case 'scheduled':
|
||||||
|
autoContinueBar.style.display = 'none';
|
||||||
|
btnAutoRun.innerHTML = `已计划${runLabel}`;
|
||||||
|
break;
|
||||||
case 'waiting_email':
|
case 'waiting_email':
|
||||||
autoContinueBar.style.display = 'flex';
|
autoContinueBar.style.display = 'flex';
|
||||||
btnAutoRun.innerHTML = `已暂停${runLabel}`;
|
btnAutoRun.innerHTML = `已暂停${runLabel}`;
|
||||||
@@ -379,7 +484,9 @@ function applyAutoRunStatus(payload = currentAutoRun) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStopButtonState(paused || locked || Object.values(getStepStatuses()).some(status => status === 'running'));
|
updateAutoDelayInputState();
|
||||||
|
syncScheduledCountdownTicker();
|
||||||
|
updateStopButtonState(scheduled || paused || locked || Object.values(getStepStatuses()).some(status => status === 'running'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeManualStepActions() {
|
function initializeManualStepActions() {
|
||||||
@@ -451,6 +558,11 @@ async function restoreState() {
|
|||||||
inputInbucketMailbox.value = state.inbucketMailbox;
|
inputInbucketMailbox.value = state.inbucketMailbox;
|
||||||
}
|
}
|
||||||
inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures);
|
inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures);
|
||||||
|
inputAutoDelayEnabled.checked = Boolean(state.autoRunDelayEnabled);
|
||||||
|
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state.autoRunDelayMinutes));
|
||||||
|
if (state.autoRunTotalRuns) {
|
||||||
|
inputRunCount.value = String(state.autoRunTotalRuns);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.stepStatuses) {
|
if (state.stepStatuses) {
|
||||||
for (const [step, status] of Object.entries(state.stepStatuses)) {
|
for (const [step, status] of Object.entries(state.stepStatuses)) {
|
||||||
@@ -466,6 +578,7 @@ async function restoreState() {
|
|||||||
|
|
||||||
applyAutoRunStatus(state);
|
applyAutoRunStatus(state);
|
||||||
markSettingsDirty(false);
|
markSettingsDirty(false);
|
||||||
|
updateAutoDelayInputState();
|
||||||
updateStatusDisplay(latestState);
|
updateStatusDisplay(latestState);
|
||||||
updateProgressCounter();
|
updateProgressCounter();
|
||||||
updateMailProviderUI();
|
updateMailProviderUI();
|
||||||
@@ -518,12 +631,13 @@ function updateButtonStates() {
|
|||||||
const statuses = getStepStatuses();
|
const statuses = getStepStatuses();
|
||||||
const anyRunning = Object.values(statuses).some(s => s === 'running');
|
const anyRunning = Object.values(statuses).some(s => s === 'running');
|
||||||
const autoLocked = isAutoRunLockedPhase();
|
const autoLocked = isAutoRunLockedPhase();
|
||||||
|
const autoScheduled = isAutoRunScheduledPhase();
|
||||||
|
|
||||||
for (let step = 1; step <= 9; step++) {
|
for (let step = 1; step <= 9; step++) {
|
||||||
const btn = document.querySelector(`.step-btn[data-step="${step}"]`);
|
const btn = document.querySelector(`.step-btn[data-step="${step}"]`);
|
||||||
if (!btn) continue;
|
if (!btn) continue;
|
||||||
|
|
||||||
if (anyRunning || autoLocked) {
|
if (anyRunning || autoLocked || autoScheduled) {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
} else if (step === 1) {
|
} else if (step === 1) {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
@@ -539,7 +653,7 @@ function updateButtonStates() {
|
|||||||
const currentStatus = statuses[step];
|
const currentStatus = statuses[step];
|
||||||
const prevStatus = statuses[step - 1];
|
const prevStatus = statuses[step - 1];
|
||||||
|
|
||||||
if (!SKIPPABLE_STEPS.has(step) || anyRunning || autoLocked || currentStatus === 'running' || isDoneStatus(currentStatus)) {
|
if (!SKIPPABLE_STEPS.has(step) || anyRunning || autoLocked || autoScheduled || currentStatus === 'running' || isDoneStatus(currentStatus)) {
|
||||||
btn.style.display = 'none';
|
btn.style.display = 'none';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.title = '当前不可跳过';
|
btn.title = '当前不可跳过';
|
||||||
@@ -558,7 +672,8 @@ function updateButtonStates() {
|
|||||||
btn.title = `跳过步骤 ${step}`;
|
btn.title = `跳过步骤 ${step}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
updateStopButtonState(anyRunning || isAutoRunPausedPhase() || autoLocked);
|
btnReset.disabled = anyRunning || autoScheduled || isAutoRunPausedPhase() || autoLocked;
|
||||||
|
updateStopButtonState(anyRunning || autoScheduled || isAutoRunPausedPhase() || autoLocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStopButtonState(active) {
|
function updateStopButtonState(active) {
|
||||||
@@ -570,6 +685,17 @@ function updateStatusDisplay(state) {
|
|||||||
|
|
||||||
statusBar.className = 'status-bar';
|
statusBar.className = 'status-bar';
|
||||||
|
|
||||||
|
if (isAutoRunScheduledPhase()) {
|
||||||
|
const remainingMs = Number.isFinite(currentAutoRun.scheduledAt)
|
||||||
|
? currentAutoRun.scheduledAt - Date.now()
|
||||||
|
: 0;
|
||||||
|
displayStatus.textContent = remainingMs > 0
|
||||||
|
? `自动计划中,剩余 ${formatCountdown(remainingMs)}`
|
||||||
|
: '倒计时即将结束,正在准备启动...';
|
||||||
|
statusBar.classList.add('scheduled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isAutoRunPausedPhase()) {
|
if (isAutoRunPausedPhase()) {
|
||||||
displayStatus.textContent = `自动已暂停${getAutoRunLabel()},等待邮箱后继续`;
|
displayStatus.textContent = `自动已暂停${getAutoRunLabel()},等待邮箱后继续`;
|
||||||
statusBar.classList.add('paused');
|
statusBar.classList.add('paused');
|
||||||
@@ -818,7 +944,7 @@ btnSaveSettings.addEventListener('click', async () => {
|
|||||||
btnStop.addEventListener('click', async () => {
|
btnStop.addEventListener('click', async () => {
|
||||||
btnStop.disabled = true;
|
btnStop.disabled = true;
|
||||||
await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} });
|
await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} });
|
||||||
showToast('正在停止当前流程...', 'warn', 2000);
|
showToast(isAutoRunScheduledPhase() ? '正在取消倒计时计划...' : '正在停止当前流程...', 'warn', 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
autoStartModal?.addEventListener('click', (event) => {
|
autoStartModal?.addEventListener('click', (event) => {
|
||||||
@@ -831,7 +957,7 @@ btnAutoStartClose?.addEventListener('click', () => resolveModalChoice(null));
|
|||||||
// Auto Run
|
// Auto Run
|
||||||
btnAutoRun.addEventListener('click', async () => {
|
btnAutoRun.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
const totalRuns = parseInt(inputRunCount.value) || 1;
|
const totalRuns = Math.min(50, Math.max(1, parseInt(inputRunCount.value, 10) || 1));
|
||||||
let mode = 'restart';
|
let mode = 'restart';
|
||||||
|
|
||||||
if (shouldOfferAutoModeChoice()) {
|
if (shouldOfferAutoModeChoice()) {
|
||||||
@@ -845,12 +971,18 @@ btnAutoRun.addEventListener('click', async () => {
|
|||||||
|
|
||||||
btnAutoRun.disabled = true;
|
btnAutoRun.disabled = true;
|
||||||
inputRunCount.disabled = true;
|
inputRunCount.disabled = true;
|
||||||
btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 运行中...';
|
const delayEnabled = inputAutoDelayEnabled.checked;
|
||||||
|
const delayMinutes = normalizeAutoDelayMinutes(inputAutoDelayMinutes.value);
|
||||||
|
inputAutoDelayMinutes.value = String(delayMinutes);
|
||||||
|
btnAutoRun.innerHTML = delayEnabled
|
||||||
|
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 计划中...'
|
||||||
|
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 运行中...';
|
||||||
const response = await chrome.runtime.sendMessage({
|
const response = await chrome.runtime.sendMessage({
|
||||||
type: 'AUTO_RUN',
|
type: delayEnabled ? 'SCHEDULE_AUTO_RUN' : 'AUTO_RUN',
|
||||||
source: 'sidepanel',
|
source: 'sidepanel',
|
||||||
payload: {
|
payload: {
|
||||||
totalRuns,
|
totalRuns,
|
||||||
|
delayMinutes,
|
||||||
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
||||||
mode,
|
mode,
|
||||||
},
|
},
|
||||||
@@ -875,6 +1007,29 @@ btnAutoContinue.addEventListener('click', async () => {
|
|||||||
await chrome.runtime.sendMessage({ type: 'RESUME_AUTO_RUN', source: 'sidepanel', payload: { email } });
|
await chrome.runtime.sendMessage({ type: 'RESUME_AUTO_RUN', source: 'sidepanel', payload: { email } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
btnAutoRunNow?.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
btnAutoRunNow.disabled = true;
|
||||||
|
await chrome.runtime.sendMessage({ type: 'START_SCHEDULED_AUTO_RUN_NOW', source: 'sidepanel', payload: {} });
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
btnAutoRunNow.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnAutoCancelSchedule?.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
btnAutoCancelSchedule.disabled = true;
|
||||||
|
await chrome.runtime.sendMessage({ type: 'CANCEL_SCHEDULED_AUTO_RUN', source: 'sidepanel', payload: {} });
|
||||||
|
showToast('已取消倒计时计划。', 'info', 1800);
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
btnAutoCancelSchedule.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
btnReset.addEventListener('click', async () => {
|
btnReset.addEventListener('click', async () => {
|
||||||
const confirmed = await openConfirmModal({
|
const confirmed = await openConfirmModal({
|
||||||
@@ -889,7 +1044,7 @@ btnReset.addEventListener('click', async () => {
|
|||||||
|
|
||||||
await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
|
await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
|
||||||
syncLatestState({ stepStatuses: STEP_DEFAULT_STATUSES });
|
syncLatestState({ stepStatuses: STEP_DEFAULT_STATUSES });
|
||||||
syncAutoRunState({ autoRunning: false, autoRunPhase: 'idle', autoRunCurrentRun: 0, autoRunTotalRuns: 1, autoRunAttemptRun: 0 });
|
syncAutoRunState({ autoRunning: false, autoRunPhase: 'idle', autoRunCurrentRun: 0, autoRunTotalRuns: 1, autoRunAttemptRun: 0, scheduledAutoRunAt: null });
|
||||||
displayOauthUrl.textContent = '等待中...';
|
displayOauthUrl.textContent = '等待中...';
|
||||||
displayOauthUrl.classList.remove('has-value');
|
displayOauthUrl.classList.remove('has-value');
|
||||||
displayLocalhostUrl.textContent = '等待中...';
|
displayLocalhostUrl.textContent = '等待中...';
|
||||||
@@ -973,6 +1128,21 @@ inputAutoSkipFailures.addEventListener('change', () => {
|
|||||||
saveSettings({ silent: true }).catch(() => { });
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
inputAutoDelayEnabled.addEventListener('change', () => {
|
||||||
|
updateAutoDelayInputState();
|
||||||
|
markSettingsDirty(true);
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
|
inputAutoDelayMinutes.addEventListener('input', () => {
|
||||||
|
markSettingsDirty(true);
|
||||||
|
scheduleSettingsAutoSave();
|
||||||
|
});
|
||||||
|
inputAutoDelayMinutes.addEventListener('blur', () => {
|
||||||
|
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(inputAutoDelayMinutes.value));
|
||||||
|
saveSettings({ silent: true }).catch(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Listen for Background broadcasts
|
// Listen for Background broadcasts
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -1019,6 +1189,7 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
password: null,
|
password: null,
|
||||||
stepStatuses: STEP_DEFAULT_STATUSES,
|
stepStatuses: STEP_DEFAULT_STATUSES,
|
||||||
logs: [],
|
logs: [],
|
||||||
|
scheduledAutoRunAt: null,
|
||||||
});
|
});
|
||||||
displayOauthUrl.textContent = '等待中...';
|
displayOauthUrl.textContent = '等待中...';
|
||||||
displayOauthUrl.classList.remove('has-value');
|
displayOauthUrl.classList.remove('has-value');
|
||||||
@@ -1057,11 +1228,12 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
|
|
||||||
case 'AUTO_RUN_STATUS': {
|
case 'AUTO_RUN_STATUS': {
|
||||||
syncLatestState({
|
syncLatestState({
|
||||||
autoRunning: ['running', 'waiting_email', 'retrying'].includes(message.payload.phase),
|
autoRunning: ['scheduled', 'running', 'waiting_email', 'retrying'].includes(message.payload.phase),
|
||||||
autoRunPhase: message.payload.phase,
|
autoRunPhase: message.payload.phase,
|
||||||
autoRunCurrentRun: message.payload.currentRun,
|
autoRunCurrentRun: message.payload.currentRun,
|
||||||
autoRunTotalRuns: message.payload.totalRuns,
|
autoRunTotalRuns: message.payload.totalRuns,
|
||||||
autoRunAttemptRun: message.payload.attemptRun,
|
autoRunAttemptRun: message.payload.attemptRun,
|
||||||
|
scheduledAutoRunAt: message.payload.scheduledAt ?? null,
|
||||||
});
|
});
|
||||||
applyAutoRunStatus(message.payload);
|
applyAutoRunStatus(message.payload);
|
||||||
updateStatusDisplay(latestState);
|
updateStatusDisplay(latestState);
|
||||||
|
|||||||
Reference in New Issue
Block a user