Эх сурвалжийг харах

feat: 增强设置管理功能,支持持久化保存配置,优化密码显示和保存按钮交互

QLHazyCoder 4 сар өмнө
parent
commit
bf7de173de

+ 15 - 4
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 注册信息
 - tab 注册信息
 - 各来源最近一次打开的地址(用于打开新地址前清理旧标签)
 - 各来源最近一次打开的地址(用于打开新地址前清理旧标签)
-- 自定义设置
 - Auto 当前阶段、当前轮次、暂停信息
 - Auto 当前阶段、当前轮次、暂停信息
 
 
+配置项另外使用 `chrome.storage.local` 持久化保存:
+
+- VPS 地址
+- VPS 管理密钥
+- 自定义密码
+- 邮箱服务
+- Inbucket 主机
+- Inbucket 邮箱名
+- 兜底开关
+
 特点:
 特点:
 
 
-- 浏览器会话级存储
+- 运行时步骤状态是浏览器会话级存储
+- 配置项会持久化保存,关闭浏览器后重新打开仍会恢复
 - 扩展运行期间可在多个步骤之间共享
 - 扩展运行期间可在多个步骤之间共享
 - 代码里已启用 `storage.session` 对 content script 的访问
 - 代码里已启用 `storage.session` 对 content script 的访问
 - 同一来源打开新地址前,会先按来源/站点范围关闭旧标签,避免旧页面残留
 - 同一来源打开新地址前,会先按来源/站点范围关闭旧标签,避免旧页面残留

+ 54 - 30
background.js

