feat: 添加自动运行选择对话框,支持继续当前流程或重新开始,优化用户体验
This commit is contained in:
@@ -127,6 +127,11 @@ Step 3 使用的注册邮箱。
|
|||||||
|
|
||||||
支持多轮运行,运行次数由右上角数字框决定。
|
支持多轮运行,运行次数由右上角数字框决定。
|
||||||
|
|
||||||
|
如果当前面板里已经存在未完成进度,点击 `Auto` 时会弹出选择:
|
||||||
|
|
||||||
|
- `重新开始`:重置当前流程进度,从 Step 1 开始新一轮
|
||||||
|
- `继续当前`:把 `已完成 / 已跳过` 视为已处理,从第一个未处理步骤继续往后执行
|
||||||
|
|
||||||
## 工作流
|
## 工作流
|
||||||
|
|
||||||
### 单步模式
|
### 单步模式
|
||||||
@@ -161,6 +166,7 @@ Step 3 使用的注册邮箱。
|
|||||||
- 如果不能自动获取,Auto 会在邮箱阶段暂停
|
- 如果不能自动获取,Auto 会在邮箱阶段暂停
|
||||||
- Auto 的暂停状态会保存在会话状态中,重新打开侧边栏后仍可继续
|
- Auto 的暂停状态会保存在会话状态中,重新打开侧边栏后仍可继续
|
||||||
- 如果你在 Auto 暂停时改为手动点步骤或跳过步骤,面板会先确认并停止 Auto,再切回手动控制
|
- 如果你在 Auto 暂停时改为手动点步骤或跳过步骤,面板会先确认并停止 Auto,再切回手动控制
|
||||||
|
- 选择 `继续当前` 时,后台不会先做大而全的前置校验,而是从当前步骤状态直接继续;缺什么条件,就在运行到那一步时再报错或暂停
|
||||||
|
|
||||||
## 详细步骤说明
|
## 详细步骤说明
|
||||||
|
|
||||||
|
|||||||
+139
-65
@@ -573,6 +573,19 @@ function isStepDoneStatus(status) {
|
|||||||
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFirstUnfinishedStep(statuses = {}) {
|
||||||
|
for (let step = 1; step <= 9; step++) {
|
||||||
|
if (!isStepDoneStatus(statuses[step] || 'pending')) {
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSavedProgress(statuses = {}) {
|
||||||
|
return Object.values({ ...DEFAULT_STATE.stepStatuses, ...statuses }).some((status) => status !== 'pending');
|
||||||
|
}
|
||||||
|
|
||||||
function clearStopRequest() {
|
function clearStopRequest() {
|
||||||
stopRequested = false;
|
stopRequested = false;
|
||||||
}
|
}
|
||||||
@@ -833,8 +846,9 @@ async function handleMessage(message, sender) {
|
|||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
const totalRuns = message.payload?.totalRuns || 1;
|
const totalRuns = message.payload?.totalRuns || 1;
|
||||||
const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures);
|
const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures);
|
||||||
|
const mode = message.payload?.mode === 'continue' ? 'continue' : 'restart';
|
||||||
await setState({ autoRunSkipFailures });
|
await setState({ autoRunSkipFailures });
|
||||||
autoRunLoop(totalRuns, { autoRunSkipFailures }); // fire-and-forget
|
autoRunLoop(totalRuns, { autoRunSkipFailures, mode }); // fire-and-forget
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1120,6 +1134,17 @@ let autoRunActive = false;
|
|||||||
let autoRunCurrentRun = 0;
|
let autoRunCurrentRun = 0;
|
||||||
let autoRunTotalRuns = 1;
|
let autoRunTotalRuns = 1;
|
||||||
let autoRunAttemptRun = 0;
|
let autoRunAttemptRun = 0;
|
||||||
|
const AUTO_STEP_DELAYS = {
|
||||||
|
1: 2000,
|
||||||
|
2: 2000,
|
||||||
|
3: 3000,
|
||||||
|
4: 2000,
|
||||||
|
5: 3000,
|
||||||
|
6: 3000,
|
||||||
|
7: 2000,
|
||||||
|
8: 2000,
|
||||||
|
9: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
async function resumeAutoRunIfWaitingForEmail(options = {}) {
|
async function resumeAutoRunIfWaitingForEmail(options = {}) {
|
||||||
const { silent = false } = options;
|
const { silent = false } = options;
|
||||||
@@ -1140,6 +1165,75 @@ async function resumeAutoRunIfWaitingForEmail(options = {}) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
|
||||||
|
const currentState = await getState();
|
||||||
|
if (currentState.email) {
|
||||||
|
return currentState.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const duckEmail = await fetchDuckEmail({ generateNew: true });
|
||||||
|
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:Duck 邮箱已就绪:${duckEmail}(第 ${attemptRuns} 次尝试)===`, 'ok');
|
||||||
|
return duckEmail;
|
||||||
|
} catch (err) {
|
||||||
|
await addLog(`Duck 邮箱自动获取失败:${err.message}`, 'warn');
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先获取 Duck 邮箱或手动粘贴邮箱,然后继续 ===`, 'warn');
|
||||||
|
await broadcastAutoRunStatus('waiting_email', {
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForResume();
|
||||||
|
|
||||||
|
const resumedState = await getState();
|
||||||
|
if (!resumedState.email) {
|
||||||
|
throw new Error('无法继续:当前没有邮箱地址。');
|
||||||
|
}
|
||||||
|
return resumedState.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAutoSequenceFromStep(startStep, context = {}) {
|
||||||
|
const { targetRun, totalRuns, attemptRuns, continued = false } = context;
|
||||||
|
|
||||||
|
if (continued) {
|
||||||
|
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续当前进度,从步骤 ${startStep} 开始(第 ${attemptRuns} 次尝试)===`, 'info');
|
||||||
|
} else {
|
||||||
|
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startStep <= 2) {
|
||||||
|
for (const step of [1, 2]) {
|
||||||
|
if (step < startStep) continue;
|
||||||
|
await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startStep <= 3) {
|
||||||
|
await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns);
|
||||||
|
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,注册、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info');
|
||||||
|
await broadcastAutoRunStatus('running', {
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
|
await executeStepAndWait(3, AUTO_STEP_DELAYS[3]);
|
||||||
|
} else {
|
||||||
|
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续执行剩余流程(第 ${attemptRuns} 次尝试)===`, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const signupTabId = await getTabId('signup-page');
|
||||||
|
if (signupTabId) {
|
||||||
|
await chrome.tabs.update(signupTabId, { active: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let step = Math.max(startStep, 4); step <= 9; step++) {
|
||||||
|
await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Outer loop: keep retrying until the target number of successful runs is reached.
|
// Outer loop: keep retrying until the target number of successful runs is reached.
|
||||||
async function autoRunLoop(totalRuns, options = {}) {
|
async function autoRunLoop(totalRuns, options = {}) {
|
||||||
if (autoRunActive) {
|
if (autoRunActive) {
|
||||||
@@ -1153,10 +1247,12 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
autoRunCurrentRun = 0;
|
autoRunCurrentRun = 0;
|
||||||
autoRunAttemptRun = 0;
|
autoRunAttemptRun = 0;
|
||||||
const autoRunSkipFailures = Boolean(options.autoRunSkipFailures);
|
const autoRunSkipFailures = Boolean(options.autoRunSkipFailures);
|
||||||
|
const initialMode = options.mode === 'continue' ? 'continue' : 'restart';
|
||||||
const maxAttempts = autoRunSkipFailures ? Math.max(totalRuns * 10, totalRuns + 20) : totalRuns;
|
const maxAttempts = autoRunSkipFailures ? Math.max(totalRuns * 10, totalRuns + 20) : totalRuns;
|
||||||
let successfulRuns = 0;
|
let successfulRuns = 0;
|
||||||
let attemptRuns = 0;
|
let attemptRuns = 0;
|
||||||
let forceFreshTabsNextRun = false;
|
let forceFreshTabsNextRun = false;
|
||||||
|
let continueCurrentOnFirstAttempt = initialMode === 'continue';
|
||||||
|
|
||||||
await setState({
|
await setState({
|
||||||
autoRunSkipFailures,
|
autoRunSkipFailures,
|
||||||
@@ -1168,32 +1264,51 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
const targetRun = successfulRuns + 1;
|
const targetRun = successfulRuns + 1;
|
||||||
autoRunCurrentRun = targetRun;
|
autoRunCurrentRun = targetRun;
|
||||||
autoRunAttemptRun = attemptRuns;
|
autoRunAttemptRun = attemptRuns;
|
||||||
|
let startStep = 1;
|
||||||
|
let useExistingProgress = false;
|
||||||
|
|
||||||
// Reset everything at the start of each attempt (keep user settings).
|
if (continueCurrentOnFirstAttempt) {
|
||||||
const prevState = await getState();
|
const currentState = await getState();
|
||||||
const keepSettings = {
|
const resumeStep = getFirstUnfinishedStep(currentState.stepStatuses);
|
||||||
vpsUrl: prevState.vpsUrl,
|
if (resumeStep && hasSavedProgress(currentState.stepStatuses)) {
|
||||||
vpsPassword: prevState.vpsPassword,
|
startStep = resumeStep;
|
||||||
customPassword: prevState.customPassword,
|
useExistingProgress = true;
|
||||||
autoRunSkipFailures: prevState.autoRunSkipFailures,
|
} else if (hasSavedProgress(currentState.stepStatuses)) {
|
||||||
mailProvider: prevState.mailProvider,
|
await addLog('当前流程已全部处理,将按“重新开始”新开一轮自动运行。', 'info');
|
||||||
inbucketHost: prevState.inbucketHost,
|
}
|
||||||
inbucketMailbox: prevState.inbucketMailbox,
|
continueCurrentOnFirstAttempt = false;
|
||||||
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }),
|
}
|
||||||
...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}),
|
|
||||||
};
|
if (!useExistingProgress) {
|
||||||
await resetState();
|
// Reset everything at the start of each fresh attempt (keep user settings).
|
||||||
await setState(keepSettings);
|
const prevState = await getState();
|
||||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {});
|
const keepSettings = {
|
||||||
await sleepWithStop(500);
|
vpsUrl: prevState.vpsUrl,
|
||||||
|
vpsPassword: prevState.vpsPassword,
|
||||||
|
customPassword: prevState.customPassword,
|
||||||
|
autoRunSkipFailures: prevState.autoRunSkipFailures,
|
||||||
|
mailProvider: prevState.mailProvider,
|
||||||
|
inbucketHost: prevState.inbucketHost,
|
||||||
|
inbucketMailbox: prevState.inbucketMailbox,
|
||||||
|
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }),
|
||||||
|
...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}),
|
||||||
|
};
|
||||||
|
await resetState();
|
||||||
|
await setState(keepSettings);
|
||||||
|
chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {});
|
||||||
|
await sleepWithStop(500);
|
||||||
|
} else {
|
||||||
|
await setState({
|
||||||
|
autoRunSkipFailures,
|
||||||
|
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (forceFreshTabsNextRun) {
|
if (forceFreshTabsNextRun) {
|
||||||
await addLog(`兜底模式:上一轮已放弃,当前开始第 ${attemptRuns} 次尝试,将使用新线程继续补足第 ${targetRun}/${totalRuns} 轮。`, 'warn');
|
await addLog(`兜底模式:上一轮已放弃,当前开始第 ${attemptRuns} 次尝试,将使用新线程继续补足第 ${targetRun}/${totalRuns} 轮。`, 'warn');
|
||||||
forceFreshTabsNextRun = false;
|
forceFreshTabsNextRun = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
await broadcastAutoRunStatus('running', {
|
await broadcastAutoRunStatus('running', {
|
||||||
@@ -1202,54 +1317,13 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
attemptRun: attemptRuns,
|
attemptRun: attemptRuns,
|
||||||
});
|
});
|
||||||
|
|
||||||
await executeStepAndWait(1, 2000);
|
await runAutoSequenceFromStep(startStep, {
|
||||||
await executeStepAndWait(2, 2000);
|
targetRun,
|
||||||
|
|
||||||
let emailReady = false;
|
|
||||||
try {
|
|
||||||
const duckEmail = await fetchDuckEmail({ generateNew: true });
|
|
||||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:Duck 邮箱已就绪:${duckEmail}(第 ${attemptRuns} 次尝试)===`, 'ok');
|
|
||||||
emailReady = true;
|
|
||||||
} catch (err) {
|
|
||||||
await addLog(`Duck 邮箱自动获取失败:${err.message}`, 'warn');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!emailReady) {
|
|
||||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先获取 Duck 邮箱或手动粘贴邮箱,然后继续 ===`, 'warn');
|
|
||||||
await broadcastAutoRunStatus('waiting_email', {
|
|
||||||
currentRun: targetRun,
|
|
||||||
totalRuns,
|
|
||||||
attemptRun: attemptRuns,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitForResume();
|
|
||||||
|
|
||||||
const resumedState = await getState();
|
|
||||||
if (!resumedState.email) {
|
|
||||||
throw new Error('无法继续:当前没有邮箱地址。');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,注册、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info');
|
|
||||||
await broadcastAutoRunStatus('running', {
|
|
||||||
currentRun: targetRun,
|
|
||||||
totalRuns,
|
totalRuns,
|
||||||
attemptRun: attemptRuns,
|
attemptRuns,
|
||||||
|
continued: useExistingProgress,
|
||||||
});
|
});
|
||||||
|
|
||||||
const signupTabId = await getTabId('signup-page');
|
|
||||||
if (signupTabId) {
|
|
||||||
await chrome.tabs.update(signupTabId, { active: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
await executeStepAndWait(3, 3000);
|
|
||||||
await executeStepAndWait(4, 2000);
|
|
||||||
await executeStepAndWait(5, 3000);
|
|
||||||
await executeStepAndWait(6, 3000);
|
|
||||||
await executeStepAndWait(7, 2000);
|
|
||||||
await executeStepAndWait(8, 2000);
|
|
||||||
await executeStepAndWait(9, 1000);
|
|
||||||
|
|
||||||
successfulRuns += 1;
|
successfulRuns += 1;
|
||||||
autoRunCurrentRun = successfulRuns;
|
autoRunCurrentRun = successfulRuns;
|
||||||
await addLog(`=== 目标 ${successfulRuns}/${totalRuns} 轮已完成(第 ${attemptRuns} 次尝试成功)===`, 'ok');
|
await addLog(`=== 目标 ${successfulRuns}/${totalRuns} 轮已完成(第 ${attemptRuns} 次尝试成功)===`, 'ok');
|
||||||
|
|||||||
@@ -677,6 +677,79 @@ header {
|
|||||||
|
|
||||||
.log-line { animation: fadeIn 120ms ease-out; }
|
.log-line { animation: fadeIn 120ms ease-out; }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Modal
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1200;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(15, 17, 23, 0.32);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card {
|
||||||
|
width: min(100%, 320px);
|
||||||
|
background: var(--bg-base);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow-md), 0 18px 36px rgba(0, 0, 0, 0.18);
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all var(--transition);
|
||||||
|
}
|
||||||
|
.modal-close:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-message {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Toast Notifications
|
Toast Notifications
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|||||||
@@ -174,6 +174,21 @@
|
|||||||
<div id="log-area"></div>
|
<div id="log-area"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div id="auto-start-modal" class="modal-overlay" hidden>
|
||||||
|
<div class="modal-card">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-title">启动自动</span>
|
||||||
|
<button id="btn-auto-start-close" class="modal-close" type="button" aria-label="关闭">×</button>
|
||||||
|
</div>
|
||||||
|
<p id="auto-start-message" class="modal-message">检测到已有流程进度,选择继续当前还是重新开始。</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="btn-auto-start-cancel" class="btn btn-ghost btn-sm" type="button">取消</button>
|
||||||
|
<button id="btn-auto-start-restart" class="btn btn-outline btn-sm" type="button">重新开始</button>
|
||||||
|
<button id="btn-auto-start-continue" class="btn btn-primary btn-sm" type="button">继续当前</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="toast-container"></div>
|
<div id="toast-container"></div>
|
||||||
<script src="sidepanel.js"></script>
|
<script src="sidepanel.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+96
-12
@@ -36,6 +36,12 @@ 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 autoStartModal = document.getElementById('auto-start-modal');
|
||||||
|
const autoStartMessage = document.getElementById('auto-start-message');
|
||||||
|
const btnAutoStartClose = document.getElementById('btn-auto-start-close');
|
||||||
|
const btnAutoStartCancel = document.getElementById('btn-auto-start-cancel');
|
||||||
|
const btnAutoStartRestart = document.getElementById('btn-auto-start-restart');
|
||||||
|
const btnAutoStartContinue = document.getElementById('btn-auto-start-continue');
|
||||||
const STEP_DEFAULT_STATUSES = {
|
const STEP_DEFAULT_STATUSES = {
|
||||||
1: 'pending',
|
1: 'pending',
|
||||||
2: 'pending',
|
2: 'pending',
|
||||||
@@ -60,6 +66,7 @@ let currentAutoRun = {
|
|||||||
let settingsDirty = false;
|
let settingsDirty = false;
|
||||||
let settingsSaveInFlight = false;
|
let settingsSaveInFlight = false;
|
||||||
let settingsAutoSaveTimer = null;
|
let settingsAutoSaveTimer = null;
|
||||||
|
let autoStartChoiceResolver = 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>';
|
||||||
@@ -103,6 +110,33 @@ function dismissToast(toast) {
|
|||||||
toast.addEventListener('animationend', () => toast.remove());
|
toast.addEventListener('animationend', () => toast.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAutoStartChoice(choice) {
|
||||||
|
if (autoStartChoiceResolver) {
|
||||||
|
autoStartChoiceResolver(choice);
|
||||||
|
autoStartChoiceResolver = null;
|
||||||
|
}
|
||||||
|
if (autoStartModal) {
|
||||||
|
autoStartModal.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAutoStartChoiceDialog(startStep) {
|
||||||
|
if (!autoStartModal) {
|
||||||
|
return Promise.resolve('restart');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoStartChoiceResolver) {
|
||||||
|
resolveAutoStartChoice(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
autoStartMessage.textContent = `检测到当前已有流程进度。继续当前会从步骤 ${startStep} 开始自动执行,重新开始会清空当前流程进度并从步骤 1 新开一轮。`;
|
||||||
|
autoStartModal.hidden = false;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
autoStartChoiceResolver = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function isDoneStatus(status) {
|
function isDoneStatus(status) {
|
||||||
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
|
||||||
}
|
}
|
||||||
@@ -111,6 +145,25 @@ function getStepStatuses(state = latestState) {
|
|||||||
return { ...STEP_DEFAULT_STATUSES, ...(state?.stepStatuses || {}) };
|
return { ...STEP_DEFAULT_STATUSES, ...(state?.stepStatuses || {}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFirstUnfinishedStep(state = latestState) {
|
||||||
|
const statuses = getStepStatuses(state);
|
||||||
|
for (let step = 1; step <= 9; step++) {
|
||||||
|
if (!isDoneStatus(statuses[step])) {
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSavedProgress(state = latestState) {
|
||||||
|
const statuses = getStepStatuses(state);
|
||||||
|
return Object.values(statuses).some((status) => status !== 'pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldOfferAutoModeChoice(state = latestState) {
|
||||||
|
return hasSavedProgress(state) && getFirstUnfinishedStep(state) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
function syncLatestState(nextState) {
|
function syncLatestState(nextState) {
|
||||||
const mergedStepStatuses = nextState?.stepStatuses
|
const mergedStepStatuses = nextState?.stepStatuses
|
||||||
? { ...STEP_DEFAULT_STATUSES, ...(latestState?.stepStatuses || {}), ...nextState.stepStatuses }
|
? { ...STEP_DEFAULT_STATUSES, ...(latestState?.stepStatuses || {}), ...nextState.stepStatuses }
|
||||||
@@ -681,20 +734,51 @@ btnStop.addEventListener('click', async () => {
|
|||||||
showToast('正在停止当前流程...', 'warn', 2000);
|
showToast('正在停止当前流程...', 'warn', 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
autoStartModal?.addEventListener('click', (event) => {
|
||||||
|
if (event.target === autoStartModal) {
|
||||||
|
resolveAutoStartChoice(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
btnAutoStartClose?.addEventListener('click', () => resolveAutoStartChoice(null));
|
||||||
|
btnAutoStartCancel?.addEventListener('click', () => resolveAutoStartChoice(null));
|
||||||
|
btnAutoStartRestart?.addEventListener('click', () => resolveAutoStartChoice('restart'));
|
||||||
|
btnAutoStartContinue?.addEventListener('click', () => resolveAutoStartChoice('continue'));
|
||||||
|
|
||||||
// Auto Run
|
// Auto Run
|
||||||
btnAutoRun.addEventListener('click', async () => {
|
btnAutoRun.addEventListener('click', async () => {
|
||||||
const totalRuns = parseInt(inputRunCount.value) || 1;
|
try {
|
||||||
btnAutoRun.disabled = true;
|
const totalRuns = parseInt(inputRunCount.value) || 1;
|
||||||
inputRunCount.disabled = true;
|
let mode = 'restart';
|
||||||
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> 运行中...';
|
|
||||||
await chrome.runtime.sendMessage({
|
if (shouldOfferAutoModeChoice()) {
|
||||||
type: 'AUTO_RUN',
|
const startStep = getFirstUnfinishedStep();
|
||||||
source: 'sidepanel',
|
const choice = await openAutoStartChoiceDialog(startStep);
|
||||||
payload: {
|
if (!choice) {
|
||||||
totalRuns,
|
return;
|
||||||
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
}
|
||||||
},
|
mode = choice;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
btnAutoRun.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 response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'AUTO_RUN',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: {
|
||||||
|
totalRuns,
|
||||||
|
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
||||||
|
mode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setDefaultAutoRunButton();
|
||||||
|
inputRunCount.disabled = false;
|
||||||
|
showToast(err.message, 'error');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
btnAutoContinue.addEventListener('click', async () => {
|
btnAutoContinue.addEventListener('click', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user