Merge origin/master into master
This commit is contained in:
@@ -5411,6 +5411,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa
|
||||
normalizeMail2925Accounts,
|
||||
pickMail2925AccountForRun,
|
||||
requestStop,
|
||||
ensureContentScriptReadyOnTab,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScriptResilient,
|
||||
sendToMailContentScriptResilient,
|
||||
@@ -5419,6 +5420,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa
|
||||
sleepWithStop,
|
||||
throwIfStopped,
|
||||
upsertMail2925AccountInList,
|
||||
waitForTabUrlMatch,
|
||||
});
|
||||
|
||||
async function upsertMail2925Account(input = {}) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
pickMail2925AccountForRun,
|
||||
getState,
|
||||
isAutoRunLockedState,
|
||||
ensureContentScriptReadyOnTab,
|
||||
requestStop,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScriptResilient,
|
||||
@@ -24,11 +25,12 @@
|
||||
sleepWithStop,
|
||||
throwIfStopped,
|
||||
upsertMail2925AccountInList,
|
||||
waitForTabUrlMatch,
|
||||
} = deps;
|
||||
|
||||
const MAIL2925_SOURCE = 'mail-2925';
|
||||
const MAIL2925_URL = 'https://2925.com/#/mailList';
|
||||
const MAIL2925_LOGIN_URL = 'https://2925.com/';
|
||||
const MAIL2925_LOGIN_URL = 'https://2925.com/login/';
|
||||
const MAIL2925_INJECT = ['content/utils.js', 'content/mail-2925.js'];
|
||||
const MAIL2925_INJECT_SOURCE = 'mail-2925';
|
||||
const MAIL2925_COOKIE_DOMAINS = [
|
||||
@@ -387,7 +389,8 @@
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
await reuseOrCreateTab(MAIL2925_SOURCE, forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL, {
|
||||
const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL;
|
||||
const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, {
|
||||
inject: MAIL2925_INJECT,
|
||||
injectSource: MAIL2925_INJECT_SOURCE,
|
||||
});
|
||||
@@ -497,13 +500,40 @@
|
||||
|
||||
throwIfStopped();
|
||||
await addLog(`2925:准备打开登录页 ${MAIL2925_LOGIN_URL}(forceRelogin=${forceRelogin ? 'true' : 'false'})`, 'info');
|
||||
await reuseOrCreateTab(MAIL2925_SOURCE, forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL, {
|
||||
const targetUrl = forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL;
|
||||
const tabId = await reuseOrCreateTab(MAIL2925_SOURCE, targetUrl, {
|
||||
inject: MAIL2925_INJECT,
|
||||
injectSource: MAIL2925_INJECT_SOURCE,
|
||||
});
|
||||
const openedUrl = await getMail2925CurrentTabUrl();
|
||||
await addLog(`2925:打开页后当前标签地址:${openedUrl || 'unknown'}`, 'info');
|
||||
|
||||
if (forceRelogin && typeof waitForTabUrlMatch === 'function') {
|
||||
const matchedLoginTab = await waitForTabUrlMatch(
|
||||
tabId,
|
||||
(url) => {
|
||||
try {
|
||||
const parsed = new URL(String(url || ''));
|
||||
return (parsed.hostname === '2925.com' || parsed.hostname === 'www.2925.com')
|
||||
&& /^\/login\/?$/.test(parsed.pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ timeoutMs: 15000, retryDelayMs: 300 }
|
||||
);
|
||||
await addLog(`2925:等待最终落到登录页结果:${matchedLoginTab?.url || 'timeout'}`, matchedLoginTab ? 'info' : 'warn');
|
||||
if (matchedLoginTab && typeof ensureContentScriptReadyOnTab === 'function') {
|
||||
await ensureContentScriptReadyOnTab(MAIL2925_SOURCE, tabId, {
|
||||
inject: MAIL2925_INJECT,
|
||||
injectSource: MAIL2925_INJECT_SOURCE,
|
||||
timeoutMs: 20000,
|
||||
retryDelayMs: 800,
|
||||
logMessage: '步骤 0:2925 登录页内容脚本未就绪,正在等待页面稳定后继续登录...',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (forceRelogin && typeof sleepWithStop === 'function') {
|
||||
await addLog('2925:登录页已打开,等待 3 秒后开始检查输入框并执行登录...', 'info');
|
||||
await sleepWithStop(3000);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
(function attachBackgroundStep8(root, factory) {
|
||||
root.MultiPageBackgroundStep8 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() {
|
||||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
||||
|
||||
function createStep8Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
@@ -61,6 +63,9 @@
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
|
||||
const stepStartedAt = Date.now();
|
||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: stepStartedAt;
|
||||
const verificationSessionKey = `8:${stepStartedAt}`;
|
||||
const authTabId = await getTabId('signup-page');
|
||||
|
||||
@@ -140,7 +145,7 @@
|
||||
...state,
|
||||
step8VerificationTargetEmail: displayedVerificationEmail || '',
|
||||
}, mail, {
|
||||
filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt,
|
||||
filterAfterTimestamp: verificationFilterAfterTimestamp,
|
||||
sessionKey: verificationSessionKey,
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || ''),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
(function attachBackgroundStep4(root, factory) {
|
||||
root.MultiPageBackgroundStep4 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep4Module() {
|
||||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
||||
|
||||
function createStep4Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
@@ -26,6 +28,9 @@
|
||||
const mail = getMailConfig(state);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
const stepStartedAt = Date.now();
|
||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: stepStartedAt;
|
||||
const verificationSessionKey = `4:${stepStartedAt}`;
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
if (!signupTabId) {
|
||||
@@ -101,7 +106,7 @@
|
||||
}
|
||||
|
||||
await resolveVerificationStep(4, state, mail, {
|
||||
filterAfterTimestamp: mail.provider === '2925' ? 0 : stepStartedAt,
|
||||
filterAfterTimestamp: verificationFilterAfterTimestamp,
|
||||
sessionKey: verificationSessionKey,
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
|
||||
|
||||
@@ -239,12 +239,16 @@
|
||||
return requestedAt;
|
||||
}
|
||||
|
||||
function shouldPreclear2925Mailbox(step, mail) {
|
||||
return mail?.provider === '2925' && (step === 4 || step === 8);
|
||||
function shouldPreclear2925Mailbox(step, mail, options = {}) {
|
||||
if (mail?.provider !== '2925' || (step !== 4 && step !== 8)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(Number(options.filterAfterTimestamp) > 0);
|
||||
}
|
||||
|
||||
async function clear2925MailboxBeforePolling(step, mail, options = {}) {
|
||||
if (!shouldPreclear2925Mailbox(step, mail)) {
|
||||
if (!shouldPreclear2925Mailbox(step, mail, options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -629,6 +629,13 @@ function extractVerificationCode(text, strictChatGPTCodeOnly = false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeMinuteTimestamp(timestamp) {
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) return 0;
|
||||
const date = new Date(timestamp);
|
||||
date.setSeconds(0, 0);
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
function parseMailItemTimestamp(item) {
|
||||
const timeText = getMailItemTimeText(item);
|
||||
if (!timeText) return null;
|
||||
@@ -920,10 +927,12 @@ async function handlePollEmail(step, payload) {
|
||||
subjectFilters,
|
||||
maxAttempts,
|
||||
intervalMs,
|
||||
filterAfterTimestamp = 0,
|
||||
excludeCodes = [],
|
||||
strictChatGPTCodeOnly = false,
|
||||
} = payload || {};
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
|
||||
if (typeof throwIfMail2925LimitReached === 'function') {
|
||||
throwIfMail2925LimitReached();
|
||||
}
|
||||
@@ -975,6 +984,11 @@ async function handlePollEmail(step, payload) {
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
const item = items[index];
|
||||
const itemTimestamp = parseMailItemTimestamp(item);
|
||||
const itemMinute = normalizeMinuteTimestamp(itemTimestamp || 0);
|
||||
|
||||
if (filterAfterMinute && (!itemMinute || itemMinute < filterAfterMinute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previewText = getMailItemText(item);
|
||||
if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) {
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "多页面自动化",
|
||||
"version": "4.9",
|
||||
"version_name": "Pro4.9",
|
||||
"version": "5.0",
|
||||
"version_name": "Pro5.0",
|
||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||
"permissions": [
|
||||
"sidePanel",
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
(() => {
|
||||
const PORTAL_BASE_URL = 'https://apikey.qzz.io';
|
||||
const CONTENT_SUMMARY_API_URL = `${PORTAL_BASE_URL}/api/content-summary`;
|
||||
const CACHE_KEY = 'multipage-contribution-content-summary-v1';
|
||||
const FETCH_TIMEOUT_MS = 6000;
|
||||
|
||||
function sanitizeItem(item = {}) {
|
||||
return {
|
||||
slug: String(item?.slug || '').trim(),
|
||||
title: String(item?.title || '').trim(),
|
||||
isEnabled: Boolean(item?.is_enabled),
|
||||
hasContent: Boolean(item?.has_content),
|
||||
isVisible: Boolean(item?.is_visible),
|
||||
updatedAt: String(item?.updated_at || '').trim(),
|
||||
updatedAtDisplay: String(item?.updated_at_display || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSnapshot(payload = {}) {
|
||||
const items = Array.isArray(payload?.items)
|
||||
? payload.items.map(sanitizeItem).filter((item) => item.slug)
|
||||
: [];
|
||||
const promptVersion = String(payload?.prompt_version || '').trim();
|
||||
const latestUpdatedAt = String(payload?.latest_updated_at || '').trim();
|
||||
const latestUpdatedAtDisplay = String(payload?.latest_updated_at_display || '').trim();
|
||||
const hasVisibleUpdates = Boolean(payload?.has_visible_updates) && Boolean(promptVersion);
|
||||
|
||||
return {
|
||||
status: hasVisibleUpdates ? 'update-available' : 'idle',
|
||||
promptVersion,
|
||||
hasVisibleUpdates,
|
||||
latestUpdatedAt,
|
||||
latestUpdatedAtDisplay,
|
||||
items,
|
||||
portalUrl: PORTAL_BASE_URL,
|
||||
apiUrl: CONTENT_SUMMARY_API_URL,
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function readCache() {
|
||||
try {
|
||||
const raw = localStorage.getItem(CACHE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const snapshot = buildSnapshot({
|
||||
items: parsed.items,
|
||||
prompt_version: parsed.promptVersion,
|
||||
has_visible_updates: parsed.hasVisibleUpdates,
|
||||
latest_updated_at: parsed.latestUpdatedAt,
|
||||
latest_updated_at_display: parsed.latestUpdatedAtDisplay,
|
||||
});
|
||||
if (!Number.isFinite(parsed.checkedAt)) {
|
||||
return snapshot;
|
||||
}
|
||||
snapshot.checkedAt = parsed.checkedAt;
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(snapshot) {
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(snapshot));
|
||||
} catch (error) {
|
||||
// Ignore cache write failures.
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContentSummary() {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(CONTENT_SUMMARY_API_URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`内容摘要请求失败:${response.status}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (!payload || payload.ok !== true) {
|
||||
throw new Error('内容摘要返回格式异常');
|
||||
}
|
||||
|
||||
const snapshot = buildSnapshot(payload);
|
||||
writeCache(snapshot);
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('内容摘要请求超时');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
async function getContentUpdateSnapshot() {
|
||||
try {
|
||||
return await fetchContentSummary();
|
||||
} catch (error) {
|
||||
const cached = readCache();
|
||||
if (cached) {
|
||||
return {
|
||||
...cached,
|
||||
fromCache: true,
|
||||
errorMessage: error?.message || '内容摘要获取失败',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
promptVersion: '',
|
||||
hasVisibleUpdates: false,
|
||||
latestUpdatedAt: '',
|
||||
latestUpdatedAtDisplay: '',
|
||||
items: [],
|
||||
portalUrl: PORTAL_BASE_URL,
|
||||
apiUrl: CONTENT_SUMMARY_API_URL,
|
||||
checkedAt: Date.now(),
|
||||
errorMessage: error?.message || '内容摘要获取失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.SidepanelContributionContentService = {
|
||||
getContentUpdateSnapshot,
|
||||
portalUrl: PORTAL_BASE_URL,
|
||||
apiUrl: CONTENT_SUMMARY_API_URL,
|
||||
};
|
||||
})();
|
||||
+71
-1
@@ -243,6 +243,76 @@ header {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.contribution-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contribution-update-hint {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
max-width: 220px;
|
||||
margin-left: 6px;
|
||||
padding: 8px 10px;
|
||||
background: color-mix(in srgb, var(--amber) 10%, var(--bg-base));
|
||||
border: 1px solid color-mix(in srgb, var(--amber) 34%, var(--border));
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.contribution-update-hint::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 14px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: inherit;
|
||||
border-top: inherit;
|
||||
border-left: inherit;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.contribution-update-hint-text {
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.contribution-update-hint-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background var(--transition), color var(--transition);
|
||||
}
|
||||
|
||||
.contribution-update-hint-close:hover {
|
||||
background: color-mix(in srgb, var(--amber) 16%, transparent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.contribution-update-hint-close:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--amber) 34%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Theme Toggle
|
||||
============================================================ */
|
||||
@@ -366,7 +436,7 @@ header {
|
||||
|
||||
.run-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -34,8 +34,17 @@
|
||||
</div>
|
||||
<div class="header-btns">
|
||||
<div class="run-group">
|
||||
<button id="btn-contribution-mode" class="btn btn-outline btn-sm btn-contribution-mode" type="button"
|
||||
aria-pressed="false" title="进入贡献模式并打开上传页">贡献/使用</button>
|
||||
<div class="contribution-entry">
|
||||
<button id="btn-contribution-mode" class="btn btn-outline btn-sm btn-contribution-mode" type="button"
|
||||
aria-pressed="false" title="进入贡献模式并打开上传页">贡献/使用</button>
|
||||
<div id="contribution-update-hint" class="contribution-update-hint" aria-live="polite" hidden>
|
||||
<p id="contribution-update-hint-text" class="contribution-update-hint-text">
|
||||
公告 / 使用教程有更新了,可点上方“贡献/使用”查看。
|
||||
</p>
|
||||
<button id="btn-dismiss-contribution-update-hint" class="contribution-update-hint-close" type="button"
|
||||
aria-label="关闭更新提示" title="关闭更新提示">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="运行次数" />
|
||||
<button id="btn-auto-run" class="btn btn-success" title="自动执行全部步骤">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
@@ -724,6 +733,7 @@
|
||||
<script src="../luckmail-utils.js"></script>
|
||||
<script src="../data/step-definitions.js"></script>
|
||||
<script src="update-service.js"></script>
|
||||
<script src="contribution-content-update-service.js"></script>
|
||||
<script src="hotmail-manager.js"></script>
|
||||
<script src="mail-2925-manager.js"></script>
|
||||
<script src="icloud-manager.js"></script>
|
||||
|
||||
+118
-54
@@ -57,6 +57,9 @@ const btnSaveSettings = document.getElementById('btn-save-settings');
|
||||
const btnStop = document.getElementById('btn-stop');
|
||||
const btnReset = document.getElementById('btn-reset');
|
||||
const btnContributionMode = document.getElementById('btn-contribution-mode');
|
||||
const contributionUpdateHint = document.getElementById('contribution-update-hint');
|
||||
const contributionUpdateHintText = document.getElementById('contribution-update-hint-text');
|
||||
const btnDismissContributionUpdateHint = document.getElementById('btn-dismiss-contribution-update-hint');
|
||||
const stepsProgress = document.getElementById('steps-progress');
|
||||
const btnAutoRun = document.getElementById('btn-auto-run');
|
||||
const btnAutoContinue = document.getElementById('btn-auto-continue');
|
||||
@@ -243,6 +246,7 @@ const MAIL_2925_MODE_RECEIVE = 'receive';
|
||||
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
|
||||
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 CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY = 'multipage-contribution-content-prompt-dismissed-version';
|
||||
const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 15;
|
||||
const AUTO_RUN_FALLBACK_RISK_RECOMMENDED_THREAD_INTERVAL_MINUTES = 5;
|
||||
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
|
||||
@@ -500,6 +504,8 @@ let scheduledCountdownTimer = null;
|
||||
let configMenuOpen = false;
|
||||
let configActionInFlight = false;
|
||||
let currentReleaseSnapshot = null;
|
||||
let currentContributionContentSnapshot = null;
|
||||
let contributionContentSnapshotRequestInFlight = 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>';
|
||||
@@ -517,6 +523,7 @@ const normalizeLuckmailTimestampValue = window.LuckMailUtils?.normalizeTimestamp
|
||||
return Number.isFinite(timestamp) ? timestamp : 0;
|
||||
});
|
||||
const sidepanelUpdateService = window.SidepanelUpdateService;
|
||||
const contributionContentService = window.SidepanelContributionContentService;
|
||||
const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = window.LuckMailUtils?.DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME || '保留';
|
||||
const normalizeIcloudHost = window.IcloudUtils?.normalizeIcloudHost
|
||||
|| ((value) => {
|
||||
@@ -827,6 +834,19 @@ function setPromptDismissed(storageKey, dismissed) {
|
||||
}
|
||||
}
|
||||
|
||||
function getDismissedContributionContentPromptVersion() {
|
||||
return String(localStorage.getItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY) || '').trim();
|
||||
}
|
||||
|
||||
function setDismissedContributionContentPromptVersion(version) {
|
||||
const normalized = String(version || '').trim();
|
||||
if (normalized) {
|
||||
localStorage.setItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY, normalized);
|
||||
} else {
|
||||
localStorage.removeItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
function isAutoSkipFailuresPromptDismissed() {
|
||||
return isPromptDismissed(AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY);
|
||||
}
|
||||
@@ -2289,6 +2309,81 @@ async function initializeReleaseInfo() {
|
||||
renderReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
function getContributionUpdateHintMessage(snapshot = currentContributionContentSnapshot) {
|
||||
if (!snapshot?.promptVersion) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '公告 / 使用教程有更新了,可点上方“贡献/使用”查看。';
|
||||
}
|
||||
|
||||
function shouldShowContributionUpdateHint(snapshot = currentContributionContentSnapshot) {
|
||||
const promptVersion = String(snapshot?.promptVersion || '').trim();
|
||||
if (!contributionUpdateHint || !contributionUpdateHintText || !btnContributionMode) {
|
||||
return false;
|
||||
}
|
||||
if (!promptVersion) {
|
||||
return false;
|
||||
}
|
||||
if (promptVersion === getDismissedContributionContentPromptVersion()) {
|
||||
return false;
|
||||
}
|
||||
if (latestState?.contributionMode) {
|
||||
return false;
|
||||
}
|
||||
return !btnContributionMode.disabled;
|
||||
}
|
||||
|
||||
function renderContributionUpdateHint(snapshot = currentContributionContentSnapshot) {
|
||||
if (!contributionUpdateHint) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visible = shouldShowContributionUpdateHint(snapshot);
|
||||
contributionUpdateHint.hidden = !visible;
|
||||
if (!visible || !contributionUpdateHintText) {
|
||||
return;
|
||||
}
|
||||
|
||||
contributionUpdateHintText.textContent = getContributionUpdateHintMessage(snapshot);
|
||||
}
|
||||
|
||||
function dismissContributionUpdateHint() {
|
||||
const promptVersion = String(currentContributionContentSnapshot?.promptVersion || '').trim();
|
||||
if (promptVersion) {
|
||||
setDismissedContributionContentPromptVersion(promptVersion);
|
||||
}
|
||||
renderContributionUpdateHint();
|
||||
}
|
||||
|
||||
async function refreshContributionContentHint() {
|
||||
if (!contributionContentService?.getContentUpdateSnapshot) {
|
||||
currentContributionContentSnapshot = null;
|
||||
renderContributionUpdateHint();
|
||||
return null;
|
||||
}
|
||||
if (contributionContentSnapshotRequestInFlight) {
|
||||
return contributionContentSnapshotRequestInFlight;
|
||||
}
|
||||
|
||||
contributionContentSnapshotRequestInFlight = contributionContentService.getContentUpdateSnapshot()
|
||||
.then((snapshot) => {
|
||||
currentContributionContentSnapshot = snapshot;
|
||||
renderContributionUpdateHint(snapshot);
|
||||
return snapshot;
|
||||
})
|
||||
.catch((error) => {
|
||||
currentContributionContentSnapshot = null;
|
||||
renderContributionUpdateHint(null);
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
contributionContentSnapshotRequestInFlight = null;
|
||||
});
|
||||
|
||||
return contributionContentSnapshotRequestInFlight;
|
||||
}
|
||||
|
||||
function syncPasswordField(state) {
|
||||
inputPassword.value = state?.contributionMode ? '' : (state.customPassword || state.password || '');
|
||||
}
|
||||
@@ -3367,12 +3462,16 @@ const contributionModeManager = window.SidepanelContributionMode?.createContribu
|
||||
sendMessage: (message) => chrome.runtime.sendMessage(message),
|
||||
},
|
||||
constants: {
|
||||
contributionOauthUrl: 'https://apikey.qzz.io/oauth/',
|
||||
contributionUploadUrl: 'https://apikey.qzz.io',
|
||||
contributionOauthUrl: `${contributionContentService?.portalUrl || 'https://apikey.qzz.io'}/oauth/`,
|
||||
contributionUploadUrl: contributionContentService?.portalUrl || 'https://apikey.qzz.io',
|
||||
},
|
||||
});
|
||||
const renderContributionMode = contributionModeManager?.render
|
||||
const baseRenderContributionMode = contributionModeManager?.render
|
||||
|| (() => { });
|
||||
const renderContributionMode = () => {
|
||||
baseRenderContributionMode();
|
||||
renderContributionUpdateHint();
|
||||
};
|
||||
const bindContributionModeEvents = contributionModeManager?.bindEvents
|
||||
|| (() => { });
|
||||
bindContributionModeEvents();
|
||||
@@ -3693,6 +3792,11 @@ extensionUpdateStatus?.addEventListener('click', () => {
|
||||
openReleaseListPage();
|
||||
});
|
||||
|
||||
btnDismissContributionUpdateHint?.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
dismissContributionUpdateHint();
|
||||
});
|
||||
|
||||
configMenu?.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
@@ -3728,6 +3832,12 @@ autoStartModal?.addEventListener('click', (event) => {
|
||||
btnAutoStartClose?.addEventListener('click', () => resolveModalChoice(null));
|
||||
|
||||
async function startAutoRunFromCurrentSettings() {
|
||||
try {
|
||||
await refreshContributionContentHint();
|
||||
} catch (error) {
|
||||
console.warn('Failed to refresh contribution content hint before auto run:', error);
|
||||
}
|
||||
|
||||
const totalRuns = getRunCountValue();
|
||||
let mode = 'restart';
|
||||
const autoRunSkipFailures = inputAutoSkipFailures.checked;
|
||||
@@ -3789,57 +3899,7 @@ async function startAutoRunFromCurrentSettings() {
|
||||
// Auto Run
|
||||
btnAutoRun.addEventListener('click', async () => {
|
||||
try {
|
||||
return await startAutoRunFromCurrentSettings();
|
||||
const totalRuns = getRunCountValue();
|
||||
let mode = 'restart';
|
||||
const autoRunSkipFailures = inputAutoSkipFailures.checked;
|
||||
const fallbackThreadIntervalMinutes = normalizeAutoRunThreadIntervalMinutes(
|
||||
inputAutoSkipFailuresThreadIntervalMinutes.value
|
||||
);
|
||||
inputAutoSkipFailuresThreadIntervalMinutes.value = String(fallbackThreadIntervalMinutes);
|
||||
|
||||
if (shouldOfferAutoModeChoice()) {
|
||||
const startStep = getFirstUnfinishedStep();
|
||||
const runningStep = getRunningSteps()[0] ?? null;
|
||||
const choice = await openAutoStartChoiceDialog(startStep, { runningStep });
|
||||
if (!choice) {
|
||||
return;
|
||||
}
|
||||
mode = choice;
|
||||
}
|
||||
|
||||
if (shouldWarnAutoRunFallbackRisk(totalRuns, autoRunSkipFailures)
|
||||
&& !isAutoRunFallbackRiskPromptDismissed()) {
|
||||
const result = await openAutoRunFallbackRiskConfirmModal(totalRuns, fallbackThreadIntervalMinutes);
|
||||
if (!result.confirmed) {
|
||||
return;
|
||||
}
|
||||
if (result.dismissPrompt) {
|
||||
setAutoRunFallbackRiskPromptDismissed(true);
|
||||
}
|
||||
}
|
||||
|
||||
btnAutoRun.disabled = true;
|
||||
inputRunCount.disabled = true;
|
||||
const delayEnabled = inputAutoDelayEnabled.checked;
|
||||
const delayMinutes = normalizeAutoDelayMinutes(inputAutoDelayMinutes.value);
|
||||
inputAutoDelayMinutes.value = String(delayMinutes);
|
||||
btnAutoRun.innerHTML = delayEnabled
|
||||
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 计划中...'
|
||||
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 运行中...';
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: delayEnabled ? 'SCHEDULE_AUTO_RUN' : 'AUTO_RUN',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
totalRuns,
|
||||
delayMinutes,
|
||||
autoRunSkipFailures,
|
||||
mode,
|
||||
},
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
await startAutoRunFromCurrentSettings();
|
||||
} catch (err) {
|
||||
setDefaultAutoRunButton();
|
||||
inputRunCount.disabled = false;
|
||||
@@ -4786,4 +4846,8 @@ loadHeroSmsCountries().catch((err) => {
|
||||
updatePanelModeUI();
|
||||
updateButtonStates();
|
||||
updateStatusDisplay(latestState);
|
||||
return refreshContributionContentHint();
|
||||
}).catch((err) => {
|
||||
console.error('Failed to initialize sidepanel state:', err);
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -3,6 +3,11 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const mail2925Utils = require('../mail2925-utils.js');
|
||||
|
||||
test('background mail2925 session uses /login/ as relogin entry url', () => {
|
||||
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
|
||||
assert.match(source, /const MAIL2925_LOGIN_URL = 'https:\/\/2925\.com\/login\/';/);
|
||||
});
|
||||
|
||||
test('ensureMail2925MailboxSession waits 3 seconds before and after opening login page on force relogin', async () => {
|
||||
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
|
||||
const globalScope = {};
|
||||
@@ -17,6 +22,8 @@ test('ensureMail2925MailboxSession waits 3 seconds before and after opening logi
|
||||
};
|
||||
const events = {
|
||||
sleeps: [],
|
||||
openedUrls: [],
|
||||
readyCalls: 0,
|
||||
};
|
||||
|
||||
const manager = api.createMail2925SessionManager({
|
||||
@@ -41,7 +48,13 @@ test('ensureMail2925MailboxSession waits 3 seconds before and after opening logi
|
||||
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
|
||||
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
|
||||
requestStop: async () => {},
|
||||
reuseOrCreateTab: async () => 1,
|
||||
ensureContentScriptReadyOnTab: async () => {
|
||||
events.readyCalls += 1;
|
||||
},
|
||||
reuseOrCreateTab: async (_source, url) => {
|
||||
events.openedUrls.push(url);
|
||||
return 1;
|
||||
},
|
||||
sendToMailContentScriptResilient: async () => ({ loggedIn: true }),
|
||||
setPersistentSettings: async (payload) => {
|
||||
currentState = { ...currentState, ...payload };
|
||||
@@ -54,6 +67,7 @@ test('ensureMail2925MailboxSession waits 3 seconds before and after opening logi
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
|
||||
waitForTabUrlMatch: async () => ({ url: 'https://2925.com/login/' }),
|
||||
});
|
||||
|
||||
await manager.ensureMail2925MailboxSession({
|
||||
@@ -62,5 +76,7 @@ test('ensureMail2925MailboxSession waits 3 seconds before and after opening logi
|
||||
actionLabel: '步骤 4:确认 2925 邮箱登录态',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(events.openedUrls, ['https://2925.com/login/']);
|
||||
assert.deepStrictEqual(events.sleeps, [3000, 3000]);
|
||||
assert.equal(events.readyCalls, 1);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background/steps/fetch-signup-code.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep4;`)(globalScope);
|
||||
|
||||
test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling', async () => {
|
||||
let capturedOptions = null;
|
||||
const realDateNow = Date.now;
|
||||
Date.now = () => 700000;
|
||||
|
||||
const executor = api.createStep4Executor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureMail2925MailboxSession: async () => {},
|
||||
getMailConfig: () => ({
|
||||
provider: '2925',
|
||||
label: '2925 邮箱',
|
||||
source: 'mail-2925',
|
||||
url: 'https://2925.com',
|
||||
}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isTabAlive: async () => true,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
resolveVerificationStep: async (_step, _state, _mail, options) => {
|
||||
capturedOptions = options;
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
try {
|
||||
await executor.executeStep4({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
mail2925UseAccountPool: false,
|
||||
});
|
||||
} finally {
|
||||
Date.now = realDateNow;
|
||||
}
|
||||
|
||||
assert.equal(capturedOptions.filterAfterTimestamp, 100000);
|
||||
assert.equal(capturedOptions.resendIntervalMs, 0);
|
||||
});
|
||||
@@ -89,8 +89,10 @@ test('step 8 submits login verification directly without replaying step 7', asyn
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 8 disables resend interval for 2925 mailbox polling', async () => {
|
||||
test('step 8 uses a fixed 10-minute lookback window and disables resend interval for 2925 mailbox polling', async () => {
|
||||
let capturedOptions = null;
|
||||
const realDateNow = Date.now;
|
||||
Date.now = () => 900000;
|
||||
|
||||
const executor = api.createStep8Executor({
|
||||
addLog: async () => {},
|
||||
@@ -130,12 +132,17 @@ test('step 8 disables resend interval for 2925 mailbox polling', async () => {
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep8({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
try {
|
||||
await executor.executeStep8({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
} finally {
|
||||
Date.now = realDateNow;
|
||||
}
|
||||
|
||||
assert.equal(capturedOptions.filterAfterTimestamp, 300000);
|
||||
assert.equal(capturedOptions.resendIntervalMs, 0);
|
||||
assert.equal(capturedOptions.targetEmail, '');
|
||||
assert.equal(capturedOptions.beforeSubmit, undefined);
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('sidepanel/contribution-content-update-service.js', 'utf8');
|
||||
|
||||
function createContributionContentService(options = {}) {
|
||||
const cache = new Map();
|
||||
const windowObject = {};
|
||||
let fetchCalls = 0;
|
||||
|
||||
const localStorage = {
|
||||
getItem(key) {
|
||||
return cache.has(key) ? cache.get(key) : null;
|
||||
},
|
||||
setItem(key, value) {
|
||||
cache.set(key, String(value));
|
||||
},
|
||||
removeItem(key) {
|
||||
cache.delete(key);
|
||||
},
|
||||
};
|
||||
|
||||
if (options.cachedSnapshot) {
|
||||
cache.set(
|
||||
'multipage-contribution-content-summary-v1',
|
||||
JSON.stringify(options.cachedSnapshot)
|
||||
);
|
||||
}
|
||||
|
||||
const fetchImpl = options.fetchImpl || (async () => ({
|
||||
ok: true,
|
||||
async json() {
|
||||
return {
|
||||
ok: true,
|
||||
items: [],
|
||||
prompt_version: '',
|
||||
has_visible_updates: false,
|
||||
latest_updated_at: '',
|
||||
latest_updated_at_display: '',
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
const wrappedFetch = async (...args) => {
|
||||
fetchCalls += 1;
|
||||
return fetchImpl(...args);
|
||||
};
|
||||
|
||||
const api = new Function(
|
||||
'window',
|
||||
'localStorage',
|
||||
'fetch',
|
||||
'AbortController',
|
||||
'setTimeout',
|
||||
'clearTimeout',
|
||||
`${source}; return window.SidepanelContributionContentService;`
|
||||
)(
|
||||
windowObject,
|
||||
localStorage,
|
||||
wrappedFetch,
|
||||
AbortController,
|
||||
setTimeout,
|
||||
clearTimeout
|
||||
);
|
||||
|
||||
return {
|
||||
api,
|
||||
getFetchCalls() {
|
||||
return fetchCalls;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('getContentUpdateSnapshot returns a prompt version for visible contribution content updates', async () => {
|
||||
const { api } = createContributionContentService({
|
||||
fetchImpl: async () => ({
|
||||
ok: true,
|
||||
async json() {
|
||||
return {
|
||||
ok: true,
|
||||
prompt_version: 'announcement:2026-04-21T12:00:00Z|tutorial:2026-04-21T12:05:00Z',
|
||||
has_visible_updates: true,
|
||||
latest_updated_at: '2026-04-21T12:05:00Z',
|
||||
latest_updated_at_display: '2026-04-21 20:05',
|
||||
items: [
|
||||
{
|
||||
slug: 'announcement',
|
||||
title: '最新公告',
|
||||
is_enabled: true,
|
||||
has_content: true,
|
||||
is_visible: true,
|
||||
updated_at: '2026-04-21T12:00:00Z',
|
||||
updated_at_display: '2026-04-21 20:00',
|
||||
},
|
||||
{
|
||||
slug: 'tutorial',
|
||||
title: '使用教程',
|
||||
is_enabled: true,
|
||||
has_content: true,
|
||||
is_visible: true,
|
||||
updated_at: '2026-04-21T12:05:00Z',
|
||||
updated_at_display: '2026-04-21 20:05',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const snapshot = await api.getContentUpdateSnapshot();
|
||||
|
||||
assert.equal(snapshot.status, 'update-available');
|
||||
assert.equal(snapshot.promptVersion, 'announcement:2026-04-21T12:00:00Z|tutorial:2026-04-21T12:05:00Z');
|
||||
assert.equal(snapshot.hasVisibleUpdates, true);
|
||||
assert.equal(snapshot.latestUpdatedAt, '2026-04-21T12:05:00Z');
|
||||
assert.equal(snapshot.items.length, 2);
|
||||
assert.deepEqual(
|
||||
snapshot.items.map((item) => [item.slug, item.isVisible]),
|
||||
[['announcement', true], ['tutorial', true]]
|
||||
);
|
||||
});
|
||||
|
||||
test('getContentUpdateSnapshot falls back to cached snapshot when the live request fails', async () => {
|
||||
const cachedSnapshot = {
|
||||
status: 'update-available',
|
||||
promptVersion: 'announcement:2026-04-20T00:00:00Z',
|
||||
hasVisibleUpdates: true,
|
||||
latestUpdatedAt: '2026-04-20T00:00:00Z',
|
||||
latestUpdatedAtDisplay: '2026-04-20 08:00',
|
||||
items: [
|
||||
{
|
||||
slug: 'announcement',
|
||||
title: '站点公告',
|
||||
isEnabled: true,
|
||||
hasContent: true,
|
||||
isVisible: true,
|
||||
updatedAt: '2026-04-20T00:00:00Z',
|
||||
updatedAtDisplay: '2026-04-20 08:00',
|
||||
},
|
||||
],
|
||||
portalUrl: 'https://apikey.qzz.io',
|
||||
apiUrl: 'https://apikey.qzz.io/api/content-summary',
|
||||
checkedAt: Date.now() - 1000,
|
||||
};
|
||||
|
||||
const { api, getFetchCalls } = createContributionContentService({
|
||||
cachedSnapshot,
|
||||
fetchImpl: async () => {
|
||||
throw new Error('offline');
|
||||
},
|
||||
});
|
||||
|
||||
const snapshot = await api.getContentUpdateSnapshot();
|
||||
|
||||
assert.equal(getFetchCalls(), 1);
|
||||
assert.equal(snapshot.fromCache, true);
|
||||
assert.equal(snapshot.promptVersion, cachedSnapshot.promptVersion);
|
||||
assert.equal(snapshot.errorMessage, 'offline');
|
||||
assert.equal(snapshot.items[0].slug, 'announcement');
|
||||
});
|
||||
@@ -56,7 +56,10 @@ function extractFunction(name) {
|
||||
}
|
||||
|
||||
test('handlePollEmail establishes a baseline after opening from detail view and only picks mail from a later refresh', async () => {
|
||||
const bundle = extractFunction('handlePollEmail');
|
||||
const bundle = [
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let state = 'detail';
|
||||
@@ -162,7 +165,10 @@ return {
|
||||
});
|
||||
|
||||
test('handlePollEmail ignores targetEmail and still tests any matching ChatGPT mail', async () => {
|
||||
const bundle = extractFunction('handlePollEmail');
|
||||
const bundle = [
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let state = 'empty';
|
||||
@@ -242,6 +248,94 @@ return {
|
||||
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-1']);
|
||||
});
|
||||
|
||||
test('handlePollEmail only accepts 2925 mails inside the fixed lookback window', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeMinuteTimestamp'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let state = 'ready';
|
||||
const seenCodes = new Set();
|
||||
const readAndDeleteCalls = [];
|
||||
const oldMail = {
|
||||
id: 'mail-old',
|
||||
text: 'OpenAI verification code 111111',
|
||||
timestamp: 1000,
|
||||
};
|
||||
const windowMail = {
|
||||
id: 'mail-window',
|
||||
text: 'OpenAI verification code 222222',
|
||||
timestamp: 301000,
|
||||
};
|
||||
|
||||
function findMailItems() {
|
||||
return state === 'ready' ? [oldMail, windowMail] : [];
|
||||
}
|
||||
|
||||
function getMailItemId(item) {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
function getCurrentMailIds(items = []) {
|
||||
return new Set(items.map((item) => item.id));
|
||||
}
|
||||
|
||||
function parseMailItemTimestamp(item) {
|
||||
return item.timestamp;
|
||||
}
|
||||
|
||||
function matchesMailFilters(text) {
|
||||
return /openai|verification/i.test(String(text || ''));
|
||||
}
|
||||
|
||||
function getMailItemText(item) {
|
||||
return item.text;
|
||||
}
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
const match = String(text || '').match(/(\\d{6})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
async function sleep() {}
|
||||
async function sleepRandom() {}
|
||||
async function returnToInbox() {
|
||||
return true;
|
||||
}
|
||||
async function refreshInbox() {}
|
||||
|
||||
async function openMailAndDeleteAfterRead(item) {
|
||||
readAndDeleteCalls.push(item.id);
|
||||
return item.text;
|
||||
}
|
||||
|
||||
async function ensureSeenCodesSession() {}
|
||||
function persistSeenCodes() {}
|
||||
function log() {}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
handlePollEmail,
|
||||
getReadAndDeleteCalls() {
|
||||
return readAndDeleteCalls.slice();
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.handlePollEmail(4, {
|
||||
senderFilters: ['openai'],
|
||||
subjectFilters: ['verification'],
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
filterAfterTimestamp: 120000,
|
||||
});
|
||||
|
||||
assert.equal(result.code, '222222');
|
||||
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-window']);
|
||||
});
|
||||
|
||||
test('ensureSeenCodesSession resets tried codes only when a new verification step session starts', async () => {
|
||||
const bundle = [
|
||||
extractFunction('buildSeenCodeSessionKey'),
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => sidepanelSource.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 < sidepanelSource.length; i += 1) {
|
||||
const ch = sidepanelSource[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < sidepanelSource.length; end += 1) {
|
||||
const ch = sidepanelSource[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sidepanelSource.slice(start, end);
|
||||
}
|
||||
|
||||
function createApi({ refreshImpl } = {}) {
|
||||
const bundle = extractFunction('startAutoRunFromCurrentSettings');
|
||||
|
||||
return new Function(`
|
||||
const events = [];
|
||||
const latestState = { contributionMode: false };
|
||||
const inputAutoSkipFailures = { checked: false };
|
||||
const inputContributionNickname = { value: 'tester' };
|
||||
const inputContributionQq = { value: '123456' };
|
||||
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
|
||||
const inputAutoDelayEnabled = { checked: false };
|
||||
const inputAutoDelayMinutes = { value: '30' };
|
||||
const btnAutoRun = { disabled: false, innerHTML: '' };
|
||||
const inputRunCount = { disabled: false };
|
||||
const chrome = {
|
||||
runtime: {
|
||||
async sendMessage(message) {
|
||||
events.push({ type: 'send', message });
|
||||
return { ok: true };
|
||||
},
|
||||
},
|
||||
};
|
||||
const console = {
|
||||
warn(...args) {
|
||||
events.push({ type: 'warn', args });
|
||||
},
|
||||
};
|
||||
function getRunCountValue() { return 3; }
|
||||
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
|
||||
function shouldOfferAutoModeChoice() { return false; }
|
||||
async function openAutoStartChoiceDialog() { throw new Error('should not be called'); }
|
||||
function getFirstUnfinishedStep() { return 1; }
|
||||
function getRunningSteps() { return []; }
|
||||
function shouldWarnAutoRunFallbackRisk() { return false; }
|
||||
function isAutoRunFallbackRiskPromptDismissed() { return false; }
|
||||
async function openAutoRunFallbackRiskConfirmModal() { throw new Error('should not be called'); }
|
||||
function setAutoRunFallbackRiskPromptDismissed() {}
|
||||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
|
||||
async function refreshContributionContentHint() {
|
||||
events.push({ type: 'refresh' });
|
||||
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
|
||||
}
|
||||
${bundle}
|
||||
return {
|
||||
startAutoRunFromCurrentSettings,
|
||||
getEvents() {
|
||||
return events;
|
||||
},
|
||||
};
|
||||
`)();
|
||||
}
|
||||
|
||||
test('startAutoRunFromCurrentSettings refreshes contribution content hint before starting auto run', async () => {
|
||||
const api = createApi();
|
||||
|
||||
const result = await api.startAutoRunFromCurrentSettings();
|
||||
|
||||
assert.equal(result, true);
|
||||
assert.deepEqual(
|
||||
api.getEvents().map((entry) => entry.type),
|
||||
['refresh', 'send']
|
||||
);
|
||||
assert.equal(api.getEvents()[1].message.type, 'AUTO_RUN');
|
||||
});
|
||||
|
||||
test('startAutoRunFromCurrentSettings continues auto run when contribution content refresh fails', async () => {
|
||||
const api = createApi({
|
||||
refreshImpl: 'async () => { throw new Error("refresh failed"); }',
|
||||
});
|
||||
|
||||
const result = await api.startAutoRunFromCurrentSettings();
|
||||
const events = api.getEvents();
|
||||
|
||||
assert.equal(result, true);
|
||||
assert.deepEqual(
|
||||
events.map((entry) => entry.type),
|
||||
['refresh', 'warn', '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');
|
||||
});
|
||||
@@ -5,10 +5,18 @@ const fs = require('node:fs');
|
||||
test('sidepanel html keeps a single contribution mode button in header', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
const matches = html.match(/id="btn-contribution-mode"/g) || [];
|
||||
const serviceIndex = html.indexOf('<script src="contribution-content-update-service.js"></script>');
|
||||
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
||||
|
||||
assert.equal(matches.length, 1);
|
||||
assert.match(html, /id="btn-contribution-mode"[^>]*title="进入贡献模式并打开上传页"/);
|
||||
assert.match(html, />贡献\/使用<\/button>/);
|
||||
assert.match(html, /id="contribution-update-hint"/);
|
||||
assert.match(html, /id="contribution-update-hint-text"/);
|
||||
assert.match(html, /id="btn-dismiss-contribution-update-hint"/);
|
||||
assert.notEqual(serviceIndex, -1);
|
||||
assert.notEqual(sidepanelIndex, -1);
|
||||
assert.ok(serviceIndex < sidepanelIndex);
|
||||
});
|
||||
|
||||
test('sidepanel source no longer keeps the legacy upload-page handler on the header contribution button', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ const source = fs.readFileSync('background/verification-flow.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundVerificationFlow;`)(globalScope);
|
||||
|
||||
test('verification flow extends 2925 polling window', () => {
|
||||
test('verification flow keeps 2925 polling cadence in the default payload', () => {
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
@@ -37,10 +37,8 @@ test('verification flow extends 2925 polling window', () => {
|
||||
const step4Payload = helpers.getVerificationPollPayload(4, { email: 'user@example.com', mailProvider: '2925' });
|
||||
const step8Payload = helpers.getVerificationPollPayload(8, { email: 'user@example.com', mailProvider: '2925' });
|
||||
|
||||
assert.equal(step4Payload.filterAfterTimestamp, 0);
|
||||
assert.equal(step4Payload.maxAttempts, 15);
|
||||
assert.equal(step4Payload.intervalMs, 15000);
|
||||
assert.equal(step8Payload.filterAfterTimestamp, 0);
|
||||
assert.equal(step8Payload.maxAttempts, 15);
|
||||
assert.equal(step8Payload.intervalMs, 15000);
|
||||
});
|
||||
@@ -111,7 +109,7 @@ test('verification flow runs beforeSubmit hook before filling the code', async (
|
||||
]);
|
||||
});
|
||||
|
||||
test('verification flow clears 2925 mailbox before polling and after successful login code submission', async () => {
|
||||
test('verification flow skips 2925 mailbox preclear when using a fixed login mail window and still clears after success', async () => {
|
||||
const mailMessages = [];
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
@@ -164,15 +162,15 @@ test('verification flow clears 2925 mailbox before polling and after successful
|
||||
lastLoginCode: null,
|
||||
},
|
||||
{ provider: '2925', label: '2925 邮箱' },
|
||||
{}
|
||||
{ filterAfterTimestamp: 123456 }
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
assert.deepStrictEqual(mailMessages, ['DELETE_ALL_EMAILS', 'POLL_EMAIL', 'DELETE_ALL_EMAILS']);
|
||||
assert.deepStrictEqual(mailMessages, ['POLL_EMAIL', 'DELETE_ALL_EMAILS']);
|
||||
});
|
||||
|
||||
test('verification flow clears 2925 mailbox before polling and after successful signup code submission', async () => {
|
||||
test('verification flow skips 2925 mailbox preclear when using a fixed signup mail window and still clears after success', async () => {
|
||||
const mailMessages = [];
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
@@ -229,13 +227,14 @@ test('verification flow clears 2925 mailbox before polling and after successful
|
||||
},
|
||||
{ provider: '2925', label: '2925 邮箱' },
|
||||
{
|
||||
filterAfterTimestamp: 123456,
|
||||
requestFreshCodeFirst: false,
|
||||
}
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
assert.deepStrictEqual(mailMessages, ['DELETE_ALL_EMAILS', 'POLL_EMAIL', 'DELETE_ALL_EMAILS']);
|
||||
assert.deepStrictEqual(mailMessages, ['POLL_EMAIL', 'DELETE_ALL_EMAILS']);
|
||||
});
|
||||
|
||||
test('verification flow completes step 8 and flags phone verification when add-phone appears after login code submit', async () => {
|
||||
|
||||
+28
-1
@@ -28,7 +28,7 @@
|
||||
|
||||
### 2.1 Sidepanel
|
||||
|
||||
[sidepanel/sidepanel.html](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.html) + [sidepanel/sidepanel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.js) + [sidepanel/update-service.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/update-service.js)
|
||||
[sidepanel/sidepanel.html](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.html) + [sidepanel/sidepanel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.js) + [sidepanel/update-service.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/update-service.js) + [sidepanel/contribution-content-update-service.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/contribution-content-update-service.js)
|
||||
|
||||
职责:
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
- 动态渲染步骤列表
|
||||
- 管理顶部“贡献”按钮与贡献模式主面板;贡献模式本身是 sidepanel 的运行态 UI 模式,不是新的 `panelMode` 来源
|
||||
- 在贡献模式下复用同一套主自动流程启动,并在面板内展示贡献链路的 `OAUTH / 回调 / 总状态` 三块实时状态
|
||||
- 在顶部“贡献/使用”按钮下方展示一个非强制的内容更新轻提示;提示来源于 `apikey.qzz.io` 的公开公告 / 教程摘要,用户关闭后仅对当前 `promptVersion` 静默,下次内容版本变化后会重新出现
|
||||
- 在 sidepanel 初始化和点击“自动”按钮前刷新一次贡献站公开内容摘要;如果刷新失败,不阻塞主自动流程
|
||||
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表
|
||||
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Pro` 与 legacy `v` 两个版本族,排序时优先保持版本族语义一致,同时会在读取缓存后重新排序,避免旧缓存把 `v` 版本误显示为比 `Pro` 更新
|
||||
|
||||
@@ -207,6 +209,26 @@
|
||||
- 安全边界仍然在服务端;扩展只负责公开 OAuth 贡献前端流程
|
||||
- 退出贡献模式后,要恢复普通模式 UI,并恢复持久配置中的自定义密码/本地同步偏好
|
||||
|
||||
### 4.5 贡献内容更新提示链路
|
||||
|
||||
这条链路用于把 `apikey.qzz.io` 上的公开公告 / 使用教程更新,映射成 sidepanel 顶部“贡献/使用”按钮下方的轻提示。
|
||||
|
||||
1. 贡献站公开暴露 `GET https://apikey.qzz.io/api/content-summary`
|
||||
2. 接口返回 `announcement / tutorial` 两类内容的可见状态、更新时间,以及聚合后的 `promptVersion`
|
||||
3. `sidepanel/contribution-content-update-service.js` 负责拉取这个摘要,并在本地缓存最近一次可用结果
|
||||
4. `sidepanel.js` 在 sidepanel 初始化时会先拉一次摘要;如果当前 `promptVersion` 没被本地关闭过,就显示提示
|
||||
5. 用户点击提示右侧 `X` 后,只会把当前 `promptVersion` 记入 `localStorage`,不会永久关闭整个能力
|
||||
6. 当公告或教程再次更新,服务端返回新的 `promptVersion`,提示会重新出现
|
||||
7. 用户点击“自动”按钮时,sidepanel 会在真正启动自动流程前再刷新一次摘要,尽量让提示状态更及时
|
||||
8. 如果这次刷新失败,sidepanel 只记录警告并继续自动流程,不会因为提示链路故障阻塞主功能
|
||||
|
||||
这条链路的关键边界是:
|
||||
|
||||
- 它只消费公开摘要接口,不接触任何管理接口
|
||||
- 它只影响顶部轻提示,不改变贡献模式主状态机
|
||||
- 它使用 `localStorage` 做缓存和关闭版本记录,而不是写入 `chrome.storage.session/local`
|
||||
- 它是辅助提示链路,不允许反向阻塞自动运行
|
||||
|
||||
## 5. 内容脚本通信链路
|
||||
|
||||
### 5.1 READY 机制
|
||||
@@ -620,3 +642,8 @@
|
||||
- Step 8 如果发现认证页已经进入登录超时报错/重试页,会直接报错并回到 Step 7 重新开始,而不是在 Step 8 内部点击 `重试`。
|
||||
- 任意认证页重试页如果正文中出现 `max_check_attempts`,会被视为 Cloudflare 风控触发:后台立刻完全停止流程,侧边栏会复用现有确认弹窗提示等待 15~30 分钟后再试,避免继续刷新或反复重试加重风控,确认按钮显示为“我知道了”。
|
||||
- Step 9 在点击 OAuth 同意页 `继续` 后,会额外检查是否进入认证页重试页;若命中则先通过共享恢复逻辑最多自动点击 5 次 `重试` 尝试恢复,再重新执行当前轮的 `继续` 点击。
|
||||
## 2026-04-21 2925 邮件时间窗补充
|
||||
|
||||
- `2925` 在 Step 4 / Step 8 现在会携带固定的步骤开始时间窗口,实际筛选下限为“步骤开始时间向前回看 10 分钟”。
|
||||
- 为了保留这段固定时间窗内已经到达的验证码邮件,后台不再在轮询开始前预先清空 2925 邮箱。
|
||||
- `2925` 验证码最终提交成功后,后台仍会异步发送 `DELETE_ALL_EMAILS` 做收尾清理。
|
||||
|
||||
+6
-3
@@ -117,10 +117,11 @@
|
||||
- `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
|
||||
- `sidepanel/mail-2925-manager.js`:侧边栏 2925 账号池管理器,负责 2925 账号的新增、导入、切换、手动登录、启停、清冷却与删除。
|
||||
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。
|
||||
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
|
||||
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行的禁用与隐藏。
|
||||
- `sidepanel/sidepanel.css`:侧边栏样式文件。
|
||||
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,别名基邮箱行右侧会显示“号池”开关,开启后输入框切换为号池邮箱下拉选择,并显示 2925 账号池管理区。
|
||||
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 manager;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 2925 号池开关开启时,当前账号变化会同步映射到 2925 基邮箱下拉选择,关闭时则回退到原来的手填基邮箱路线。
|
||||
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,别名基邮箱行右侧会显示“号池”开关,开启后输入框切换为号池邮箱下拉选择,并显示 2925 账号池管理区。
|
||||
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 2925 号池开关开启时,当前账号变化会同步映射到 2925 基邮箱下拉选择,关闭时则回退到原来的手填基邮箱路线;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。
|
||||
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。
|
||||
|
||||
## `tests/`
|
||||
@@ -171,9 +172,11 @@
|
||||
- `tests/managed-alias-utils.test.js`:覆盖 Gmail / 2925 共享别名工具的解析、生成、兼容性判断。
|
||||
- `tests/microsoft-email.test.js`:测试 Microsoft 邮件拉取与验证码提取逻辑。
|
||||
- `tests/sidepanel-hotmail-manager.test.js`:测试侧边栏 Hotmail 管理器模块接线与空态渲染。
|
||||
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线,以及直接打开账号贡献上传页的最小行为。
|
||||
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
|
||||
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏邮箱记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
|
||||
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
|
||||
- `tests/sidepanel-contribution-mode.test.js`:测试侧边栏贡献模式的 HTML 接线、runtime-only 设置保护,以及贡献模式 manager 复用主自动流启动、状态轮询和退出清理逻辑。
|
||||
- `tests/sidepanel-auto-run-content-refresh.test.js`:测试点击“自动”时会先刷新贡献内容更新摘要,且刷新失败不会阻塞自动流程启动。
|
||||
- `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。
|
||||
- `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析逻辑。
|
||||
- `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
|
||||
|
||||
Reference in New Issue
Block a user