diff --git a/background.js b/background.js index e62b730..c989833 100644 --- a/background.js +++ b/background.js @@ -465,6 +465,10 @@ const PERSISTED_SETTING_DEFAULTS = { hotmailServiceMode: HOTMAIL_SERVICE_MODE_LOCAL, hotmailRemoteBaseUrl: DEFAULT_HOTMAIL_REMOTE_BASE_URL, hotmailLocalBaseUrl: DEFAULT_HOTMAIL_LOCAL_BASE_URL, + luckmailApiKey: '', + luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL, + luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE, + luckmailDomain: '', cloudflareDomain: '', cloudflareDomains: [], cloudflareTempEmailBaseUrl: '', @@ -1295,6 +1299,14 @@ function normalizePersistentSettingValue(key, value) { return normalizeHotmailRemoteBaseUrl(value); case 'hotmailLocalBaseUrl': return normalizeHotmailLocalBaseUrl(value); + case 'luckmailApiKey': + return String(value || ''); + case 'luckmailBaseUrl': + return normalizeLuckmailBaseUrl(value); + case 'luckmailEmailType': + return normalizeLuckmailEmailType(value); + case 'luckmailDomain': + return String(value || '').trim(); case 'cloudflareDomain': return normalizeCloudflareDomain(value); case 'cloudflareDomains': diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 5d66c65..4d416d3 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -1407,6 +1407,12 @@ async function settlePendingSettingsBeforeImport() { await waitForSettingsSaveIdle(); } +async function persistCurrentSettingsForAction() { + clearTimeout(settingsAutoSaveTimer); + await waitForSettingsSaveIdle(); + await saveSettings({ silent: true, force: true }); +} + function downloadTextFile(content, fileName, mimeType = 'application/json;charset=utf-8') { const blob = new Blob([content], { type: mimeType }); const objectUrl = URL.createObjectURL(blob); @@ -2559,10 +2565,10 @@ function scheduleSettingsAutoSave() { } async function saveSettings(options = {}) { - const { silent = false } = options; + const { silent = false, force = false } = options; clearTimeout(settingsAutoSaveTimer); - if (!settingsDirty && !settingsSaveInFlight && silent) { + if (!force && !settingsDirty && !settingsSaveInFlight && silent) { return; } @@ -5005,6 +5011,7 @@ stepsList?.addEventListener('click', async (event) => { if (!(await maybeTakeoverAutoRun(`执行步骤 ${step}`))) { return; } + await persistCurrentSettingsForAction(); if (step === 3) { if (inputPassword.value !== (latestState?.customPassword || '')) { await chrome.runtime.sendMessage({ @@ -5236,8 +5243,8 @@ async function startAutoRunFromCurrentSettings() { console.warn('Failed to refresh contribution content hint before auto run:', error); } - if (typeof settingsDirty !== 'undefined' && settingsDirty && typeof saveSettings === 'function') { - await saveSettings({ silent: true }); + if (typeof persistCurrentSettingsForAction === 'function') { + await persistCurrentSettingsForAction(); } const customEmailPoolEnabled = typeof usesCustomEmailPoolGenerator === 'function' diff --git a/tests/background-luckmail.test.js b/tests/background-luckmail.test.js index ee36635..debeb9a 100644 --- a/tests/background-luckmail.test.js +++ b/tests/background-luckmail.test.js @@ -437,6 +437,58 @@ return { assert.equal(snapshot.tokenCodeCalls, 2); }); +test('buildPersistentSettingsPayload keeps LuckMail config fields for storage.local persistence', () => { + const bundle = [ + extractFunction('normalizePersistentSettingValue'), + extractFunction('buildPersistentSettingsPayload'), + ].join('\n'); + + const factory = new Function(` +const DEFAULT_LUCKMAIL_BASE_URL = 'https://mails.luckyous.com'; +const DEFAULT_LUCKMAIL_EMAIL_TYPE = 'ms_graph'; +const PERSISTED_SETTING_DEFAULTS = { + luckmailApiKey: '', + luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL, + luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE, + luckmailDomain: '', +}; +const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS); +function normalizeLuckmailBaseUrl(value) { + const normalized = String(value || '').trim() || DEFAULT_LUCKMAIL_BASE_URL; + return normalized.replace(/\\/$/, ''); +} +function normalizeLuckmailEmailType(value) { + return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim()) + ? String(value || '').trim() + : DEFAULT_LUCKMAIL_EMAIL_TYPE; +} +function resolveLegacyAutoStepDelaySeconds() { + return undefined; +} + +${bundle} + +return { + buildPersistentSettingsPayload, +}; +`); + + const api = factory(); + const payload = api.buildPersistentSettingsPayload({ + luckmailApiKey: 'sk-live-demo', + luckmailBaseUrl: 'https://demo.example.com/', + luckmailEmailType: 'ms_imap', + luckmailDomain: ' outlook.com ', + }); + + assert.deepStrictEqual(payload, { + luckmailApiKey: 'sk-live-demo', + luckmailBaseUrl: 'https://demo.example.com', + luckmailEmailType: 'ms_imap', + luckmailDomain: 'outlook.com', + }); +}); + test('listLuckmailPurchasesByProject only keeps openai purchases', async () => { const bundle = extractFunction('listLuckmailPurchasesByProject'); diff --git a/tests/sidepanel-auto-run-content-refresh.test.js b/tests/sidepanel-auto-run-content-refresh.test.js index 5258fcf..6e9b5a1 100644 --- a/tests/sidepanel-auto-run-content-refresh.test.js +++ b/tests/sidepanel-auto-run-content-refresh.test.js @@ -85,6 +85,9 @@ const console = { events.push({ type: 'warn', args }); }, }; +async function persistCurrentSettingsForAction() { + events.push({ type: 'sync-settings' }); +} function getRunCountValue() { return ${Math.max(1, Number(runCount) || 1)}; } function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; } function shouldOfferAutoModeChoice() { return false; } @@ -135,9 +138,9 @@ test('startAutoRunFromCurrentSettings refreshes contribution content hint before assert.equal(result, true); assert.deepEqual( api.getEvents().map((entry) => entry.type), - ['refresh', 'send'] + ['refresh', 'sync-settings', 'send'] ); - assert.equal(api.getEvents()[1].message.type, 'AUTO_RUN'); + assert.equal(api.getEvents()[2].message.type, 'AUTO_RUN'); }); test('startAutoRunFromCurrentSettings continues auto run when contribution content refresh fails', async () => { @@ -151,10 +154,10 @@ test('startAutoRunFromCurrentSettings continues auto run when contribution conte assert.equal(result, true); assert.deepEqual( events.map((entry) => entry.type), - ['refresh', 'warn', 'send'] + ['refresh', 'warn', 'sync-settings', 'send'] ); assert.match(String(events[1].args[0]), /Failed to refresh contribution content hint before auto run/); - assert.equal(events[2].message.type, 'AUTO_RUN'); + assert.equal(events[3].message.type, 'AUTO_RUN'); }); test('startAutoRunFromCurrentSettings does not block auto run when contribution content has updates', async () => { @@ -170,7 +173,7 @@ test('startAutoRunFromCurrentSettings does not block auto run when contribution assert.equal(result, true); assert.deepEqual( api.getEvents().map((entry) => entry.type), - ['refresh', 'send'] + ['refresh', 'sync-settings', 'send'] ); }); @@ -187,10 +190,10 @@ test('startAutoRunFromCurrentSettings shows Plus risk warning before starting mo assert.equal(result, true); assert.deepEqual( events.map((entry) => entry.type), - ['refresh', 'plus-risk-modal', 'send'] + ['refresh', 'sync-settings', 'plus-risk-modal', 'send'] ); - assert.equal(events[1].totalRuns, 4); - assert.equal(events[2].message.payload.totalRuns, 4); + assert.equal(events[2].totalRuns, 4); + assert.equal(events[3].message.payload.totalRuns, 4); }); test('startAutoRunFromCurrentSettings aborts when Plus risk warning is declined', async () => { @@ -206,7 +209,7 @@ test('startAutoRunFromCurrentSettings aborts when Plus risk warning is declined' assert.equal(result, false); assert.deepEqual( api.getEvents().map((entry) => entry.type), - ['refresh', 'plus-risk-modal'] + ['refresh', 'sync-settings', 'plus-risk-modal'] ); }); @@ -224,7 +227,58 @@ test('startAutoRunFromCurrentSettings aborts when Plus contribution prompt opens assert.equal(result, false); assert.deepEqual( api.getEvents().map((entry) => entry.type), - ['refresh', 'plus-contribution-modal'] + ['refresh', 'sync-settings', 'plus-contribution-modal'] ); - assert.equal(api.getEvents()[1].plusModeEnabled, true); + assert.equal(api.getEvents()[2].plusModeEnabled, true); +}); + +test('persistCurrentSettingsForAction forces a silent save even when settings are not marked dirty', async () => { + const bundle = [ + extractFunction('waitForSettingsSaveIdle'), + extractFunction('saveSettings'), + extractFunction('persistCurrentSettingsForAction'), + ].join('\n'); + + const api = new Function(` +let settingsAutoSaveTimer = 123; +let clearedTimer = null; +let settingsSaveInFlight = false; +let settingsDirty = false; +let settingsSaveRevision = 0; +const saveCalls = []; +function clearTimeout(value) { + clearedTimer = value; +} +function updateSaveButtonState() {} +function collectSettingsPayload() { + return { luckmailApiKey: 'autofilled-key' }; +} +function syncLatestState() {} +function updatePanelModeUI() {} +function updateMailProviderUI() {} +function updateButtonStates() {} +function markSettingsDirty() {} +function applySettingsState() {} +const chrome = { + runtime: { + async sendMessage(message) { + saveCalls.push(message.payload); + return { state: { luckmailApiKey: message.payload.luckmailApiKey } }; + }, + }, +}; +${bundle} +return { + persistCurrentSettingsForAction, + getSnapshot() { + return { clearedTimer, saveCalls }; + }, +}; +`)(); + + await api.persistCurrentSettingsForAction(); + const snapshot = api.getSnapshot(); + + assert.equal(snapshot.clearedTimer, 123); + assert.deepStrictEqual(snapshot.saveCalls, [{ luckmailApiKey: 'autofilled-key' }]); });