From 5d70d2b6817ab7b184624d7df94d1ff24f1ea31b Mon Sep 17 00:00:00 2001 From: QLHazyCoder Date: Mon, 4 May 2026 14:20:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AF=B9=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=BF=90=E8=A1=8C=E7=8A=B6=E6=80=81=E7=9A=84=E7=AE=A1?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E4=BC=98=E5=8C=96=E8=BD=AE=E6=95=B0=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E9=80=BB=E8=BE=91=E5=8F=8A=E7=9B=B8=E5=85=B3=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sidepanel/sidepanel.js | 65 +++++++++++++++- tests/run-count-unlimited.test.js | 78 +++++++++++++++++++ ...sidepanel-auto-run-content-refresh.test.js | 41 +++++++++- 项目完整链路说明.md | 6 +- 项目文件结构说明.md | 4 +- 5 files changed, 185 insertions(+), 9 deletions(-) diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 7f8235a..214493c 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -1034,6 +1034,8 @@ let currentAutoRun = { countdownTitle: '', countdownNote: '', }; +let pendingAutoRunStartTotalRuns = 0; +let pendingAutoRunStartExpiresAt = 0; let settingsDirty = false; let settingsSaveInFlight = false; let settingsAutoSaveTimer = null; @@ -2011,6 +2013,34 @@ function readAutoRunStateValue(source, keys, fallback) { return fallback; } +function normalizePendingAutoRunStartRunCount(value) { + const numeric = Math.floor(Number(value) || 0); + return numeric > 0 ? numeric : 0; +} + +function registerPendingAutoRunStartRunCount(totalRuns) { + pendingAutoRunStartTotalRuns = normalizePendingAutoRunStartRunCount(totalRuns); + pendingAutoRunStartExpiresAt = pendingAutoRunStartTotalRuns > 0 + ? Date.now() + 30000 + : 0; +} + +function clearPendingAutoRunStartRunCount() { + pendingAutoRunStartTotalRuns = 0; + pendingAutoRunStartExpiresAt = 0; +} + +function getPendingAutoRunStartRunCount() { + if (pendingAutoRunStartTotalRuns > 0 && pendingAutoRunStartExpiresAt > 0 && Date.now() > pendingAutoRunStartExpiresAt) { + clearPendingAutoRunStartRunCount(); + } + return pendingAutoRunStartTotalRuns; +} + +function getAutoRunSourceTotalRuns(source = {}) { + return normalizePendingAutoRunStartRunCount(readAutoRunStateValue(source, ['autoRunTotalRuns', 'totalRuns'], 0)); +} + function syncAutoRunState(source = {}) { const phase = source.autoRunPhase ?? source.phase ?? currentAutoRun.phase; const autoRunning = source.autoRunning !== undefined @@ -2074,7 +2104,22 @@ function shouldSyncRunCountFromAutoRunSource(source = {}) { const autoRunning = source.autoRunning !== undefined ? Boolean(source.autoRunning) : isAutoRunSourceSyncPhase(phase); - return autoRunning || isAutoRunSourceSyncPhase(phase); + const shouldSync = autoRunning || isAutoRunSourceSyncPhase(phase); + if (!shouldSync) { + return false; + } + + const pendingTotalRuns = getPendingAutoRunStartRunCount(); + if (pendingTotalRuns > 0) { + const sourceTotalRuns = getAutoRunSourceTotalRuns(source); + if (sourceTotalRuns > 0 && sourceTotalRuns !== pendingTotalRuns) { + return false; + } + if (sourceTotalRuns === pendingTotalRuns) { + clearPendingAutoRunStartRunCount(); + } + } + return true; } function getAutoRunLabel(payload = currentAutoRun) { @@ -10142,6 +10187,14 @@ autoStartModal?.addEventListener('click', (event) => { btnAutoStartClose?.addEventListener('click', () => resolveModalChoice(null)); async function startAutoRunFromCurrentSettings() { + const initialLockedRunCount = typeof getLockedRunCountFromEmailPool === 'function' + ? getLockedRunCountFromEmailPool() + : 0; + const requestedTotalRuns = initialLockedRunCount > 0 + ? initialLockedRunCount + : getRunCountValue(); + registerPendingAutoRunStartRunCount(requestedTotalRuns); + try { await refreshContributionContentHint(); } catch (error) { @@ -10160,7 +10213,8 @@ async function startAutoRunFromCurrentSettings() { if (customEmailPoolEnabled && lockedRunCount <= 0) { throw new Error('请先在邮箱池里至少填写 1 个邮箱。'); } - const totalRuns = lockedRunCount > 0 ? lockedRunCount : getRunCountValue(); + const totalRuns = lockedRunCount > 0 ? lockedRunCount : requestedTotalRuns; + registerPendingAutoRunStartRunCount(totalRuns); if (lockedRunCount > 0) { inputRunCount.value = String(lockedRunCount); } @@ -10181,6 +10235,7 @@ async function startAutoRunFromCurrentSettings() { const runningStep = getRunningSteps()[0] ?? null; const choice = await openAutoStartChoiceDialog(startStep, { runningStep }); if (!choice) { + clearPendingAutoRunStartRunCount(); return false; } mode = choice; @@ -10188,6 +10243,7 @@ async function startAutoRunFromCurrentSettings() { const confirmedPlusContributionPrompt = await maybeShowPlusContributionPromptBeforeAutoRun(plusModeEnabled); if (!confirmedPlusContributionPrompt) { + clearPendingAutoRunStartRunCount(); return false; } @@ -10195,6 +10251,7 @@ async function startAutoRunFromCurrentSettings() { && !isAutoRunFallbackRiskPromptDismissed()) { const result = await openAutoRunFallbackRiskConfirmModal(totalRuns); if (!result.confirmed) { + clearPendingAutoRunStartRunCount(); return false; } if (result.dismissPrompt) { @@ -10206,6 +10263,7 @@ async function startAutoRunFromCurrentSettings() { && !isAutoRunPlusRiskPromptDismissed()) { const result = await openPlusAutoRunRiskConfirmModal(totalRuns); if (!result.confirmed) { + clearPendingAutoRunStartRunCount(); return false; } if (result.dismissPrompt) { @@ -10235,6 +10293,7 @@ async function startAutoRunFromCurrentSettings() { }, }); if (response?.error) { + clearPendingAutoRunStartRunCount(); throw new Error(response.error); } return true; @@ -10245,6 +10304,7 @@ btnAutoRun.addEventListener('click', async () => { try { await startAutoRunFromCurrentSettings(); } catch (err) { + clearPendingAutoRunStartRunCount(); setDefaultAutoRunButton(); inputRunCount.disabled = shouldLockRunCountToEmailPool(); showToast(err.message, 'error'); @@ -11227,6 +11287,7 @@ inputInbucketHost.addEventListener('blur', () => { }); inputRunCount.addEventListener('input', () => { + clearPendingAutoRunStartRunCount(); updateFallbackThreadIntervalInputState(); }); inputRunCount.addEventListener('blur', () => { diff --git a/tests/run-count-unlimited.test.js b/tests/run-count-unlimited.test.js index 1d20cb0..b0346fc 100644 --- a/tests/run-count-unlimited.test.js +++ b/tests/run-count-unlimited.test.js @@ -157,6 +157,84 @@ return { assert.equal(api.isDisabled(), true); }); +test('sidepanel pending auto-run start ignores stale active run count sync', () => { + const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8'); + const bundle = [ + extractFunction(source, 'hasOwnStateValue'), + extractFunction(source, 'readAutoRunStateValue'), + extractFunction(source, 'normalizePendingAutoRunStartRunCount'), + extractFunction(source, 'registerPendingAutoRunStartRunCount'), + extractFunction(source, 'clearPendingAutoRunStartRunCount'), + extractFunction(source, 'getPendingAutoRunStartRunCount'), + extractFunction(source, 'getAutoRunSourceTotalRuns'), + extractFunction(source, 'syncAutoRunState'), + extractFunction(source, 'isAutoRunLockedPhase'), + extractFunction(source, 'isAutoRunPausedPhase'), + extractFunction(source, 'isAutoRunScheduledPhase'), + extractFunction(source, 'isAutoRunSourceSyncPhase'), + extractFunction(source, 'shouldSyncRunCountFromAutoRunSource'), + extractFunction(source, 'applyAutoRunStatus'), + ].join('\n'); + + const api = new Function(` +let currentAutoRun = { + autoRunning: false, + phase: 'idle', + currentRun: 0, + totalRuns: 1, + attemptRun: 0, + scheduledAt: null, + countdownAt: null, + countdownTitle: '', + countdownNote: '', +}; +let pendingAutoRunStartTotalRuns = 0; +let pendingAutoRunStartExpiresAt = 0; +const inputRunCount = { value: '20', disabled: false }; +const btnAutoRun = { disabled: false, innerHTML: '' }; +const btnFetchEmail = { disabled: false }; +const inputEmail = { disabled: false }; +const inputAutoSkipFailures = { disabled: false }; +const autoContinueBar = { style: { display: '' } }; + +function setSettingsCardLocked() {} +function getAutoRunLabel() { return ''; } +function getLockedRunCountFromEmailPool() { return 0; } +function isCustomMailProvider() { return false; } +function usesCustomEmailPoolGenerator() { return false; } +function setDefaultAutoRunButton() {} +function updateAutoDelayInputState() {} +function updateFallbackThreadIntervalInputState() {} +function syncScheduledCountdownTicker() {} +function updateStopButtonState() {} +function getStepStatuses() { return {}; } +function updateConfigMenuControls() {} +function renderContributionMode() {} + +${bundle} + +return { + applyAutoRunStatus, + registerPendingAutoRunStartRunCount, + getValue() { + return inputRunCount.value; + }, + getPending() { + return pendingAutoRunStartTotalRuns; + }, +}; +`)(); + + api.registerPendingAutoRunStartRunCount(20); + api.applyAutoRunStatus({ autoRunning: true, autoRunPhase: 'running', autoRunTotalRuns: 1 }); + assert.equal(api.getValue(), '20'); + assert.equal(api.getPending(), 20); + + api.applyAutoRunStatus({ autoRunning: true, autoRunPhase: 'running', autoRunTotalRuns: 20 }); + assert.equal(api.getValue(), '20'); + assert.equal(api.getPending(), 0); +}); + test('background normalizeRunCount no longer clamps values to 50', () => { const source = fs.readFileSync('background.js', 'utf8'); const bundle = extractFunction(source, 'normalizeRunCount'); diff --git a/tests/sidepanel-auto-run-content-refresh.test.js b/tests/sidepanel-auto-run-content-refresh.test.js index 6e9b5a1..59ca371 100644 --- a/tests/sidepanel-auto-run-content-refresh.test.js +++ b/tests/sidepanel-auto-run-content-refresh.test.js @@ -56,8 +56,14 @@ function createApi({ plusRiskConfirmed = true, plusRiskDismissPrompt = false, plusContributionImpl, + persistImpl, } = {}) { - const bundle = extractFunction('startAutoRunFromCurrentSettings'); + const bundle = [ + extractFunction('normalizePendingAutoRunStartRunCount'), + extractFunction('registerPendingAutoRunStartRunCount'), + extractFunction('clearPendingAutoRunStartRunCount'), + extractFunction('startAutoRunFromCurrentSettings'), + ].join('\n'); return new Function(` const events = []; @@ -72,6 +78,9 @@ const inputAutoDelayEnabled = { checked: false }; const inputAutoDelayMinutes = { value: '30' }; const btnAutoRun = { disabled: false, innerHTML: '' }; const inputRunCount = { disabled: false }; +let runCountValue = ${Math.max(1, Number(runCount) || 1)}; +let pendingAutoRunStartTotalRuns = 0; +let pendingAutoRunStartExpiresAt = 0; const chrome = { runtime: { async sendMessage(message) { @@ -87,8 +96,16 @@ const console = { }; async function persistCurrentSettingsForAction() { events.push({ type: 'sync-settings' }); + ${persistImpl ? `return (${persistImpl})(events, { + setRunCount(value) { + runCountValue = Math.max(1, Number(value) || 1); + }, + getRunCount() { + return runCountValue; + }, + });` : ''} } -function getRunCountValue() { return ${Math.max(1, Number(runCount) || 1)}; } +function getRunCountValue() { return Math.max(1, Number(runCountValue) || 1); } function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; } function shouldOfferAutoModeChoice() { return false; } async function openAutoStartChoiceDialog() { throw new Error('should not be called'); } @@ -177,6 +194,26 @@ test('startAutoRunFromCurrentSettings does not block auto run when contribution ); }); +test('startAutoRunFromCurrentSettings freezes run count before async settings sync can repaint it', async () => { + const api = createApi({ + runCount: 20, + persistImpl: `(events, controls) => { + controls.setRunCount(1); + events.push({ type: 'stale-status-reset', runCount: controls.getRunCount() }); + }`, + }); + + const result = await api.startAutoRunFromCurrentSettings(); + const events = api.getEvents(); + + assert.equal(result, true); + assert.deepEqual( + events.map((entry) => entry.type), + ['refresh', 'sync-settings', 'stale-status-reset', 'send'] + ); + assert.equal(events[3].message.payload.totalRuns, 20); +}); + test('startAutoRunFromCurrentSettings shows Plus risk warning before starting more than 3 runs', async () => { const api = createApi({ runCount: 4, diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 800bcf2..c9476af 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -40,7 +40,7 @@ - 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源 - 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态 - 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `apikey.qzz.io` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现 -- 在 sidepanel 初始化和点击“自动”按钮前刷新一次贡献站公开内容摘要;如果刷新失败,不阻塞主自动流程 +- 在 sidepanel 初始化和点击“自动”按钮前刷新一次贡献站公开内容摘要;点击“自动”时会先冻结当时输入或号池锁定的目标轮数,后续刷新、保存和后台状态回灌不能把启动目标改回旧的 1 轮;如果刷新失败,不阻塞主自动流程 - 在日志区通过“记录”按钮打开独立的账号记录覆盖层,并展示成功/失败/停止/重试统计与分页列表;phone-only 记录会回退展示手机号 - 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro` 或 `v` 版本误显示为比 `Ultra` 更新 - 展示一个单独的“接码”开关、注册方式 `signupMethod` 与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim / NexSMS。普通模式下开启接码后可把注册方式切到手机号注册,并在 OAuth 登录链路命中手机号登录验证码页时继续复用同一手机号续跑短信验证 @@ -249,7 +249,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功 4. `sidepanel.js` 在 sidepanel 初始化时会先拉一次摘要;如果当前 `promptVersion` 没被本地关闭过,就显示提示 5. 用户点击提示右侧 `X` 后,只会把当前 `promptVersion` 记入 `localStorage`,不会永久关闭整个能力 6. 当公告或教程再次更新,服务端返回新的 `promptVersion`,提示会重新出现 -7. 用户点击“自动”按钮时,sidepanel 会在真正启动自动流程前再刷新一次摘要,尽量让提示状态更及时 +7. 用户点击“自动”按钮时,sidepanel 会先冻结当时输入或号池锁定的目标轮数,再在真正启动自动流程前刷新一次摘要,尽量让提示状态更及时;刷新、保存或后台旧状态回灌期间不会重新读取输入框导致轮数回退 8. 如果这次刷新失败,sidepanel 只记录警告并继续自动流程,不会因为提示链路故障阻塞主功能 这条链路的关键边界是: @@ -845,7 +845,7 @@ Hide My Email 获取与管理链路: 流程: -1. 读取总轮数与模式 +1. 读取总轮数与模式;sidepanel 在点击“自动”瞬间先冻结目标轮数,后台确认进入同一目标轮数前会忽略旧 active 状态里的过期 `autoRunTotalRuns` 2. 为本轮自动流程分配唯一 `autoRunSessionId` 3. 计算是否从中断点继续 4. 每轮执行前重置必要运行态 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 0f5ac74..30695e0 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -154,7 +154,7 @@ 到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显 示并同时服务于 provide / receive 两种模式;当 provider 为 iCloud 时,会保存并回显目标邮箱类型与转发邮箱 provider,相关 provider 选 项和 label 复用 `mail-provider-utils.js`;`自定义邮箱池` 模式会按邮箱池长度锁定自动轮数,并把当前输入的邮箱池配置参与自动启动前保 - 存;`邮箱服务 = 自定义邮箱` 时,如果配置了 `customMailProviderPool`,也会按号池长度锁定自动轮数并在 Auto 中按轮次分配注册邮箱,同时 + 存;点击“自动”时会先冻结当时的目标轮数,避免启动前的异步刷新、保存或后台旧状态回灌把输入框重置为 1;`邮箱服务 = 自定义邮箱` 时,如果配置了 `customMailProviderPool`,也会按号池长度锁定自动轮数并在 Auto 中按轮次分配注册邮箱,同时 在普通失败时继续复用当前邮箱,只有成功或出现手机号验证时才切换下一个邮箱;Plus 模式开启后,步骤列表会切换为 13 步,并显示 PayPal 账 号下拉框与添加入口;当前选中的 PayPal 账号会同步回兼容字段 `paypalEmail / paypalPassword` 供后台步骤复用;Step 8 的自定义邮箱确认弹 窗当前额外提供“出现手机号验证”按钮,用于直接走与真实 add-phone 一致的 fatal 分支;HeroSMS 国家列表 @@ -230,7 +230,7 @@ - `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。 - `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池、自定义邮箱服务号池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。 - `tests/sidepanel-contribution-mode.test.js`:测试侧边栏贡献模式的 HTML 接线、runtime-only 设置保护,以及贡献模式 manager 复用主自动流启动、状态轮询和退出清理逻辑。 -- `tests/sidepanel-auto-run-content-refresh.test.js`:测试点击“自动”时会先刷新贡献内容更新摘要,且刷新失败不会阻塞自动流程启动。 +- `tests/sidepanel-auto-run-content-refresh.test.js`:测试点击“自动”时会先冻结当前轮数并刷新贡献内容更新摘要,且刷新失败或启动前状态回灌不会阻塞或改写自动流程启动目标。 - `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。 - `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析、目标邮箱类型 / 转发邮箱 provider 保存、回显和显隐联动。 - `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。