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

This commit is contained in:
QLHazyCoder
2026-04-08 11:22:14 +08:00
parent 9d4b86e7e0
commit bf7de173de
5 changed files with 238 additions and 80 deletions
+15 -4
View File
@@ -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 的访问
- 同一来源打开新地址前,会先按来源/站点范围关闭旧标签,避免旧页面残留 - 同一来源打开新地址前,会先按来源/站点范围关闭旧标签,避免旧页面残留
+49 -25
View File
@@ -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: '', ...PERSISTED_SETTING_DEFAULTS,
vpsPassword: '',
customPassword: '',
autoRunSkipFailures: false,
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); const [state, persistedSettings] = await Promise.all([
return { ...DEFAULT_STATE, ...state }; 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([ const [prev, persistedSettings] = await Promise.all([
chrome.storage.session.get([
'seenCodes', 'seenCodes',
'seenInbucketMailIds', 'seenInbucketMailIds',
'accounts', 'accounts',
'tabRegistry', 'tabRegistry',
'sourceLastUrls', 'sourceLastUrls',
'vpsUrl', ]),
'vpsPassword', getPersistedSettings(),
'customPassword',
'autoRunSkipFailures',
'mailProvider',
'inbucketHost',
'inbucketMailbox',
]); ]);
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
View File
@@ -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
View File
@@ -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="留空则自动生成" /> <div class="input-with-icon">
<button id="btn-toggle-password" class="btn btn-outline btn-sm" type="button">显示</button> <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
View File
@@ -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('input', () => {
markSettingsDirty(true);
inputVpsUrl.addEventListener('change', async () => { scheduleSettingsAutoSave();
const vpsUrl = inputVpsUrl.value.trim(); });
if (vpsUrl) { inputVpsUrl.addEventListener('blur', () => {
await chrome.runtime.sendMessage({ type: 'SAVE_SETTING', source: 'sidepanel', payload: { vpsUrl } }); saveSettings({ silent: true }).catch(() => {});
}
}); });
inputVpsPassword.addEventListener('change', async () => { inputVpsPassword.addEventListener('input', () => {
await chrome.runtime.sendMessage({ markSettingsDirty(true);
type: 'SAVE_SETTING', scheduleSettingsAutoSave();
source: 'sidepanel', });
payload: { vpsPassword: inputVpsPassword.value }, inputVpsPassword.addEventListener('blur', () => {
}); saveSettings({ silent: true }).catch(() => {});
}); });
inputPassword.addEventListener('change', async () => { inputPassword.addEventListener('input', () => {
await chrome.runtime.sendMessage({ markSettingsDirty(true);
type: 'SAVE_SETTING', updateButtonStates();
source: 'sidepanel', scheduleSettingsAutoSave();
payload: { customPassword: inputPassword.value }, });
}); inputPassword.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => {});
}); });
selectMailProvider.addEventListener('change', async () => { selectMailProvider.addEventListener('change', () => {
updateMailProviderUI(); updateMailProviderUI();
await chrome.runtime.sendMessage({ markSettingsDirty(true);
type: 'SAVE_SETTING', source: 'sidepanel', saveSettings({ silent: true }).catch(() => {});
payload: { mailProvider: selectMailProvider.value },
});
}); });
inputInbucketMailbox.addEventListener('change', async () => { inputInbucketMailbox.addEventListener('input', () => {
await chrome.runtime.sendMessage({ markSettingsDirty(true);
type: 'SAVE_SETTING', scheduleSettingsAutoSave();
source: 'sidepanel', });
payload: { inbucketMailbox: inputInbucketMailbox.value.trim() }, inputInbucketMailbox.addEventListener('blur', () => {
}); saveSettings({ silent: true }).catch(() => {});
}); });
inputInbucketHost.addEventListener('change', async () => { inputInbucketHost.addEventListener('input', () => {
await chrome.runtime.sendMessage({ markSettingsDirty(true);
type: 'SAVE_SETTING', scheduleSettingsAutoSave();
source: 'sidepanel', });
payload: { inbucketHost: inputInbucketHost.value.trim() }, inputInbucketHost.addEventListener('blur', () => {
}); saveSettings({ silent: true }).catch(() => {});
}); });
inputAutoSkipFailures.addEventListener('change', async () => { inputAutoSkipFailures.addEventListener('change', () => {
await chrome.runtime.sendMessage({ markSettingsDirty(true);
type: 'SAVE_SETTING', saveSettings({ silent: true }).catch(() => {});
source: 'sidepanel',
payload: { autoRunSkipFailures: inputAutoSkipFailures.checked },
});
}); });
// ============================================================ // ============================================================
@@ -861,6 +947,7 @@ btnTheme.addEventListener('click', () => {
initializeManualStepActions(); initializeManualStepActions();
initTheme(); initTheme();
updateSaveButtonState();
restoreState().then(() => { restoreState().then(() => {
syncPasswordToggleLabel(); syncPasswordToggleLabel();
updateButtonStates(); updateButtonStates();