From bf7de173dee38eb20c839f8fd9042559bab6a4c1 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Wed, 8 Apr 2026 11:22:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96=E4=BF=9D=E5=AD=98=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=AF=86=E7=A0=81=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=92=8C=E4=BF=9D=E5=AD=98=E6=8C=89=E9=92=AE=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 19 ++++- background.js | 84 ++++++++++++------- sidepanel/sidepanel.css | 35 +++++++- sidepanel/sidepanel.html | 7 +- sidepanel/sidepanel.js | 173 +++++++++++++++++++++++++++++---------- 5 files changed, 238 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 11bcbc3..cb3f3db 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,8 @@ Step 3 使用的注册邮箱。 - 留空:自动生成强密码 - 手动输入:使用你自定义的密码 -- 可通过 `Show / Hide` 按钮切换显示 +- 可通过输入框右侧的眼睛图标切换显示 +- 配置会自动保存,也可以点击右侧 `保存` 按钮手动保存一次 扩展会把本轮实际使用的密码同步回侧边栏,便于查看和复制。 @@ -287,7 +288,7 @@ Step 3 使用的注册邮箱。 ## 状态与数据 -主要使用 `chrome.storage.session` 保存运行时状态: +运行时状态主要使用 `chrome.storage.session` 保存: - 当前步骤 - 每一步状态 @@ -298,12 +299,22 @@ Step 3 使用的注册邮箱。 - 账号记录 - tab 注册信息 - 各来源最近一次打开的地址(用于打开新地址前清理旧标签) -- 自定义设置 - Auto 当前阶段、当前轮次、暂停信息 +配置项另外使用 `chrome.storage.local` 持久化保存: + +- VPS 地址 +- VPS 管理密钥 +- 自定义密码 +- 邮箱服务 +- Inbucket 主机 +- Inbucket 邮箱名 +- 兜底开关 + 特点: -- 浏览器会话级存储 +- 运行时步骤状态是浏览器会话级存储 +- 配置项会持久化保存,关闭浏览器后重新打开仍会恢复 - 扩展运行期间可在多个步骤之间共享 - 代码里已启用 `storage.session` 对 content script 的访问 - 同一来源打开新地址前,会先按来源/站点范围关闭旧标签,避免旧页面残留 diff --git a/background.js b/background.js index 3dcd2a2..2168f92 100644 --- a/background.js +++ b/background.js @@ -11,9 +11,21 @@ const HUMAN_STEP_DELAY_MAX = 2200; initializeSessionStorageAccess(); // ============================================================ -// State Management (chrome.storage.session) +// State Management (chrome.storage.session + chrome.storage.local) // ============================================================ +const PERSISTED_SETTING_DEFAULTS = { + vpsUrl: '', + vpsPassword: '', + customPassword: '', + autoRunSkipFailures: false, + mailProvider: '163', + inbucketHost: '', + inbucketMailbox: '', +}; + +const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS); + const DEFAULT_STATE = { currentStep: 0, stepStatuses: { @@ -32,23 +44,29 @@ const DEFAULT_STATE = { tabRegistry: {}, sourceLastUrls: {}, logs: [], - vpsUrl: '', - vpsPassword: '', - customPassword: '', - autoRunSkipFailures: false, + ...PERSISTED_SETTING_DEFAULTS, autoRunning: false, autoRunPhase: 'idle', autoRunCurrentRun: 0, autoRunTotalRuns: 1, autoRunAttemptRun: 0, - mailProvider: '163', // 'qq' or '163' - inbucketHost: '', - inbucketMailbox: '', }; +async function getPersistedSettings() { + const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS); + return { + ...PERSISTED_SETTING_DEFAULTS, + ...stored, + autoRunSkipFailures: Boolean(stored.autoRunSkipFailures ?? PERSISTED_SETTING_DEFAULTS.autoRunSkipFailures), + }; +} + async function getState() { - const state = await chrome.storage.session.get(null); - return { ...DEFAULT_STATE, ...state }; + const [state, persistedSettings] = await Promise.all([ + chrome.storage.session.get(null), + getPersistedSettings(), + ]); + return { ...DEFAULT_STATE, ...persistedSettings, ...state }; } async function initializeSessionStorageAccess() { @@ -69,6 +87,21 @@ async function setState(updates) { await chrome.storage.session.set(updates); } +async function setPersistentSettings(updates) { + const persistedUpdates = {}; + for (const key of PERSISTED_SETTING_KEYS) { + if (updates[key] !== undefined) { + persistedUpdates[key] = key === 'autoRunSkipFailures' + ? Boolean(updates[key]) + : updates[key]; + } + } + + if (Object.keys(persistedUpdates).length > 0) { + await chrome.storage.local.set(persistedUpdates); + } +} + function broadcastDataUpdate(payload) { chrome.runtime.sendMessage({ type: 'DATA_UPDATED', @@ -89,35 +122,25 @@ async function setPasswordState(password) { async function resetState() { console.log(LOG_PREFIX, 'Resetting all state'); // Preserve settings and persistent data across resets - const prev = await chrome.storage.session.get([ - 'seenCodes', - 'seenInbucketMailIds', - 'accounts', - 'tabRegistry', - 'sourceLastUrls', - 'vpsUrl', - 'vpsPassword', - 'customPassword', - 'autoRunSkipFailures', - 'mailProvider', - 'inbucketHost', - 'inbucketMailbox', + const [prev, persistedSettings] = await Promise.all([ + chrome.storage.session.get([ + 'seenCodes', + 'seenInbucketMailIds', + 'accounts', + 'tabRegistry', + 'sourceLastUrls', + ]), + getPersistedSettings(), ]); await chrome.storage.session.clear(); await chrome.storage.session.set({ ...DEFAULT_STATE, + ...persistedSettings, seenCodes: prev.seenCodes || [], seenInbucketMailIds: prev.seenInbucketMailIds || [], accounts: prev.accounts || [], tabRegistry: prev.tabRegistry || {}, sourceLastUrls: prev.sourceLastUrls || {}, - vpsUrl: prev.vpsUrl || '', - vpsPassword: prev.vpsPassword || '', - customPassword: prev.customPassword || '', - autoRunSkipFailures: Boolean(prev.autoRunSkipFailures), - mailProvider: prev.mailProvider || '163', - inbucketHost: prev.inbucketHost || '', - inbucketMailbox: prev.inbucketMailbox || '', }); } @@ -915,6 +938,7 @@ async function handleMessage(message, sender) { if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider; if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost; if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox; + await setPersistentSettings(updates); await setState(updates); return { ok: true }; } diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 9a4a501..11d1741 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -256,6 +256,14 @@ header { min-width: 0; } +.input-with-icon { + position: relative; + flex: 1; + min-width: 0; + display: flex; + align-items: center; +} + .data-label { width: 56px; font-size: 11px; @@ -301,11 +309,36 @@ header { .data-input::placeholder { color: var(--text-muted); } .data-input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); } +.data-input-with-icon { + padding-right: 38px; +} + +.input-icon-btn { + position: absolute; + right: 8px; + width: 24px; + height: 24px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: var(--text-muted); + cursor: pointer; + border-radius: 999px; + transition: color var(--transition), background var(--transition); +} +.input-icon-btn:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + #btn-fetch-email { padding-inline: 10px; } -#btn-toggle-password { +#btn-save-settings { min-width: 58px; padding-inline: 10px; } diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index d0c19db..066a153 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -78,8 +78,11 @@
密码
- - +
+ + +
+
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 56009c8..959debb 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -18,6 +18,7 @@ const inputEmail = document.getElementById('input-email'); const inputPassword = document.getElementById('input-password'); const btnFetchEmail = document.getElementById('btn-fetch-email'); const btnTogglePassword = document.getElementById('btn-toggle-password'); +const btnSaveSettings = document.getElementById('btn-save-settings'); const btnStop = document.getElementById('btn-stop'); const btnReset = document.getElementById('btn-reset'); const stepsProgress = document.getElementById('steps-progress'); @@ -55,6 +56,12 @@ let currentAutoRun = { totalRuns: 1, attemptRun: 0, }; +let settingsDirty = false; +let settingsSaveInFlight = false; +let settingsAutoSaveTimer = null; + +const EYE_OPEN_ICON = ''; +const EYE_CLOSED_ICON = ''; // ============================================================ // Toast Notifications @@ -154,6 +161,77 @@ function setDefaultAutoRunButton() { btnAutoRun.innerHTML = ' 自动'; } +function collectSettingsPayload() { + return { + vpsUrl: inputVpsUrl.value.trim(), + vpsPassword: inputVpsPassword.value, + customPassword: inputPassword.value, + mailProvider: selectMailProvider.value, + inbucketHost: inputInbucketHost.value.trim(), + inbucketMailbox: inputInbucketMailbox.value.trim(), + autoRunSkipFailures: inputAutoSkipFailures.checked, + }; +} + +function markSettingsDirty(isDirty = true) { + settingsDirty = isDirty; + updateSaveButtonState(); +} + +function updateSaveButtonState() { + btnSaveSettings.disabled = settingsSaveInFlight || !settingsDirty; + btnSaveSettings.textContent = settingsSaveInFlight ? '保存中' : '保存'; +} + +function scheduleSettingsAutoSave() { + clearTimeout(settingsAutoSaveTimer); + settingsAutoSaveTimer = setTimeout(() => { + saveSettings({ silent: true }).catch(() => {}); + }, 500); +} + +async function saveSettings(options = {}) { + const { silent = false } = options; + clearTimeout(settingsAutoSaveTimer); + + if (!settingsDirty && !settingsSaveInFlight && silent) { + return; + } + + const payload = collectSettingsPayload(); + settingsSaveInFlight = true; + updateSaveButtonState(); + + try { + const response = await chrome.runtime.sendMessage({ + type: 'SAVE_SETTING', + source: 'sidepanel', + payload, + }); + + if (response?.error) { + throw new Error(response.error); + } + + syncLatestState(payload); + markSettingsDirty(false); + updateMailProviderUI(); + updateButtonStates(); + if (!silent) { + showToast('配置已保存', 'success', 1800); + } + } catch (err) { + markSettingsDirty(true); + if (!silent) { + showToast(`保存失败:${err.message}`, 'error'); + } + throw err; + } finally { + settingsSaveInFlight = false; + updateSaveButtonState(); + } +} + function applyAutoRunStatus(payload = currentAutoRun) { syncAutoRunState(payload); const runLabel = getAutoRunLabel(currentAutoRun); @@ -274,6 +352,7 @@ async function restoreState() { } applyAutoRunStatus(state); + markSettingsDirty(false); updateStatusDisplay(latestState); updateProgressCounter(); updateMailProviderUI(); @@ -503,7 +582,10 @@ async function fetchDuckEmail(options = {}) { } function syncPasswordToggleLabel() { - btnTogglePassword.textContent = inputPassword.type === 'password' ? '显示' : '隐藏'; + const isHidden = inputPassword.type === 'password'; + btnTogglePassword.innerHTML = isHidden ? EYE_OPEN_ICON : EYE_CLOSED_ICON; + btnTogglePassword.setAttribute('aria-label', isHidden ? '显示密码' : '隐藏密码'); + btnTogglePassword.title = isHidden ? '显示密码' : '隐藏密码'; } async function maybeTakeoverAutoRun(actionLabel) { @@ -606,6 +688,14 @@ btnTogglePassword.addEventListener('click', () => { syncPasswordToggleLabel(); }); +btnSaveSettings.addEventListener('click', async () => { + if (!settingsDirty) { + showToast('配置已是最新', 'info', 1400); + return; + } + await saveSettings({ silent: false }).catch(() => {}); +}); + btnStop.addEventListener('click', async () => { btnStop.disabled = true; await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} }); @@ -656,6 +746,7 @@ btnReset.addEventListener('click', async () => { document.querySelectorAll('.step-status').forEach(el => el.textContent = ''); setDefaultAutoRunButton(); applyAutoRunStatus(currentAutoRun); + markSettingsDirty(false); updateStopButtonState(false); updateButtonStates(); updateProgressCounter(); @@ -675,61 +766,56 @@ inputEmail.addEventListener('change', async () => { } }); inputEmail.addEventListener('input', updateButtonStates); -inputPassword.addEventListener('input', updateButtonStates); - -inputVpsUrl.addEventListener('change', async () => { - const vpsUrl = inputVpsUrl.value.trim(); - if (vpsUrl) { - await chrome.runtime.sendMessage({ type: 'SAVE_SETTING', source: 'sidepanel', payload: { vpsUrl } }); - } +inputVpsUrl.addEventListener('input', () => { + markSettingsDirty(true); + scheduleSettingsAutoSave(); +}); +inputVpsUrl.addEventListener('blur', () => { + saveSettings({ silent: true }).catch(() => {}); }); -inputVpsPassword.addEventListener('change', async () => { - await chrome.runtime.sendMessage({ - type: 'SAVE_SETTING', - source: 'sidepanel', - payload: { vpsPassword: inputVpsPassword.value }, - }); +inputVpsPassword.addEventListener('input', () => { + markSettingsDirty(true); + scheduleSettingsAutoSave(); +}); +inputVpsPassword.addEventListener('blur', () => { + saveSettings({ silent: true }).catch(() => {}); }); -inputPassword.addEventListener('change', async () => { - await chrome.runtime.sendMessage({ - type: 'SAVE_SETTING', - source: 'sidepanel', - payload: { customPassword: inputPassword.value }, - }); +inputPassword.addEventListener('input', () => { + markSettingsDirty(true); + updateButtonStates(); + scheduleSettingsAutoSave(); +}); +inputPassword.addEventListener('blur', () => { + saveSettings({ silent: true }).catch(() => {}); }); -selectMailProvider.addEventListener('change', async () => { +selectMailProvider.addEventListener('change', () => { updateMailProviderUI(); - await chrome.runtime.sendMessage({ - type: 'SAVE_SETTING', source: 'sidepanel', - payload: { mailProvider: selectMailProvider.value }, - }); + markSettingsDirty(true); + saveSettings({ silent: true }).catch(() => {}); }); -inputInbucketMailbox.addEventListener('change', async () => { - await chrome.runtime.sendMessage({ - type: 'SAVE_SETTING', - source: 'sidepanel', - payload: { inbucketMailbox: inputInbucketMailbox.value.trim() }, - }); +inputInbucketMailbox.addEventListener('input', () => { + markSettingsDirty(true); + scheduleSettingsAutoSave(); +}); +inputInbucketMailbox.addEventListener('blur', () => { + saveSettings({ silent: true }).catch(() => {}); }); -inputInbucketHost.addEventListener('change', async () => { - await chrome.runtime.sendMessage({ - type: 'SAVE_SETTING', - source: 'sidepanel', - payload: { inbucketHost: inputInbucketHost.value.trim() }, - }); +inputInbucketHost.addEventListener('input', () => { + markSettingsDirty(true); + scheduleSettingsAutoSave(); +}); +inputInbucketHost.addEventListener('blur', () => { + saveSettings({ silent: true }).catch(() => {}); }); -inputAutoSkipFailures.addEventListener('change', async () => { - await chrome.runtime.sendMessage({ - type: 'SAVE_SETTING', - source: 'sidepanel', - payload: { autoRunSkipFailures: inputAutoSkipFailures.checked }, - }); +inputAutoSkipFailures.addEventListener('change', () => { + markSettingsDirty(true); + saveSettings({ silent: true }).catch(() => {}); }); // ============================================================ @@ -861,6 +947,7 @@ btnTheme.addEventListener('click', () => { initializeManualStepActions(); initTheme(); +updateSaveButtonState(); restoreState().then(() => { syncPasswordToggleLabel(); updateButtonStates();