feat: 修改兜底模式样式,增加线程间隔设置和相关UI组件

This commit is contained in:
QLHazyCoder
2026-04-13 21:38:50 +08:00
parent 9ba958b1a2
commit 8a37872336
4 changed files with 336 additions and 12 deletions
+67 -1
View File
@@ -50,7 +50,7 @@ initializeSessionStorageAccess();
// 状态管理(chrome.storage.session + chrome.storage.local
// ============================================================
const PERSISTED_SETTING_DEFAULTS = {
const LEGACY_PERSISTED_SETTING_DEFAULTS = {
panelMode: 'cpa', // Step 1 / Step 9 的来源模式:cpa | sub2api。
vpsUrl: '', // VPS 面板地址,可手动填写。
vpsPassword: '', // VPS 面板登录密码,可手动填写。
@@ -61,6 +61,18 @@ const PERSISTED_SETTING_DEFAULTS = {
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME, // SUB2API 创建账号时绑定的分组名。
customPassword: '', // 自定义账号密码;留空时由程序自动生成随机密码。
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
autoRunFallbackThreadIntervalMinutes: 0, // 兜底模式下,两轮线程之间的等待分钟数。
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。
autoStepDelaySeconds: null, // 自动运行每一步执行前的额外等待秒数;0 或空表示不延迟。
mailProvider: '163', // 验证码邮箱来源(163 / 163-vip / qq / inbucket)。
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。
autoStepDelaySeconds: null, // 自动运行每一步执行前的额外等待秒数;0 或空表示不延迟。
mailProvider: '163', // 验证码邮箱来源(163 / 163-vip / qq / inbucket)。
autoRunSkipFailures: false, // 自动运行遇到失败步骤后,是否继续执行后续流程。
autoRunFallbackThreadIntervalMinutes: 0, // 兜底模式下,两轮线程之间的等待分钟数。
autoRunDelayEnabled: false, // 自动运行是否启用启动前倒计时。
autoRunDelayMinutes: 30, // 自动运行倒计时分钟数。
autoStepDelaySeconds: null, // 自动运行每一步执行前的额外等待秒数;0 或空表示不延迟。
@@ -73,6 +85,30 @@ const PERSISTED_SETTING_DEFAULTS = {
hotmailAccounts: [],
};
const PERSISTED_SETTING_DEFAULTS = {
panelMode: 'cpa',
vpsUrl: '',
vpsPassword: '',
localCpaStep9Mode: DEFAULT_LOCAL_CPA_STEP9_MODE,
sub2apiUrl: DEFAULT_SUB2API_URL,
sub2apiEmail: '',
sub2apiPassword: '',
sub2apiGroupName: DEFAULT_SUB2API_GROUP_NAME,
customPassword: '',
autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
inbucketHost: '',
inbucketMailbox: '',
cloudflareDomain: '',
cloudflareDomains: [],
hotmailAccounts: [],
};
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
const SETTINGS_EXPORT_SCHEMA_VERSION = 1;
const SETTINGS_EXPORT_FILENAME_PREFIX = 'multipage-settings';
@@ -123,6 +159,23 @@ function normalizeAutoRunDelayMinutes(value) {
);
}
function normalizeAutoRunFallbackThreadIntervalMinutes(value) {
const rawValue = String(value ?? '').trim();
if (!rawValue) {
return 0;
}
const numeric = Number(rawValue);
if (!Number.isFinite(numeric)) {
return 0;
}
return Math.min(
AUTO_RUN_DELAY_MAX_MINUTES,
Math.max(0, Math.floor(numeric))
);
}
function normalizeAutoStepDelaySeconds(value, fallback = null) {
const rawValue = String(value ?? '').trim();
if (!rawValue) {
@@ -256,6 +309,8 @@ function normalizePersistentSettingValue(key, value) {
case 'autoRunSkipFailures':
case 'autoRunDelayEnabled':
return Boolean(value);
case 'autoRunFallbackThreadIntervalMinutes':
return normalizeAutoRunFallbackThreadIntervalMinutes(value);
case 'autoRunDelayMinutes':
return normalizeAutoRunDelayMinutes(value);
case 'autoStepDelaySeconds':
@@ -3435,6 +3490,7 @@ async function autoRunLoop(totalRuns, options = {}) {
vpsPassword: prevState.vpsPassword,
customPassword: prevState.customPassword,
autoRunSkipFailures: prevState.autoRunSkipFailures,
autoRunFallbackThreadIntervalMinutes: prevState.autoRunFallbackThreadIntervalMinutes,
autoRunDelayEnabled: prevState.autoRunDelayEnabled,
autoRunDelayMinutes: prevState.autoRunDelayMinutes,
autoStepDelaySeconds: prevState.autoStepDelaySeconds,
@@ -3481,6 +3537,16 @@ async function autoRunLoop(totalRuns, options = {}) {
successfulRuns += 1;
autoRunCurrentRun = successfulRuns;
await addLog(`=== 目标 ${successfulRuns}/${totalRuns} 轮已完成(第 ${attemptRuns} 次尝试成功)===`, 'ok');
const fallbackThreadIntervalMinutes = normalizeAutoRunFallbackThreadIntervalMinutes(
(await getState()).autoRunFallbackThreadIntervalMinutes
);
if (autoRunSkipFailures && totalRuns > 1 && successfulRuns < totalRuns && fallbackThreadIntervalMinutes > 0) {
await addLog(
`兜底模式:第 ${successfulRuns}/${totalRuns} 轮已完成,等待 ${fallbackThreadIntervalMinutes} 分钟后再启动下一轮新线程。`,
'info'
);
await sleepWithStop(fallbackThreadIntervalMinutes * 60 * 1000);
}
continue;
} catch (err) {
if (isStopError(err)) {
+84
View File
@@ -721,6 +721,72 @@ header {
flex-shrink: 0;
}
.fallback-inline {
justify-content: space-between;
}
.fallback-thread-interval {
display: flex;
align-items: center;
gap: 12px;
margin-left: auto;
}
.toggle-switch {
position: relative;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
cursor: pointer;
}
.toggle-switch > span:not(.toggle-switch-track) {
display: none;
}
.toggle-switch input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
pointer-events: none;
}
.toggle-switch-track {
width: 38px;
height: 22px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--bg-elevated);
display: inline-flex;
align-items: center;
padding: 2px;
transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.toggle-switch-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--bg-base);
box-shadow: var(--shadow-sm);
transition: transform var(--transition), background var(--transition);
}
.toggle-switch input:checked + .toggle-switch-track {
background: var(--blue-soft);
border-color: var(--blue);
}
.toggle-switch input:checked + .toggle-switch-track .toggle-switch-thumb {
transform: translateX(16px);
background: var(--blue);
}
.toggle-switch input:disabled + .toggle-switch-track {
opacity: 0.55;
}
.auto-delay-inline {
justify-content: space-between;
gap: 12px;
@@ -1196,6 +1262,24 @@ header {
margin-bottom: 14px;
}
.modal-option-row {
margin-bottom: 14px;
}
.modal-option-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-secondary);
cursor: pointer;
}
.modal-option-label input[type="checkbox"] {
accent-color: var(--blue);
cursor: pointer;
}
.modal-actions {
display: flex;
justify-content: flex-end;
+25 -8
View File
@@ -172,12 +172,24 @@
<button id="btn-fetch-email" class="btn btn-outline btn-sm data-inline-btn" type="button">获取</button>
</div>
</div>
<div class="data-row data-check-row">
<div class="data-row">
<span class="data-label">兜底</span>
<label class="data-check" for="input-auto-skip-failures">
<input type="checkbox" id="input-auto-skip-failures" />
<span>出现错误无法继续时,直接丢弃当前线程并新开一轮,直到补足目标运行次数</span>
</label>
<div class="data-inline fallback-inline">
<label class="toggle-switch" for="input-auto-skip-failures">
<input type="checkbox" id="input-auto-skip-failures" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
</label>
<div class="fallback-thread-interval">
<span class="auto-delay-caption">线程间隔</span>
<div class="auto-delay-controls">
<input type="number" id="input-auto-skip-failures-thread-interval-minutes" class="data-input auto-delay-input" value="0"
min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
<span class="data-unit">分钟</span>
</div>
</div>
</div>
</div>
<div class="data-row">
<span class="data-label">延迟</span>
@@ -202,8 +214,7 @@
</div>
</div>
</div>
</div>
<div class="data-row">
<div class="data-row">
<span class="data-label">OAuth</span>
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
</div>
@@ -359,6 +370,12 @@
<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 id="modal-option-row" class="modal-option-row" hidden>
<label class="modal-option-label" for="modal-option-input">
<input type="checkbox" id="modal-option-input" />
<span id="modal-option-text">不再提示</span>
</label>
</div>
<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>
@@ -373,4 +390,4 @@
<script src="sidepanel.js"></script>
</body>
</html>
</html>
+160 -3
View File
@@ -83,12 +83,16 @@ const inputCfDomain = document.getElementById('input-cf-domain');
const btnCfDomainMode = document.getElementById('btn-cf-domain-mode');
const inputRunCount = document.getElementById('input-run-count');
const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
const inputAutoSkipFailuresThreadIntervalMinutes = document.getElementById('input-auto-skip-failures-thread-interval-minutes');
const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes');
const inputAutoStepDelaySeconds = document.getElementById('input-auto-step-delay-seconds');
const autoStartModal = document.getElementById('auto-start-modal');
const autoStartTitle = autoStartModal?.querySelector('.modal-title');
const autoStartMessage = document.getElementById('auto-start-message');
const modalOptionRow = document.getElementById('modal-option-row');
const modalOptionInput = document.getElementById('modal-option-input');
const modalOptionText = document.getElementById('modal-option-text');
const btnAutoStartClose = document.getElementById('btn-auto-start-close');
const btnAutoStartCancel = document.getElementById('btn-auto-start-cancel');
const btnAutoStartRestart = document.getElementById('btn-auto-start-restart');
@@ -109,9 +113,13 @@ 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;
const AUTO_FALLBACK_THREAD_INTERVAL_MIN_MINUTES = 0;
const AUTO_FALLBACK_THREAD_INTERVAL_MAX_MINUTES = 1440;
const AUTO_FALLBACK_THREAD_INTERVAL_DEFAULT_MINUTES = 0;
const AUTO_STEP_DELAY_MIN_SECONDS = 0;
const AUTO_STEP_DELAY_MAX_SECONDS = 600;
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
const AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-skip-failures-prompt-dismissed';
let latestState = null;
let currentAutoRun = {
@@ -128,6 +136,7 @@ let settingsAutoSaveTimer = null;
let cloudflareDomainEditMode = false;
let modalChoiceResolver = null;
let currentModalActions = [];
let modalResultBuilder = null;
let scheduledCountdownTimer = null;
let hotmailActionInFlight = false;
let hotmailListExpanded = false;
@@ -198,6 +207,17 @@ function dismissToast(toast) {
toast.addEventListener('animationend', () => toast.remove());
}
function resetActionModalOption() {
if (!modalOptionRow || !modalOptionInput || !modalOptionText) {
return;
}
modalOptionRow.hidden = true;
modalOptionInput.checked = false;
modalOptionInput.disabled = false;
modalOptionText.textContent = '不再提示';
}
function resetActionModalButtons() {
const buttons = [btnAutoStartCancel, btnAutoStartRestart, btnAutoStartContinue];
buttons.forEach((button) => {
@@ -224,18 +244,40 @@ function configureActionModalButton(button, action) {
button.onclick = () => resolveModalChoice(action.id);
}
function configureActionModalOption(option) {
if (!modalOptionRow || !modalOptionInput || !modalOptionText) {
return;
}
if (!option) {
resetActionModalOption();
return;
}
modalOptionRow.hidden = false;
modalOptionInput.checked = Boolean(option.checked);
modalOptionInput.disabled = Boolean(option.disabled);
modalOptionText.textContent = option.label || '不再提示';
}
function resolveModalChoice(choice) {
const optionChecked = Boolean(modalOptionInput?.checked);
const result = typeof modalResultBuilder === 'function'
? modalResultBuilder(choice, { optionChecked })
: choice;
if (modalChoiceResolver) {
modalChoiceResolver(choice);
modalChoiceResolver(result);
modalChoiceResolver = null;
}
modalResultBuilder = null;
resetActionModalButtons();
resetActionModalOption();
if (autoStartModal) {
autoStartModal.hidden = true;
}
}
function openActionModal({ title, message, actions }) {
function openActionModal({ title, message, actions, option, buildResult }) {
if (!autoStartModal) {
return Promise.resolve(null);
}
@@ -244,12 +286,15 @@ function openActionModal({ title, message, actions }) {
resolveModalChoice(null);
}
resetActionModalButtons();
autoStartTitle.textContent = title;
autoStartMessage.textContent = message;
currentModalActions = actions || [];
modalResultBuilder = typeof buildResult === 'function' ? buildResult : null;
configureActionModalButton(btnAutoStartCancel, currentModalActions[0]);
configureActionModalButton(btnAutoStartRestart, currentModalActions[1]);
configureActionModalButton(btnAutoStartContinue, currentModalActions[2]);
configureActionModalOption(option);
autoStartModal.hidden = false;
return new Promise((resolve) => {
@@ -285,6 +330,42 @@ async function openConfirmModal({ title, message, confirmLabel = '确认', confi
return choice === 'confirm';
}
function isAutoSkipFailuresPromptDismissed() {
return localStorage.getItem(AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY) === '1';
}
function setAutoSkipFailuresPromptDismissed(dismissed) {
if (dismissed) {
localStorage.setItem(AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY, '1');
} else {
localStorage.removeItem(AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY);
}
}
async function openAutoSkipFailuresConfirmModal() {
const result = await openActionModal({
title: '兜底说明',
message: '开启后,当某一轮失败且无法继续时,会直接放弃当前线程,重新开新一轮,直到补足目标运行次数。线程间隔只在开启兜底且运行次数大于 1 时生效。',
actions: [
{ id: null, label: '取消', variant: 'btn-ghost' },
{ id: 'confirm', label: '确认开启', variant: 'btn-primary' },
],
option: {
label: '不再提示',
checked: false,
},
buildResult: (choice, meta) => ({
choice,
optionChecked: Boolean(meta?.optionChecked),
}),
});
return {
confirmed: result?.choice === 'confirm',
dismissPrompt: Boolean(result?.optionChecked),
};
}
function updateConfigMenuControls() {
const disabled = configActionInFlight || settingsSaveInFlight;
const importLocked = disabled
@@ -450,6 +531,23 @@ function normalizeAutoDelayMinutes(value) {
return Math.min(AUTO_DELAY_MAX_MINUTES, Math.max(AUTO_DELAY_MIN_MINUTES, Math.floor(numeric)));
}
function normalizeAutoRunThreadIntervalMinutes(value) {
const rawValue = String(value ?? '').trim();
if (!rawValue) {
return AUTO_FALLBACK_THREAD_INTERVAL_DEFAULT_MINUTES;
}
const numeric = Number(rawValue);
if (!Number.isFinite(numeric)) {
return AUTO_FALLBACK_THREAD_INTERVAL_DEFAULT_MINUTES;
}
return Math.min(
AUTO_FALLBACK_THREAD_INTERVAL_MAX_MINUTES,
Math.max(AUTO_FALLBACK_THREAD_INTERVAL_MIN_MINUTES, Math.floor(numeric))
);
}
function normalizeAutoStepDelaySeconds(value) {
const rawValue = String(value ?? '').trim();
if (!rawValue) {
@@ -469,6 +567,19 @@ function formatAutoStepDelayInputValue(value) {
return normalized === null ? '' : String(normalized);
}
function getRunCountValue() {
return Math.min(50, Math.max(1, parseInt(inputRunCount.value, 10) || 1));
}
function updateFallbackThreadIntervalInputState() {
if (!inputAutoSkipFailuresThreadIntervalMinutes) {
return;
}
const enabled = inputAutoSkipFailures.checked && getRunCountValue() > 1;
inputAutoSkipFailuresThreadIntervalMinutes.disabled = !enabled;
}
function updateAutoDelayInputState() {
const scheduled = isAutoRunScheduledPhase();
inputAutoDelayEnabled.disabled = scheduled;
@@ -637,6 +748,7 @@ function collectSettingsPayload() {
cloudflareDomain: selectedCloudflareDomain,
cloudflareDomains: domains,
autoRunSkipFailures: inputAutoSkipFailures.checked,
autoRunFallbackThreadIntervalMinutes: normalizeAutoRunThreadIntervalMinutes(inputAutoSkipFailuresThreadIntervalMinutes.value),
autoRunDelayEnabled: inputAutoDelayEnabled.checked,
autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
autoStepDelaySeconds: normalizeAutoStepDelaySeconds(inputAutoStepDelaySeconds.value),
@@ -818,6 +930,7 @@ function applyAutoRunStatus(payload = currentAutoRun) {
}
updateAutoDelayInputState();
updateFallbackThreadIntervalInputState();
syncScheduledCountdownTicker();
updateStopButtonState(scheduled || paused || locked || Object.values(getStepStatuses()).some(status => status === 'running'));
updateConfigMenuControls();
@@ -879,6 +992,7 @@ function applySettingsState(state) {
renderCloudflareDomainOptions(state?.cloudflareDomain || '');
setCloudflareDomainEditMode(false, { clearInput: true });
inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
inputAutoSkipFailuresThreadIntervalMinutes.value = String(normalizeAutoRunThreadIntervalMinutes(state?.autoRunFallbackThreadIntervalMinutes));
inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes));
inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(state?.autoStepDelaySeconds);
@@ -889,6 +1003,7 @@ function applySettingsState(state) {
applyAutoRunStatus(state);
markSettingsDirty(false);
updateAutoDelayInputState();
updateFallbackThreadIntervalInputState();
updatePanelModeUI();
updateMailProviderUI();
updateButtonStates();
@@ -2384,11 +2499,43 @@ inputInbucketHost.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
inputAutoSkipFailures.addEventListener('change', () => {
inputRunCount.addEventListener('input', () => {
updateFallbackThreadIntervalInputState();
});
inputRunCount.addEventListener('blur', () => {
inputRunCount.value = String(getRunCountValue());
updateFallbackThreadIntervalInputState();
});
inputAutoSkipFailures.addEventListener('change', async () => {
if (inputAutoSkipFailures.checked && !isAutoSkipFailuresPromptDismissed()) {
const result = await openAutoSkipFailuresConfirmModal();
if (!result.confirmed) {
inputAutoSkipFailures.checked = false;
updateFallbackThreadIntervalInputState();
return;
}
if (result.dismissPrompt) {
setAutoSkipFailuresPromptDismissed(true);
}
}
updateFallbackThreadIntervalInputState();
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
inputAutoSkipFailuresThreadIntervalMinutes.addEventListener('input', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
inputAutoSkipFailuresThreadIntervalMinutes.addEventListener('blur', () => {
inputAutoSkipFailuresThreadIntervalMinutes.value = String(
normalizeAutoRunThreadIntervalMinutes(inputAutoSkipFailuresThreadIntervalMinutes.value)
);
saveSettings({ silent: true }).catch(() => { });
});
inputAutoDelayEnabled.addEventListener('change', () => {
updateAutoDelayInputState();
markSettingsDirty(true);
@@ -2507,6 +2654,10 @@ chrome.runtime.onMessage.addListener((message) => {
inputEmail.value = getCurrentHotmailEmail();
}
}
if (message.payload.autoRunSkipFailures !== undefined) {
inputAutoSkipFailures.checked = Boolean(message.payload.autoRunSkipFailures);
updateFallbackThreadIntervalInputState();
}
if (message.payload.autoRunDelayEnabled !== undefined) {
inputAutoDelayEnabled.checked = Boolean(message.payload.autoRunDelayEnabled);
updateAutoDelayInputState();
@@ -2514,6 +2665,12 @@ chrome.runtime.onMessage.addListener((message) => {
if (message.payload.autoRunDelayMinutes !== undefined) {
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(message.payload.autoRunDelayMinutes));
}
if (message.payload.autoRunFallbackThreadIntervalMinutes !== undefined) {
inputAutoSkipFailuresThreadIntervalMinutes.value = String(
normalizeAutoRunThreadIntervalMinutes(message.payload.autoRunFallbackThreadIntervalMinutes)
);
updateFallbackThreadIntervalInputState();
}
if (message.payload.autoStepDelaySeconds !== undefined) {
inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(message.payload.autoStepDelaySeconds);
}