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();