From 527def5a1d10168ba44dfadbebb79b3bc5f20e6f Mon Sep 17 00:00:00 2001 From: QLHazyCoder Date: Tue, 5 May 2026 17:18:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=20Plus=20=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E7=9B=B8=E5=85=B3=E7=9A=84=E4=BB=A3=E7=A0=81=E5=92=8C?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sidepanel/sidepanel.css | 16 - sidepanel/sidepanel.js | 222 ------------- ...sidepanel-auto-run-content-refresh.test.js | 79 ----- tests/sidepanel-auto-run-risk-warning.test.js | 42 --- ...sidepanel-plus-contribution-prompt.test.js | 312 ------------------ 项目文件结构说明.md | 1 - 6 files changed, 672 deletions(-) delete mode 100644 tests/sidepanel-plus-contribution-prompt.test.js diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index bc02d56..6004558 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -3286,22 +3286,6 @@ header { white-space: pre-line; } -.plus-contribution-prompt-copy { - display: block; - margin-bottom: 8px; -} - -.plus-contribution-prompt-image { - display: block; - width: min(220px, 100%); - max-height: 280px; - object-fit: contain; - margin: 12px auto 0; - border: 1px solid var(--border); - border-radius: 8px; - background: var(--bg-primary); -} - .modal-message a, .modal-alert a { color: var(--blue); diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 6e60951..5e81e45 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -708,8 +708,6 @@ const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE; const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed'; const AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-skip-failures-prompt-dismissed'; const AUTO_RUN_FALLBACK_RISK_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-run-fallback-risk-prompt-dismissed'; -const AUTO_RUN_PLUS_RISK_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-run-plus-risk-prompt-dismissed'; -const PLUS_CONTRIBUTION_PROMPT_LEDGER_STORAGE_KEY = 'multipage-plus-contribution-prompt-ledger'; const PHONE_VERIFICATION_SECTION_EXPANDED_STORAGE_KEY = 'multipage-phone-verification-section-expanded'; let phoneVerificationSectionExpanded = false; @@ -794,10 +792,6 @@ function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) { } const CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY = 'multipage-contribution-content-prompt-dismissed-version'; const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 3; -const AUTO_RUN_PLUS_RISK_WARNING_MAX_SAFE_RUNS = 3; -const PLUS_CONTRIBUTION_PROMPT_THRESHOLD = 5; -const PLUS_CONTRIBUTION_ACCOUNT_CREDIT = 5; -const PLUS_CONTRIBUTION_DONATION_CREDIT = 20; const HOTMAIL_SERVICE_MODE_REMOTE = 'remote'; const HOTMAIL_SERVICE_MODE_LOCAL = 'local'; const ICLOUD_PROVIDER = 'icloud'; @@ -1794,192 +1788,10 @@ function setAutoRunFallbackRiskPromptDismissed(dismissed) { setPromptDismissed(AUTO_RUN_FALLBACK_RISK_PROMPT_DISMISSED_STORAGE_KEY, dismissed); } -function isAutoRunPlusRiskPromptDismissed() { - return isPromptDismissed(AUTO_RUN_PLUS_RISK_PROMPT_DISMISSED_STORAGE_KEY); -} - -function setAutoRunPlusRiskPromptDismissed(dismissed) { - setPromptDismissed(AUTO_RUN_PLUS_RISK_PROMPT_DISMISSED_STORAGE_KEY, dismissed); -} - function shouldWarnAutoRunFallbackRisk(totalRuns, autoRunSkipFailures) { return totalRuns >= AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS; } -function shouldWarnPlusAutoRunRisk(totalRuns, plusModeEnabled) { - return Boolean(plusModeEnabled) - && Math.floor(Number(totalRuns) || 0) > AUTO_RUN_PLUS_RISK_WARNING_MAX_SAFE_RUNS; -} - -function normalizePlusContributionPromptNumber(value) { - const number = Math.floor(Number(value) || 0); - return Number.isFinite(number) ? number : 0; -} - -function normalizePlusContributionPromptLedger(value = {}) { - const source = value && typeof value === 'object' ? value : {}; - return { - promptBaseline: normalizePlusContributionPromptNumber(source.promptBaseline), - donationCredit: Math.max(0, normalizePlusContributionPromptNumber(source.donationCredit)), - }; -} - -function getPlusContributionPromptLedger() { - try { - return normalizePlusContributionPromptLedger( - JSON.parse(localStorage.getItem(PLUS_CONTRIBUTION_PROMPT_LEDGER_STORAGE_KEY) || '{}') - ); - } catch { - return normalizePlusContributionPromptLedger(); - } -} - -function setPlusContributionPromptLedger(ledger) { - localStorage.setItem( - PLUS_CONTRIBUTION_PROMPT_LEDGER_STORAGE_KEY, - JSON.stringify(normalizePlusContributionPromptLedger(ledger)) - ); -} - -function isSuccessfulPlusAccountRecord(record = {}) { - return record?.finalStatus === 'success' && Boolean(record.plusModeEnabled); -} - -function getPlusContributionPromptTotals(records = []) { - return (Array.isArray(records) ? records : []).reduce((totals, record) => { - if (!isSuccessfulPlusAccountRecord(record)) { - return totals; - } - if (record.contributionMode) { - totals.contributionSuccess += 1; - } else { - totals.plusSuccess += 1; - } - return totals; - }, { - plusSuccess: 0, - contributionSuccess: 0, - }); -} - -function getPlusContributionPromptProgress(records = [], ledger = getPlusContributionPromptLedger()) { - const totals = getPlusContributionPromptTotals(records); - const normalizedLedger = normalizePlusContributionPromptLedger(ledger); - const credit = (totals.contributionSuccess * PLUS_CONTRIBUTION_ACCOUNT_CREDIT) - + normalizedLedger.donationCredit; - const netCount = totals.plusSuccess - credit; - const sinceLastPrompt = netCount - normalizedLedger.promptBaseline; - return { - ...totals, - credit, - netCount, - sinceLastPrompt, - shouldPrompt: sinceLastPrompt >= PLUS_CONTRIBUTION_PROMPT_THRESHOLD, - }; -} - -function shouldShowPlusContributionPrompt(records = [], plusModeEnabled = false, ledger = getPlusContributionPromptLedger()) { - return Boolean(plusModeEnabled) - && getPlusContributionPromptProgress(records, ledger).shouldPrompt; -} - -function markPlusContributionPromptShown(records = [], ledger = getPlusContributionPromptLedger()) { - const progress = getPlusContributionPromptProgress(records, ledger); - const nextLedger = { - ...normalizePlusContributionPromptLedger(ledger), - promptBaseline: progress.netCount, - }; - setPlusContributionPromptLedger(nextLedger); - return nextLedger; -} - -function addPlusContributionPromptCredit(credit, ledger = getPlusContributionPromptLedger()) { - const normalizedLedger = normalizePlusContributionPromptLedger(ledger); - const nextLedger = { - ...normalizedLedger, - donationCredit: normalizedLedger.donationCredit + Math.max(0, normalizePlusContributionPromptNumber(credit)), - }; - setPlusContributionPromptLedger(nextLedger); - return nextLedger; -} - -function getPlusContributionSupportImageUrl() { - if (typeof chrome !== 'undefined' && chrome.runtime?.getURL) { - return chrome.runtime.getURL('docs/images/微信.png'); - } - return '../docs/images/微信.png'; -} - -function buildPlusContributionSupportPromptHtml() { - const imageUrl = getPlusContributionSupportImageUrl(); - return [ - '您觉得这个 Plus 功能怎么样?您的账户数量应该已经够个人使用啦。', - '可以打开贡献给作者贡献几个账号,以便于让作者开发更好的功能出来吗?或者打赏一下作者?', - `微信打赏二维码`, - ].join(''); -} - -function openPlusContributionSupportModal() { - return openActionModal({ - title: 'Plus 功能使用反馈', - messageHtml: buildPlusContributionSupportPromptHtml(), - actions: [ - { id: null, label: '取消', variant: 'btn-ghost' }, - { id: 'contribute', label: '去贡献账号', variant: 'btn-outline' }, - { id: 'donated', label: '已打赏', variant: 'btn-primary' }, - ], - }); -} - -async function enterContributionModeFromPlusPrompt() { - if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) { - return null; - } - - const response = await chrome.runtime.sendMessage({ - type: 'SET_CONTRIBUTION_MODE', - source: 'sidepanel', - payload: { enabled: true }, - }); - - if (response?.error) { - throw new Error(response.error); - } - if (response?.state && typeof applySettingsState === 'function') { - applySettingsState(response.state); - } - if (typeof renderContributionMode === 'function') { - renderContributionMode(); - } - return response?.state || null; -} - -async function maybeShowPlusContributionPromptBeforeAutoRun(plusModeEnabled) { - const records = Array.isArray(latestState?.accountRunHistory) ? latestState.accountRunHistory : []; - if (!shouldShowPlusContributionPrompt(records, plusModeEnabled)) { - return true; - } - - const choice = await openPlusContributionSupportModal(); - const ledger = markPlusContributionPromptShown(records); - if (choice === 'donated') { - addPlusContributionPromptCredit(PLUS_CONTRIBUTION_DONATION_CREDIT, ledger); - showToast('感谢打赏支持,已延后下一次 Plus 提醒。', 'success', 2200); - return true; - } - if (choice === 'contribute') { - openExternalUrl(getContributionPortalUrl()); - try { - await enterContributionModeFromPlusPrompt(); - showToast('已进入贡献模式,并打开贡献页面。', 'info', 2200); - } catch (error) { - showToast(`贡献模式开启失败:${error.message}`, 'error', 2600); - } - return false; - } - return true; -} - async function openAutoSkipFailuresConfirmModal() { const result = await openConfirmModalWithOption({ title: '自动重试说明', @@ -2006,19 +1818,6 @@ async function openAutoRunFallbackRiskConfirmModal(totalRuns) { }; } -async function openPlusAutoRunRiskConfirmModal(totalRuns) { - const result = await openConfirmModalWithOption({ - title: 'Plus 自动轮数提醒', - message: `Plus 模式下当前设置为 ${totalRuns} 轮。轮数过多可能造成 PayPal 或账号快速封号。建议够用就好:我注册了几个使用,没多注册,完全足够使用,并且没有封号。这个模式下只要可以注册成功就能使用,所以不要贪杯哦。`, - confirmLabel: '我知道了,继续', - }); - - return { - confirmed: result.confirmed, - dismissPrompt: result.optionChecked, - }; -} - function updateConfigMenuControls() { const disabled = configActionInFlight || settingsSaveInFlight; const contributionModeEnabled = Boolean(latestState?.contributionMode); @@ -10877,9 +10676,6 @@ async function startAutoRunFromCurrentSettings() { if (lockedRunCount > 0) { inputRunCount.value = String(lockedRunCount); } - const plusModeEnabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled - ? Boolean(inputPlusModeEnabled.checked) - : Boolean(currentPlusModeEnabled || latestState?.plusModeEnabled); let mode = 'restart'; const autoRunSkipFailures = inputAutoSkipFailures.checked; const contributionNickname = String(inputContributionNickname?.value || '').trim(); @@ -10900,12 +10696,6 @@ async function startAutoRunFromCurrentSettings() { mode = choice; } - const confirmedPlusContributionPrompt = await maybeShowPlusContributionPromptBeforeAutoRun(plusModeEnabled); - if (!confirmedPlusContributionPrompt) { - clearPendingAutoRunStartRunCount(); - return false; - } - if (shouldWarnAutoRunFallbackRisk(totalRuns, autoRunSkipFailures) && !isAutoRunFallbackRiskPromptDismissed()) { const result = await openAutoRunFallbackRiskConfirmModal(totalRuns); @@ -10918,18 +10708,6 @@ async function startAutoRunFromCurrentSettings() { } } - if (shouldWarnPlusAutoRunRisk(totalRuns, plusModeEnabled) - && !isAutoRunPlusRiskPromptDismissed()) { - const result = await openPlusAutoRunRiskConfirmModal(totalRuns); - if (!result.confirmed) { - clearPendingAutoRunStartRunCount(); - return false; - } - if (result.dismissPrompt) { - setAutoRunPlusRiskPromptDismissed(true); - } - } - btnAutoRun.disabled = true; inputRunCount.disabled = true; const delayEnabled = inputAutoDelayEnabled.checked; diff --git a/tests/sidepanel-auto-run-content-refresh.test.js b/tests/sidepanel-auto-run-content-refresh.test.js index 115be14..3a33047 100644 --- a/tests/sidepanel-auto-run-content-refresh.test.js +++ b/tests/sidepanel-auto-run-content-refresh.test.js @@ -51,11 +51,6 @@ function extractFunction(name) { function createApi({ refreshImpl, runCount = 3, - plusModeEnabled = false, - plusRiskEnabled = false, - plusRiskConfirmed = true, - plusRiskDismissPrompt = false, - plusContributionImpl, persistImpl, } = {}) { const bundle = [ @@ -68,8 +63,6 @@ function createApi({ return new Function(` const events = []; const latestState = { contributionMode: false }; -const currentPlusModeEnabled = ${JSON.stringify(Boolean(plusModeEnabled))}; -const inputPlusModeEnabled = { checked: ${JSON.stringify(Boolean(plusModeEnabled))} }; const inputAutoSkipFailures = { checked: false }; const inputContributionNickname = { value: 'tester' }; const inputContributionQq = { value: '123456' }; @@ -115,23 +108,6 @@ function shouldWarnAutoRunFallbackRisk() { return false; } function isAutoRunFallbackRiskPromptDismissed() { return false; } async function openAutoRunFallbackRiskConfirmModal() { throw new Error('should not be called'); } function setAutoRunFallbackRiskPromptDismissed() {} -function shouldWarnPlusAutoRunRisk(totalRuns, plusModeEnabled) { - return ${JSON.stringify(Boolean(plusRiskEnabled))} && Boolean(plusModeEnabled) && Number(totalRuns) > 3; -} -function isAutoRunPlusRiskPromptDismissed() { return false; } -async function openPlusAutoRunRiskConfirmModal(totalRuns) { - events.push({ type: 'plus-risk-modal', totalRuns }); - return { - confirmed: ${JSON.stringify(Boolean(plusRiskConfirmed))}, - dismissPrompt: ${JSON.stringify(Boolean(plusRiskDismissPrompt))}, - }; -} -function setAutoRunPlusRiskPromptDismissed(dismissed) { - events.push({ type: 'plus-risk-dismiss', dismissed }); -} -async function maybeShowPlusContributionPromptBeforeAutoRun(plusModeEnabled) { - ${plusContributionImpl ? 'return (' + plusContributionImpl + ')(plusModeEnabled, events);' : 'return true;'} -} function normalizeAutoDelayMinutes(value) { return Number(value) || 30; } async function refreshContributionContentHint() { events.push({ type: 'refresh' }); @@ -214,61 +190,6 @@ test('startAutoRunFromCurrentSettings freezes run count before async settings sy 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, - plusModeEnabled: true, - plusRiskEnabled: true, - }); - - const result = await api.startAutoRunFromCurrentSettings(); - const events = api.getEvents(); - - assert.equal(result, true); - assert.deepEqual( - events.map((entry) => entry.type), - ['refresh', 'sync-settings', 'plus-risk-modal', 'send'] - ); - assert.equal(events[2].totalRuns, 4); - assert.equal(events[3].message.payload.totalRuns, 4); -}); - -test('startAutoRunFromCurrentSettings aborts when Plus risk warning is declined', async () => { - const api = createApi({ - runCount: 4, - plusModeEnabled: true, - plusRiskEnabled: true, - plusRiskConfirmed: false, - }); - - const result = await api.startAutoRunFromCurrentSettings(); - - assert.equal(result, false); - assert.deepEqual( - api.getEvents().map((entry) => entry.type), - ['refresh', 'sync-settings', 'plus-risk-modal'] - ); -}); - -test('startAutoRunFromCurrentSettings aborts when Plus contribution prompt opens contribution page', async () => { - const api = createApi({ - plusModeEnabled: true, - plusContributionImpl: `async (plusModeEnabled, events) => { - events.push({ type: 'plus-contribution-modal', plusModeEnabled }); - return false; - }`, - }); - - const result = await api.startAutoRunFromCurrentSettings(); - - assert.equal(result, false); - assert.deepEqual( - api.getEvents().map((entry) => entry.type), - ['refresh', 'sync-settings', 'plus-contribution-modal'] - ); - 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'), diff --git a/tests/sidepanel-auto-run-risk-warning.test.js b/tests/sidepanel-auto-run-risk-warning.test.js index aa059d3..5a7ba4a 100644 --- a/tests/sidepanel-auto-run-risk-warning.test.js +++ b/tests/sidepanel-auto-run-risk-warning.test.js @@ -65,48 +65,6 @@ return { shouldWarnAutoRunFallbackRisk }; assert.equal(api.shouldWarnAutoRunFallbackRisk(10, true), true); }); -test('Plus auto-run risk warning only starts above 3 runs in Plus mode', () => { - const bundle = extractFunction('shouldWarnPlusAutoRunRisk'); - - const api = new Function(` -const AUTO_RUN_PLUS_RISK_WARNING_MAX_SAFE_RUNS = 3; -${bundle} -return { shouldWarnPlusAutoRunRisk }; -`)(); - - assert.equal(api.shouldWarnPlusAutoRunRisk(3, true), false); - assert.equal(api.shouldWarnPlusAutoRunRisk(4, true), true); - assert.equal(api.shouldWarnPlusAutoRunRisk(10, false), false); -}); - -test('Plus auto-run risk modal uses PayPal account warning copy', async () => { - const bundle = extractFunction('openPlusAutoRunRiskConfirmModal'); - - const api = new Function(` -let capturedOptions = null; -async function openConfirmModalWithOption(options) { - capturedOptions = options; - return { confirmed: true, optionChecked: true }; -} -${bundle} -return { - openPlusAutoRunRiskConfirmModal, - getCapturedOptions() { - return capturedOptions; - }, -}; -`)(); - - const result = await api.openPlusAutoRunRiskConfirmModal(4); - const options = api.getCapturedOptions(); - - assert.deepStrictEqual(result, { confirmed: true, dismissPrompt: true }); - assert.equal(options.title, 'Plus 自动轮数提醒'); - assert.match(options.message, /轮数过多可能造成 PayPal 或账号快速封号/); - assert.match(options.message, /不要贪杯哦/); - assert.equal(options.confirmLabel, '我知道了,继续'); -}); - test('auto-run fallback risk modal uses the single-node warning copy', async () => { const bundle = extractFunction('openAutoRunFallbackRiskConfirmModal'); diff --git a/tests/sidepanel-plus-contribution-prompt.test.js b/tests/sidepanel-plus-contribution-prompt.test.js deleted file mode 100644 index b811d6e..0000000 --- a/tests/sidepanel-plus-contribution-prompt.test.js +++ /dev/null @@ -1,312 +0,0 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); -const fs = require('node:fs'); - -const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8'); - -function extractFunction(name) { - const markers = [`async function ${name}(`, `function ${name}(`]; - const start = markers - .map((marker) => source.indexOf(marker)) - .find((index) => index >= 0); - if (start < 0) { - throw new Error(`missing function ${name}`); - } - - let parenDepth = 0; - let signatureEnded = false; - let braceStart = -1; - for (let i = start; i < source.length; i += 1) { - const ch = source[i]; - if (ch === '(') { - parenDepth += 1; - } else if (ch === ')') { - parenDepth -= 1; - if (parenDepth === 0) { - signatureEnded = true; - } - } else if (ch === '{' && signatureEnded) { - braceStart = i; - break; - } - } - if (braceStart < 0) { - throw new Error(`missing body for function ${name}`); - } - - let depth = 0; - let end = braceStart; - for (; end < source.length; end += 1) { - const ch = source[end]; - if (ch === '{') depth += 1; - if (ch === '}') { - depth -= 1; - if (depth === 0) { - end += 1; - break; - } - } - } - - return source.slice(start, end); -} - -function extractBundle(names) { - return names.map((name) => extractFunction(name)).join('\n'); -} - -function createPlusRecords(count, contributionMode = false) { - return Array.from({ length: count }, (_, index) => ({ - recordId: `${contributionMode ? 'contribution' : 'plus'}-${index}@example.com`, - email: `${contributionMode ? 'contribution' : 'plus'}-${index}@example.com`, - finalStatus: 'success', - plusModeEnabled: true, - contributionMode, - })); -} - -test('Plus contribution prompt starts every five normal Plus successes', () => { - const bundle = extractBundle([ - 'normalizePlusContributionPromptNumber', - 'normalizePlusContributionPromptLedger', - 'isSuccessfulPlusAccountRecord', - 'getPlusContributionPromptTotals', - 'getPlusContributionPromptProgress', - 'shouldShowPlusContributionPrompt', - ]); - - const api = new Function(` -const PLUS_CONTRIBUTION_PROMPT_THRESHOLD = 5; -const PLUS_CONTRIBUTION_ACCOUNT_CREDIT = 5; -${bundle} -return { - getPlusContributionPromptProgress, - shouldShowPlusContributionPrompt, -}; -`)(); - - assert.equal( - api.shouldShowPlusContributionPrompt(createPlusRecords(4), true, { promptBaseline: 0, donationCredit: 0 }), - false - ); - assert.equal( - api.shouldShowPlusContributionPrompt(createPlusRecords(5), true, { promptBaseline: 0, donationCredit: 0 }), - true - ); - assert.equal( - api.shouldShowPlusContributionPrompt(createPlusRecords(8), false, { promptBaseline: 0, donationCredit: 0 }), - false - ); -}); - -test('Plus contribution success and donated credit delay the next prompt', () => { - const bundle = extractBundle([ - 'normalizePlusContributionPromptNumber', - 'normalizePlusContributionPromptLedger', - 'isSuccessfulPlusAccountRecord', - 'getPlusContributionPromptTotals', - 'getPlusContributionPromptProgress', - 'shouldShowPlusContributionPrompt', - ]); - - const api = new Function(` -const PLUS_CONTRIBUTION_PROMPT_THRESHOLD = 5; -const PLUS_CONTRIBUTION_ACCOUNT_CREDIT = 5; -${bundle} -return { shouldShowPlusContributionPrompt }; -`)(); - - const afterPromptLedger = { promptBaseline: 5, donationCredit: 0 }; - assert.equal( - api.shouldShowPlusContributionPrompt( - [...createPlusRecords(14), ...createPlusRecords(1, true)], - true, - afterPromptLedger - ), - false - ); - assert.equal( - api.shouldShowPlusContributionPrompt( - [...createPlusRecords(15), ...createPlusRecords(1, true)], - true, - afterPromptLedger - ), - true - ); - - const donatedLedger = { promptBaseline: 5, donationCredit: 20 }; - assert.equal(api.shouldShowPlusContributionPrompt(createPlusRecords(29), true, donatedLedger), false); - assert.equal(api.shouldShowPlusContributionPrompt(createPlusRecords(30), true, donatedLedger), true); -}); - -test('Plus contribution support modal includes WeChat image and expected actions', async () => { - const bundle = extractBundle([ - 'getPlusContributionSupportImageUrl', - 'buildPlusContributionSupportPromptHtml', - 'openPlusContributionSupportModal', - ]); - - const api = new Function(` -let capturedOptions = null; -const chrome = { runtime: { getURL: (path) => 'chrome-extension://test/' + path } }; -function escapeHtml(value) { return String(value || ''); } -async function openActionModal(options) { - capturedOptions = options; - return 'donated'; -} -${bundle} -return { - openPlusContributionSupportModal, - getCapturedOptions() { - return capturedOptions; - }, -}; -`)(); - - const choice = await api.openPlusContributionSupportModal(); - const options = api.getCapturedOptions(); - - assert.equal(choice, 'donated'); - assert.equal(options.title, 'Plus 功能使用反馈'); - assert.match(options.messageHtml, /docs\/images\/微信\.png/); - assert.deepEqual(options.actions.map((action) => action.label), ['取消', '去贡献账号', '已打赏']); -}); - -test('Plus contribution prompt marks shown and donated choice adds twenty credits', async () => { - const bundle = extractBundle([ - 'normalizePlusContributionPromptNumber', - 'normalizePlusContributionPromptLedger', - 'getPlusContributionPromptLedger', - 'setPlusContributionPromptLedger', - 'isSuccessfulPlusAccountRecord', - 'getPlusContributionPromptTotals', - 'getPlusContributionPromptProgress', - 'shouldShowPlusContributionPrompt', - 'markPlusContributionPromptShown', - 'addPlusContributionPromptCredit', - 'enterContributionModeFromPlusPrompt', - 'maybeShowPlusContributionPromptBeforeAutoRun', - ]); - - const api = new Function('records', ` -const PLUS_CONTRIBUTION_PROMPT_LEDGER_STORAGE_KEY = 'multipage-plus-contribution-prompt-ledger'; -const PLUS_CONTRIBUTION_PROMPT_THRESHOLD = 5; -const PLUS_CONTRIBUTION_ACCOUNT_CREDIT = 5; -const PLUS_CONTRIBUTION_DONATION_CREDIT = 20; -const latestState = { accountRunHistory: records }; -const storage = {}; -const events = []; -const localStorage = { - getItem(key) { return Object.prototype.hasOwnProperty.call(storage, key) ? storage[key] : null; }, - setItem(key, value) { storage[key] = String(value); }, -}; -async function openPlusContributionSupportModal() { - events.push({ type: 'modal' }); - return 'donated'; -} -function showToast(message, type) { - events.push({ type: 'toast', message, toastType: type }); -} -function openExternalUrl(url) { - events.push({ type: 'open', url }); -} -function getContributionPortalUrl() { - return 'https://apikey.qzz.io'; -} -${bundle} -return { - maybeShowPlusContributionPromptBeforeAutoRun, - getLedger() { - return JSON.parse(storage[PLUS_CONTRIBUTION_PROMPT_LEDGER_STORAGE_KEY] || '{}'); - }, - getEvents() { - return events; - }, -}; -`)(createPlusRecords(5)); - - const result = await api.maybeShowPlusContributionPromptBeforeAutoRun(true); - - assert.equal(result, true); - assert.deepEqual(api.getEvents().map((event) => event.type), ['modal', 'toast']); - assert.deepEqual(api.getLedger(), { - promptBaseline: 5, - donationCredit: 20, - }); -}); - -test('Plus contribution prompt opens portal and aborts normal auto run when contribute is chosen', async () => { - const bundle = extractBundle([ - 'normalizePlusContributionPromptNumber', - 'normalizePlusContributionPromptLedger', - 'getPlusContributionPromptLedger', - 'setPlusContributionPromptLedger', - 'isSuccessfulPlusAccountRecord', - 'getPlusContributionPromptTotals', - 'getPlusContributionPromptProgress', - 'shouldShowPlusContributionPrompt', - 'markPlusContributionPromptShown', - 'addPlusContributionPromptCredit', - 'enterContributionModeFromPlusPrompt', - 'maybeShowPlusContributionPromptBeforeAutoRun', - ]); - - const api = new Function('records', ` -const PLUS_CONTRIBUTION_PROMPT_LEDGER_STORAGE_KEY = 'multipage-plus-contribution-prompt-ledger'; -const PLUS_CONTRIBUTION_PROMPT_THRESHOLD = 5; -const PLUS_CONTRIBUTION_ACCOUNT_CREDIT = 5; -const PLUS_CONTRIBUTION_DONATION_CREDIT = 20; -const latestState = { accountRunHistory: records }; -const storage = {}; -const events = []; -const localStorage = { - getItem(key) { return Object.prototype.hasOwnProperty.call(storage, key) ? storage[key] : null; }, - setItem(key, value) { storage[key] = String(value); }, -}; -async function openPlusContributionSupportModal() { - events.push({ type: 'modal' }); - return 'contribute'; -} -function showToast(message, type) { - events.push({ type: 'toast', message, toastType: type }); -} -function openExternalUrl(url) { - events.push({ type: 'open', url }); -} -function getContributionPortalUrl() { - return 'https://apikey.qzz.io'; -} -const chrome = { - runtime: { - async sendMessage(message) { - events.push({ type: 'runtime', message }); - return { state: { contributionMode: true } }; - }, - }, -}; -function applySettingsState(state) { - events.push({ type: 'apply', state }); -} -function renderContributionMode() { - events.push({ type: 'render' }); -} -${bundle} -return { - maybeShowPlusContributionPromptBeforeAutoRun, - getEvents() { - return events; - }, -}; -`)(createPlusRecords(5)); - - const result = await api.maybeShowPlusContributionPromptBeforeAutoRun(true); - - assert.equal(result, false); - assert.deepEqual(api.getEvents().map((event) => event.type), ['modal', 'open', 'runtime', 'apply', 'render', 'toast']); - assert.equal(api.getEvents()[1].url, 'https://apikey.qzz.io'); - assert.deepEqual(api.getEvents()[2].message, { - type: 'SET_CONTRIBUTION_MODE', - source: 'sidepanel', - payload: { enabled: true }, - }); -}); diff --git a/项目文件结构说明.md b/项目文件结构说明.md index e71e66e..1f3bed2 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -110,7 +110,6 @@ - `docs/ip-proxy-module.md`:IP Proxy 模块说明,记录模块定位、后台/侧栏代码结构、当前功能范围、711Proxy 参数联动、操作方式、发布策略与已知限制。 - `docs/使用教程.md`:面向使用者的补充教程文档,当前说明扩展更新流程与 Clash Verge 的“非港轮询”配置步骤。 - `docs/images/交流群.jpg`:README 中展示的官网 / QQ 交流群入口图片。 -- `docs/images/微信.png`:侧边栏 Plus 功能支持弹窗使用的微信打赏二维码图片。 - `docs/refactor/2026-04-16-architecture-refactor-plan.md`:本轮重构阶段的结构设计与迁移计划记录。 - `docs/superpowers/plans/2026-04-10-hotmail-oauth-mail-pool.md`:Hotmail OAuth 邮箱池实现计划文档。 - `docs/superpowers/specs/2026-04-10-hotmail-oauth-design.md`:Hotmail OAuth + Graph Mail 的设计规格文档。