@@ -11,9 +11,21 @@ const HUMAN_STEP_DELAY_MAX = 2200;
 initializeSessionStorageAccess();
 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 = {
 const DEFAULT_STATE = {
   currentStep: 0,
   currentStep: 0,
   stepStatuses: {
   stepStatuses: {
@@ -32,23 +44,29 @@ const DEFAULT_STATE = {
   tabRegistry: {},
   tabRegistry: {},
   sourceLastUrls: {},
   sourceLastUrls: {},
   logs: [],
   logs: [],
-  vpsUrl: '',
-  vpsPassword: '',
-  customPassword: '',
-  autoRunSkipFailures: false,
+  ...PERSISTED_SETTING_DEFAULTS,
   autoRunning: false,
   autoRunning: false,
   autoRunPhase: 'idle',
   autoRunPhase: 'idle',
   autoRunCurrentRun: 0,
   autoRunCurrentRun: 0,
   autoRunTotalRuns: 1,
   autoRunTotalRuns: 1,
   autoRunAttemptRun: 0,
   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() {
 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() {
 async function initializeSessionStorageAccess() {
@@ -69,6 +87,21 @@ async function setState(updates) {
   await chrome.storage.session.set(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) {
 function broadcastDataUpdate(payload) {
   chrome.runtime.sendMessage({
   chrome.runtime.sendMessage({
     type: 'DATA_UPDATED',
     type: 'DATA_UPDATED',
@@ -89,35 +122,25 @@ async function setPasswordState(password) {
 async function resetState() {
 async function resetState() {
   console.log(LOG_PREFIX, 'Resetting all state');
   console.log(LOG_PREFIX, 'Resetting all state');
   // Preserve settings and persistent data across resets
   // 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.clear();
   await chrome.storage.session.set({
   await chrome.storage.session.set({
     ...DEFAULT_STATE,
     ...DEFAULT_STATE,
+    ...persistedSettings,
     seenCodes: prev.seenCodes || [],
     seenCodes: prev.seenCodes || [],
     seenInbucketMailIds: prev.seenInbucketMailIds || [],
     seenInbucketMailIds: prev.seenInbucketMailIds || [],
     accounts: prev.accounts || [],
     accounts: prev.accounts || [],
     tabRegistry: prev.tabRegistry || {},
     tabRegistry: prev.tabRegistry || {},
     sourceLastUrls: prev.sourceLastUrls || {},
     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.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;
+      await setPersistentSettings(updates);
       await setState(updates);
       await setState(updates);
       return { ok: true };
       return { ok: true };
     }
     }

+ 34 - 1
sidepanel/sidepanel.css

@@ -256,6 +256,14 @@ header {
   min-width: 0;
   min-width: 0;
 }
 }
 
 
+.input-with-icon {
+  position: relative;
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  align-items: center;
+}
+
 .data-label {
 .data-label {
   width: 56px;
   width: 56px;
   font-size: 11px;
   font-size: 11px;
@@ -301,11 +309,36 @@ header {
 .data-input::placeholder { color: var(--text-muted); }
 .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: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 {
 #btn-fetch-email {
   padding-inline: 10px;
   padding-inline: 10px;
 }
 }
 
 
-#btn-toggle-password {
+#btn-save-settings {
   min-width: 58px;
   min-width: 58px;
   padding-inline: 10px;
   padding-inline: 10px;
 }
 }

+ 5 - 2
sidepanel/sidepanel.html

@@ -78,8 +78,11 @@
       <div class="data-row">
       <div class="data-row">
         <span class="data-label">密码</span>
         <span class="data-label">密码</span>
         <div class="data-inline">
         <div class="data-inline">
-          <input type="password" id="input-password" class="data-input" placeholder="留空则自动生成" />
-          <button id="btn-toggle-password" class="btn btn-outline btn-sm" type="button">显示</button>
+          <div class="input-with-icon">
+            <input type="password" id="input-password" class="data-input data-input-with-icon" placeholder="留空则自动生成" />
+            <button id="btn-toggle-password" class="input-icon-btn" type="button" aria-label="显示密码" title="显示密码"></button>
+          </div>
+          <button id="btn-save-settings" class="btn btn-outline btn-sm" type="button">保存</button>
         </div>
         </div>
       </div>
       </div>
       <div class="data-row data-check-row">
       <div class="data-row data-check-row">

+ 130 - 43
sidepanel/sidepanel.js

@@ -18,6 +18,7 @@ const inputEmail = document.getElementById('input-email');
 const inputPassword = document.getElementById('input-password');
 const inputPassword = document.getElementById('input-password');
 const btnFetchEmail = document.getElementById('btn-fetch-email');
 const btnFetchEmail = document.getElementById('btn-fetch-email');
 const btnTogglePassword = document.getElementById('btn-toggle-password');
 const btnTogglePassword = document.getElementById('btn-toggle-password');
+const btnSaveSettings = document.getElementById('btn-save-settings');
 const btnStop = document.getElementById('btn-stop');
 const btnStop = document.getElementById('btn-stop');
 const btnReset = document.getElementById('btn-reset');
 const btnReset = document.getElementById('btn-reset');
 const stepsProgress = document.getElementById('steps-progress');
 const stepsProgress = document.getElementById('steps-progress');
@@ -55,6 +56,12 @@ let currentAutoRun = {
   totalRuns: 1,
   totalRuns: 1,
   attemptRun: 0,
   attemptRun: 0,
 };
 };
+let settingsDirty = false;
+let settingsSaveInFlight = false;
+let settingsAutoSaveTimer = 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_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>';
 
 
 // ============================================================
 // ============================================================
 // Toast Notifications
 // Toast Notifications
@@ -154,6 +161,77 @@ function setDefaultAutoRunButton() {
   btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> 自动';
   btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> 自动';
 }
 }
 
 
+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) {
 function applyAutoRunStatus(payload = currentAutoRun) {
   syncAutoRunState(payload);
   syncAutoRunState(payload);
   const runLabel = getAutoRunLabel(currentAutoRun);
   const runLabel = getAutoRunLabel(currentAutoRun);
@@ -274,6 +352,7 @@ async function restoreState() {
     }
     }
 
 
     applyAutoRunStatus(state);
     applyAutoRunStatus(state);
+    markSettingsDirty(false);
     updateStatusDisplay(latestState);
     updateStatusDisplay(latestState);
     updateProgressCounter();
     updateProgressCounter();
     updateMailProviderUI();
     updateMailProviderUI();
@@ -503,7 +582,10 @@ async function fetchDuckEmail(options = {}) {
 }
 }
 
 
 function syncPasswordToggleLabel() {
 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) {
 async function maybeTakeoverAutoRun(actionLabel) {
@@ -606,6 +688,14 @@ btnTogglePassword.addEventListener('click', () => {
   syncPasswordToggleLabel();
   syncPasswordToggleLabel();
 });
 });
 
 
+btnSaveSettings.addEventListener('click', async () => {
+  if (!settingsDirty) {
+    showToast('配置已是最新', 'info', 1400);
+    return;
+  }
+  await saveSettings({ silent: false }).catch(() => {});
+});
+
 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: {} });
@@ -656,6 +746,7 @@ btnReset.addEventListener('click', async () => {
     document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
     document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
     setDefaultAutoRunButton();
     setDefaultAutoRunButton();
     applyAutoRunStatus(currentAutoRun);
     applyAutoRunStatus(currentAutoRun);
+    markSettingsDirty(false);
     updateStopButtonState(false);
     updateStopButtonState(false);
     updateButtonStates();
     updateButtonStates();
     updateProgressCounter();
     updateProgressCounter();
@@ -675,61 +766,56 @@ inputEmail.addEventListener('change', async () => {
   }
   }
 });
 });
 inputEmail.addEventListener('input', updateButtonStates);
 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();
   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();
 initializeManualStepActions();
 initTheme();
 initTheme();
+updateSaveButtonState();
 restoreState().then(() => {
 restoreState().then(() => {
   syncPasswordToggleLabel();
   syncPasswordToggleLabel();
   updateButtonStates();
   updateButtonStates();