it ok
This commit is contained in:
+443
-320
@@ -1,381 +1,504 @@
|
||||
// content/mail-163.js — Content script for 163 Mail (steps 4, 7)
|
||||
// Injected on: mail.163.com
|
||||
//
|
||||
// DOM structure:
|
||||
// Mail item: div[sign="letter"] with aria-label="你的 ChatGPT 代码为 479637 发件人 : OpenAI ..."
|
||||
// Sender: .nui-user (e.g., "OpenAI")
|
||||
// Subject: span.da0 (e.g., "你的 ChatGPT 代码为 479637")
|
||||
// Delete actions: hover trash icon on the row, or checkbox + toolbar delete button
|
||||
// content/mail-163.js - Content script for 163 Mail (steps 4, 7)
|
||||
// Injected on: mail.163.com / webmail.vip.163.com
|
||||
|
||||
const MAIL163_PREFIX = '[MultiPage:mail-163]';
|
||||
const isTopFrame = window === window.top;
|
||||
|
||||
console.log(MAIL163_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
|
||||
|
||||
// Only operate in the top frame
|
||||
if (!isTopFrame) {
|
||||
console.log(MAIL163_PREFIX, 'Skipping child frame');
|
||||
} else {
|
||||
let seenCodes = new Set();
|
||||
|
||||
// Track codes we've already seen — persisted in chrome.storage.session to survive script re-injection
|
||||
let seenCodes = new Set();
|
||||
|
||||
async function loadSeenCodes() {
|
||||
try {
|
||||
const data = await chrome.storage.session.get('seenCodes');
|
||||
if (data.seenCodes && Array.isArray(data.seenCodes)) {
|
||||
seenCodes = new Set(data.seenCodes);
|
||||
console.log(MAIL163_PREFIX, `Loaded ${seenCodes.size} previously seen codes`);
|
||||
async function loadSeenCodes() {
|
||||
try {
|
||||
const data = await chrome.storage.session.get('seenCodes');
|
||||
if (data.seenCodes && Array.isArray(data.seenCodes)) {
|
||||
seenCodes = new Set(data.seenCodes);
|
||||
console.log(MAIL163_PREFIX, `Loaded ${seenCodes.size} previously seen codes`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(MAIL163_PREFIX, 'Session storage unavailable, using in-memory seen codes:', err?.message || err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(MAIL163_PREFIX, 'Session storage unavailable, using in-memory seen codes:', err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
// Load previously seen codes on startup
|
||||
loadSeenCodes();
|
||||
loadSeenCodes();
|
||||
|
||||
async function persistSeenCodes() {
|
||||
try {
|
||||
await chrome.storage.session.set({ seenCodes: [...seenCodes] });
|
||||
} catch (err) {
|
||||
console.warn(MAIL163_PREFIX, 'Could not persist seen codes, continuing in-memory only:', err?.message || err);
|
||||
async function persistSeenCodes() {
|
||||
try {
|
||||
await chrome.storage.session.set({ seenCodes: [...seenCodes] });
|
||||
} catch (err) {
|
||||
console.warn(MAIL163_PREFIX, 'Could not persist seen codes, continuing in-memory only:', err?.message || err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Message Handler (top frame only)
|
||||
// ============================================================
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type !== 'POLL_EMAIL') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'POLL_EMAIL') {
|
||||
resetStopState();
|
||||
handlePollEmail(message.step, message.payload).then(result => {
|
||||
sendResponse(result);
|
||||
}).catch(err => {
|
||||
if (isStopError(err)) {
|
||||
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
handlePollEmail(message.step, message.payload)
|
||||
.then((result) => {
|
||||
sendResponse(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (isStopError(err)) {
|
||||
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function findMailItems() {
|
||||
return document.querySelectorAll('div[sign="letter"]');
|
||||
}
|
||||
|
||||
function getCurrentMailIds(items = findMailItems()) {
|
||||
const ids = new Set();
|
||||
Array.from(items).forEach((item) => {
|
||||
const id = item.getAttribute('id') || '';
|
||||
if (id) ids.add(id);
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
function normalizeMinuteTimestamp(timestamp) {
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) return 0;
|
||||
const date = new Date(timestamp);
|
||||
date.setSeconds(0, 0);
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
function parseMail163Timestamp(rawText) {
|
||||
const text = normalizeText(rawText);
|
||||
if (!text) return null;
|
||||
|
||||
let match = text.match(/(\d{4})年(\d{1,2})月(\d{1,2})日\s+(\d{1,2}):(\d{2})/);
|
||||
if (match) {
|
||||
const [, year, month, day, hour, minute] = match;
|
||||
return new Date(
|
||||
Number(year),
|
||||
Number(month) - 1,
|
||||
Number(day),
|
||||
Number(hour),
|
||||
Number(minute),
|
||||
0,
|
||||
0
|
||||
).getTime();
|
||||
}
|
||||
|
||||
match = text.match(/\b(\d{1,2}):(\d{2})\b/);
|
||||
if (match) {
|
||||
const [, hour, minute] = match;
|
||||
const now = new Date();
|
||||
return new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
Number(hour),
|
||||
Number(minute),
|
||||
0,
|
||||
0
|
||||
).getTime();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getMailTimestamp(item) {
|
||||
const candidates = [];
|
||||
const timeCell = item.querySelector('.e00[title], [title*="年"][title*=":"]');
|
||||
if (timeCell?.getAttribute('title')) candidates.push(timeCell.getAttribute('title'));
|
||||
if (timeCell?.textContent) candidates.push(timeCell.textContent);
|
||||
|
||||
item.querySelectorAll('[title]').forEach((node) => {
|
||||
const title = node.getAttribute('title');
|
||||
if (title) candidates.push(title);
|
||||
});
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const parsed = parseMail163Timestamp(candidate);
|
||||
if (parsed) return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function scheduleEmailCleanup(item, step) {
|
||||
setTimeout(() => {
|
||||
Promise.resolve(deleteEmail(item, step)).catch(() => {
|
||||
// Cleanup is best effort only and must never affect the main verification flow.
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function findInboxLink() {
|
||||
const direct = document.querySelector('.nui-tree-item-text[title="收件箱"]');
|
||||
if (direct) return direct;
|
||||
|
||||
const inboxPattern = /(?:Inbox|\u6536\u4ef6\u7bb1)/i;
|
||||
const candidates = document.querySelectorAll('.nui-tree-item-text, .nui-tree-item, a, span, div');
|
||||
for (const node of candidates) {
|
||||
const text = normalizeText([
|
||||
node.getAttribute?.('title') || '',
|
||||
node.getAttribute?.('aria-label') || '',
|
||||
node.textContent || '',
|
||||
].join(' '));
|
||||
if (inboxPattern.test(text)) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getMailPreviewText(item) {
|
||||
return normalizeText([
|
||||
item.querySelector('.nui-user')?.textContent || '',
|
||||
item.querySelector('span.da0')?.textContent || '',
|
||||
item.getAttribute('aria-label') || '',
|
||||
].join(' '));
|
||||
}
|
||||
|
||||
function readOpenedMailText() {
|
||||
const texts = [];
|
||||
const seenTexts = new Set();
|
||||
|
||||
const pushText = (value) => {
|
||||
const normalized = normalizeText(value);
|
||||
if (!normalized || normalized.length < 20 || seenTexts.has(normalized)) {
|
||||
return;
|
||||
}
|
||||
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
|
||||
sendResponse({ error: err.message });
|
||||
seenTexts.add(normalized);
|
||||
texts.push(normalized);
|
||||
};
|
||||
|
||||
const detailSelectors = [
|
||||
'.nui-iframe-body',
|
||||
'.nui-msgbox',
|
||||
'.mail-view',
|
||||
'.mail-detail',
|
||||
'.mail-reader',
|
||||
'.reader-main',
|
||||
'.reader-body',
|
||||
'.readMainWrap',
|
||||
'.mD0',
|
||||
'.oD0',
|
||||
'.nui-scroll',
|
||||
];
|
||||
|
||||
detailSelectors.forEach((selector) => {
|
||||
document.querySelectorAll(selector).forEach((node) => {
|
||||
pushText(node.innerText || node.textContent || '');
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Find mail items
|
||||
// ============================================================
|
||||
|
||||
function findMailItems() {
|
||||
return document.querySelectorAll('div[sign="letter"]');
|
||||
}
|
||||
|
||||
function getCurrentMailIds() {
|
||||
const ids = new Set();
|
||||
findMailItems().forEach(item => {
|
||||
const id = item.getAttribute('id') || '';
|
||||
if (id) ids.add(id);
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
function normalizeMinuteTimestamp(timestamp) {
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) return 0;
|
||||
const date = new Date(timestamp);
|
||||
date.setSeconds(0, 0);
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
function parseMail163Timestamp(rawText) {
|
||||
const text = (rawText || '').replace(/\s+/g, ' ').trim();
|
||||
if (!text) return null;
|
||||
|
||||
let match = text.match(/(\d{4})年(\d{1,2})月(\d{1,2})日\s+(\d{1,2}):(\d{2})/);
|
||||
if (match) {
|
||||
const [, year, month, day, hour, minute] = match;
|
||||
return new Date(
|
||||
Number(year),
|
||||
Number(month) - 1,
|
||||
Number(day),
|
||||
Number(hour),
|
||||
Number(minute),
|
||||
0,
|
||||
0
|
||||
).getTime();
|
||||
}
|
||||
|
||||
match = text.match(/\b(\d{1,2}):(\d{2})\b/);
|
||||
if (match) {
|
||||
const [, hour, minute] = match;
|
||||
const now = new Date();
|
||||
return new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
Number(hour),
|
||||
Number(minute),
|
||||
0,
|
||||
0
|
||||
).getTime();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getMailTimestamp(item) {
|
||||
const candidates = [];
|
||||
const timeCell = item.querySelector('.e00[title], [title*="年"][title*=":"]');
|
||||
if (timeCell?.getAttribute('title')) candidates.push(timeCell.getAttribute('title'));
|
||||
if (timeCell?.textContent) candidates.push(timeCell.textContent);
|
||||
|
||||
const titledNodes = item.querySelectorAll('[title]');
|
||||
titledNodes.forEach((node) => {
|
||||
const title = node.getAttribute('title');
|
||||
if (title) candidates.push(title);
|
||||
});
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const parsed = parseMail163Timestamp(candidate);
|
||||
if (parsed) return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function scheduleEmailCleanup(item, step) {
|
||||
setTimeout(() => {
|
||||
Promise.resolve(deleteEmail(item, step)).catch(() => {
|
||||
// Cleanup is best effort only and must never affect the main verification flow.
|
||||
document.querySelectorAll('iframe, frame').forEach((frame) => {
|
||||
try {
|
||||
const frameDoc = frame.contentDocument || frame.contentWindow?.document;
|
||||
if (!frameDoc) return;
|
||||
pushText(frameDoc.body?.innerText || frameDoc.body?.textContent || '');
|
||||
} catch (_) {
|
||||
// Ignore inaccessible frames.
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Email Polling
|
||||
// ============================================================
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [], filterAfterTimestamp = 0 } = payload;
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
|
||||
|
||||
log(`步骤 ${step}:开始轮询 163 邮箱(最多 ${maxAttempts} 次)`);
|
||||
if (filterAfterMinute) {
|
||||
log(`步骤 ${step}:仅尝试 ${new Date(filterAfterMinute).toLocaleString('zh-CN', { hour12: false })} 及之后时间的邮件。`);
|
||||
}
|
||||
|
||||
// Click inbox in sidebar to ensure we're in inbox view
|
||||
log(`步骤 ${step}:正在等待侧边栏加载...`);
|
||||
try {
|
||||
const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000);
|
||||
inboxLink.click();
|
||||
log(`步骤 ${step}:已点击收件箱`);
|
||||
} catch {
|
||||
log(`步骤 ${step}:未找到收件箱入口,继续尝试后续流程...`, 'warn');
|
||||
}
|
||||
|
||||
// Wait for mail list to appear
|
||||
log(`步骤 ${step}:正在等待邮件列表加载...`);
|
||||
let items = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
items = findMailItems();
|
||||
if (items.length > 0) break;
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
await refreshInbox();
|
||||
await sleep(2000);
|
||||
items = findMailItems();
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
throw new Error('163 邮箱列表未加载完成,请确认当前已打开收件箱。');
|
||||
}
|
||||
|
||||
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
|
||||
|
||||
// Snapshot existing mail IDs
|
||||
const existingMailIds = getCurrentMailIds();
|
||||
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
|
||||
|
||||
const FALLBACK_AFTER = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
log(`步骤 ${step}:正在轮询 163 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
||||
|
||||
if (attempt > 1) {
|
||||
await refreshInbox();
|
||||
await sleep(1000);
|
||||
if (!texts.length) {
|
||||
pushText(document.body?.innerText || document.body?.textContent || '');
|
||||
}
|
||||
|
||||
const allItems = findMailItems();
|
||||
const useFallback = attempt > FALLBACK_AFTER;
|
||||
texts.sort((left, right) => right.length - left.length);
|
||||
return texts[0] || '';
|
||||
}
|
||||
|
||||
for (const item of allItems) {
|
||||
const id = item.getAttribute('id') || '';
|
||||
const mailTimestamp = getMailTimestamp(item);
|
||||
const mailMinute = normalizeMinuteTimestamp(mailTimestamp || 0);
|
||||
const passesTimeFilter = !filterAfterMinute || (mailMinute && mailMinute >= filterAfterMinute);
|
||||
const shouldBypassOldSnapshot = Boolean(filterAfterMinute && passesTimeFilter && mailMinute > 0);
|
||||
async function returnToInbox() {
|
||||
if (findMailItems().length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!passesTimeFilter) {
|
||||
continue;
|
||||
const inboxLink = findInboxLink();
|
||||
if (inboxLink) {
|
||||
simulateClick(inboxLink);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
if (findMailItems().length > 0) {
|
||||
return true;
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function openMailAndGetMessageText(item) {
|
||||
const previewText = getMailPreviewText(item);
|
||||
|
||||
simulateClick(item);
|
||||
|
||||
let openedText = '';
|
||||
for (let i = 0; i < 24; i += 1) {
|
||||
const candidateText = readOpenedMailText();
|
||||
if (candidateText && candidateText !== previewText && candidateText.length >= previewText.length) {
|
||||
openedText = candidateText;
|
||||
if (extractVerificationCode(candidateText) || candidateText.length > previewText.length + 20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
await returnToInbox();
|
||||
return openedText;
|
||||
}
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
const {
|
||||
senderFilters,
|
||||
subjectFilters,
|
||||
maxAttempts,
|
||||
intervalMs,
|
||||
excludeCodes = [],
|
||||
filterAfterTimestamp = 0,
|
||||
} = payload;
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
|
||||
|
||||
log(`步骤 ${step}:开始轮询 163 邮箱(最多 ${maxAttempts} 次)`);
|
||||
if (filterAfterMinute) {
|
||||
log(`步骤 ${step}:仅尝试 ${new Date(filterAfterMinute).toLocaleString('zh-CN', { hour12: false })} 及之后时间的邮件。`);
|
||||
}
|
||||
|
||||
log(`步骤 ${step}:正在等待侧边栏加载...`);
|
||||
try {
|
||||
const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000);
|
||||
inboxLink.click();
|
||||
log(`步骤 ${step}:已点击收件箱`);
|
||||
} catch {
|
||||
log(`步骤 ${step}:未找到收件箱入口,继续尝试后续流程...`, 'warn');
|
||||
}
|
||||
|
||||
log(`步骤 ${step}:正在等待邮件列表加载...`);
|
||||
let items = [];
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
items = findMailItems();
|
||||
if (items.length > 0) break;
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
await refreshInbox();
|
||||
await sleep(2000);
|
||||
items = findMailItems();
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
throw new Error('163 邮箱列表未加载完成,请确认当前已打开收件箱。');
|
||||
}
|
||||
|
||||
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
|
||||
|
||||
const existingMailIds = getCurrentMailIds(items);
|
||||
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
|
||||
|
||||
const FALLBACK_AFTER = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
log(`步骤 ${step}:正在轮询 163 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
||||
|
||||
if (attempt > 1) {
|
||||
await refreshInbox();
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
if (!useFallback && !shouldBypassOldSnapshot && existingMailIds.has(id)) continue;
|
||||
const allItems = findMailItems();
|
||||
const useFallback = attempt > FALLBACK_AFTER;
|
||||
|
||||
const senderEl = item.querySelector('.nui-user');
|
||||
const sender = senderEl ? senderEl.textContent.toLowerCase() : '';
|
||||
for (const item of allItems) {
|
||||
const id = item.getAttribute('id') || '';
|
||||
const mailTimestamp = getMailTimestamp(item);
|
||||
const mailMinute = normalizeMinuteTimestamp(mailTimestamp || 0);
|
||||
const passesTimeFilter = !filterAfterMinute || (mailMinute && mailMinute >= filterAfterMinute);
|
||||
const shouldBypassOldSnapshot = Boolean(filterAfterMinute && passesTimeFilter && mailMinute > 0);
|
||||
|
||||
const subjectEl = item.querySelector('span.da0');
|
||||
const subject = subjectEl ? subjectEl.textContent : '';
|
||||
if (!passesTimeFilter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ariaLabel = (item.getAttribute('aria-label') || '').toLowerCase();
|
||||
if (!useFallback && !shouldBypassOldSnapshot && existingMailIds.has(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
|
||||
const subjectMatch = subjectFilters.some(f => subject.toLowerCase().includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
|
||||
const sender = normalizeText(item.querySelector('.nui-user')?.textContent || '').toLowerCase();
|
||||
const subject = normalizeText(item.querySelector('span.da0')?.textContent || '');
|
||||
const ariaLabel = normalizeText(item.getAttribute('aria-label') || '').toLowerCase();
|
||||
|
||||
if (senderMatch || subjectMatch) {
|
||||
const code = extractVerificationCode(subject + ' ' + ariaLabel);
|
||||
if (code && excludedCodeSet.has(code)) {
|
||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
||||
} else if (code && !seenCodes.has(code)) {
|
||||
seenCodes.add(code);
|
||||
const senderMatch = senderFilters.some((filter) => sender.includes(String(filter || '').toLowerCase()) || ariaLabel.includes(String(filter || '').toLowerCase()));
|
||||
const subjectMatch = subjectFilters.some((filter) => subject.toLowerCase().includes(String(filter || '').toLowerCase()) || ariaLabel.includes(String(filter || '').toLowerCase()));
|
||||
|
||||
if (!senderMatch && !subjectMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previewCode = extractVerificationCode(`${subject} ${ariaLabel}`);
|
||||
if (previewCode) {
|
||||
if (excludedCodeSet.has(previewCode)) {
|
||||
log(`步骤 ${step}:跳过排除的验证码:${previewCode}`, 'info');
|
||||
continue;
|
||||
}
|
||||
if (seenCodes.has(previewCode)) {
|
||||
log(`步骤 ${step}:跳过已处理过的验证码:${previewCode}`, 'info');
|
||||
continue;
|
||||
}
|
||||
|
||||
seenCodes.add(previewCode);
|
||||
persistSeenCodes();
|
||||
const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件' : '新邮件';
|
||||
const timeLabel = mailTimestamp ? `,时间:${new Date(mailTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
|
||||
log(`步骤 ${step}:已找到验证码:${code}(来源:${source}${timeLabel},主题:${subject.slice(0, 40)})`, 'ok');
|
||||
log(`步骤 ${step}:已找到验证码:${previewCode}(来源:${source}${timeLabel},主题:${subject.slice(0, 40)})`, 'ok');
|
||||
|
||||
// Trigger cleanup only as a best-effort side effect.
|
||||
scheduleEmailCleanup(item, step);
|
||||
|
||||
return { ok: true, code, emailTimestamp: Date.now(), mailId: id };
|
||||
} else if (code && seenCodes.has(code)) {
|
||||
log(`步骤 ${step}:跳过已处理过的验证码:${code}`, 'info');
|
||||
return { ok: true, code: previewCode, emailTimestamp: Date.now(), mailId: id };
|
||||
}
|
||||
|
||||
const openedText = await openMailAndGetMessageText(item);
|
||||
const bodyCode = extractVerificationCode(openedText);
|
||||
if (!bodyCode) {
|
||||
continue;
|
||||
}
|
||||
if (excludedCodeSet.has(bodyCode)) {
|
||||
log(`步骤 ${step}:跳过排除的验证码:${bodyCode}`, 'info');
|
||||
continue;
|
||||
}
|
||||
if (seenCodes.has(bodyCode)) {
|
||||
log(`步骤 ${step}:跳过已处理过的验证码:${bodyCode}`, 'info');
|
||||
continue;
|
||||
}
|
||||
|
||||
seenCodes.add(bodyCode);
|
||||
persistSeenCodes();
|
||||
const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件正文' : '新邮件正文';
|
||||
const timeLabel = mailTimestamp ? `,时间:${new Date(mailTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
|
||||
log(`步骤 ${step}:已从 163 邮件正文中找到验证码:${bodyCode}(来源:${source}${timeLabel},主题:${subject.slice(0, 40)})`, 'ok');
|
||||
|
||||
scheduleEmailCleanup(item, step);
|
||||
return { ok: true, code: bodyCode, emailTimestamp: Date.now(), mailId: id };
|
||||
}
|
||||
|
||||
if (attempt === FALLBACK_AFTER + 1) {
|
||||
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件。`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt === FALLBACK_AFTER + 1) {
|
||||
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
throw new Error(
|
||||
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 163 邮箱中找到新的匹配邮件。` +
|
||||
'请手动检查收件箱。'
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 163 邮箱中找到新的匹配邮件。` +
|
||||
'请手动检查收件箱。'
|
||||
);
|
||||
}
|
||||
async function deleteEmail(item, step) {
|
||||
try {
|
||||
log(`步骤 ${step}:正在删除邮件...`);
|
||||
|
||||
// ============================================================
|
||||
// Delete Email via Hover Trash / Toolbar Fallback
|
||||
// ============================================================
|
||||
|
||||
async function deleteEmail(item, step) {
|
||||
try {
|
||||
log(`步骤 ${step}:正在删除邮件...`);
|
||||
|
||||
// Strategy 1: Click the trash icon inside the mail item
|
||||
// Each mail item has: <b class="nui-ico nui-ico-delete" title="删除邮件" sign="trash">
|
||||
// These icons appear on hover, so we trigger mouseover first
|
||||
item.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||
item.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
||||
await sleep(300);
|
||||
|
||||
const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]');
|
||||
if (trashIcon) {
|
||||
trashIcon.click();
|
||||
log(`步骤 ${step}:已点击删除图标`, 'ok');
|
||||
await sleep(1500);
|
||||
|
||||
// Check if item disappeared (confirm deletion)
|
||||
const stillExists = document.getElementById(item.id);
|
||||
if (!stillExists || stillExists.style.display === 'none') {
|
||||
log(`步骤 ${step}:邮件已成功删除`);
|
||||
} else {
|
||||
log(`步骤 ${step}:邮件可能尚未删除,列表中仍可见`, 'warn');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy 2: Select checkbox then click toolbar delete button
|
||||
log(`步骤 ${step}:未找到删除图标,尝试使用复选框加工具栏删除...`);
|
||||
const checkbox = item.querySelector('[sign="checkbox"], .nui-chk');
|
||||
if (checkbox) {
|
||||
checkbox.click();
|
||||
item.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||
item.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
||||
await sleep(300);
|
||||
|
||||
// Click toolbar delete button
|
||||
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
|
||||
for (const btn of toolbarBtns) {
|
||||
if (btn.textContent.replace(/\s/g, '').includes('删除')) {
|
||||
btn.closest('.nui-btn').click();
|
||||
log(`步骤 ${step}:已点击工具栏删除`, 'ok');
|
||||
await sleep(1500);
|
||||
return;
|
||||
const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]');
|
||||
if (trashIcon) {
|
||||
simulateClick(trashIcon);
|
||||
log(`步骤 ${step}:已点击删除图标`, 'ok');
|
||||
await sleep(1500);
|
||||
|
||||
const stillExists = item.id ? document.getElementById(item.id) : item;
|
||||
if (!stillExists || stillExists.style?.display === 'none') {
|
||||
log(`步骤 ${step}:邮件已成功删除`);
|
||||
} else {
|
||||
log(`步骤 ${step}:邮件可能尚未删除,列表中仍可见`, 'warn');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log(`步骤 ${step}:未找到删除图标,尝试使用复选框加工具栏删除...`);
|
||||
const checkbox = item.querySelector('[sign="checkbox"], .nui-chk');
|
||||
if (checkbox) {
|
||||
simulateClick(checkbox);
|
||||
await sleep(300);
|
||||
|
||||
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
|
||||
for (const btn of toolbarBtns) {
|
||||
if (normalizeText(btn.textContent || '').includes('删除')) {
|
||||
simulateClick(btn.closest('.nui-btn') || btn);
|
||||
log(`步骤 ${step}:已点击工具栏删除`, 'ok');
|
||||
await sleep(1500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log(`步骤 ${step}:无法删除邮件(未找到删除按钮)`, 'warn');
|
||||
} catch (err) {
|
||||
log(`步骤 ${step}:删除邮件失败:${err.message}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshInbox() {
|
||||
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
|
||||
for (const btn of toolbarBtns) {
|
||||
if (normalizeText(btn.textContent || '') === '刷新') {
|
||||
simulateClick(btn.closest('.nui-btn') || btn);
|
||||
console.log(MAIL163_PREFIX, 'Clicked refresh button');
|
||||
await sleep(800);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log(`步骤 ${step}:无法删除邮件(未找到删除按钮)`, 'warn');
|
||||
} catch (err) {
|
||||
log(`步骤 ${step}:删除邮件失败:${err.message}`, 'warn');
|
||||
}
|
||||
}
|
||||
const receiveButtons = document.querySelectorAll('.ra0');
|
||||
for (const btn of receiveButtons) {
|
||||
if (normalizeText(btn.textContent || '').includes('收信')) {
|
||||
simulateClick(btn);
|
||||
console.log(MAIL163_PREFIX, 'Clicked receive button');
|
||||
await sleep(800);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Inbox Refresh
|
||||
// ============================================================
|
||||
|
||||
async function refreshInbox() {
|
||||
// Try toolbar "刷 新" button
|
||||
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
|
||||
for (const btn of toolbarBtns) {
|
||||
if (btn.textContent.replace(/\s/g, '') === '刷新') {
|
||||
btn.closest('.nui-btn').click();
|
||||
console.log(MAIL163_PREFIX, 'Clicked "刷新" button');
|
||||
const inboxLink = findInboxLink();
|
||||
if (inboxLink) {
|
||||
simulateClick(inboxLink);
|
||||
await sleep(800);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(MAIL163_PREFIX, 'Could not find refresh button');
|
||||
}
|
||||
|
||||
// Fallback: click sidebar "收 信"
|
||||
const shouXinBtns = document.querySelectorAll('.ra0');
|
||||
for (const btn of shouXinBtns) {
|
||||
if (btn.textContent.replace(/\s/g, '').includes('收信')) {
|
||||
btn.click();
|
||||
console.log(MAIL163_PREFIX, 'Clicked "收信" button');
|
||||
await sleep(800);
|
||||
return;
|
||||
}
|
||||
function extractVerificationCode(text) {
|
||||
const matchCn = String(text || '').match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchEn = String(text || '').match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
|
||||
const match6 = String(text || '').match(/\b(\d{6})\b/);
|
||||
if (match6) return match6[1];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(MAIL163_PREFIX, 'Could not find refresh button');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Verification Code Extraction
|
||||
// ============================================================
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
|
||||
const match6 = text.match(/\b(\d{6})\b/);
|
||||
if (match6) return match6[1];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
} // end of isTopFrame else block
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# 163 Mail Body Fallback Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Let the 163 mailbox provider read verification codes from opened email bodies when the inbox list does not show the code inline.
|
||||
|
||||
**Architecture:** Keep the existing list-first polling flow in `content/mail-163.js`, then add a provider-local fallback that opens candidate messages, reads body text, and returns to the inbox before continuing. Do not add `targetEmail` filtering in this change.
|
||||
|
||||
**Tech Stack:** Manifest V3 Chrome extension, plain JavaScript content scripts, Node built-in test runner
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock the regression with a failing test
|
||||
|
||||
**Files:**
|
||||
- Create: `D:\github\codex-oauth-automation-extension-Pro2.0\tests\mail-163-content.test.js`
|
||||
- Test: `D:\github\codex-oauth-automation-extension-Pro2.0\tests\mail-163-content.test.js`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```js
|
||||
test('handlePollEmail opens a matching 163 message and reads the body when the list row has no inline code', async () => {
|
||||
const bundle = extractFunction('handlePollEmail');
|
||||
const api = new Function(`...`)();
|
||||
const result = await api.handlePollEmail(8, {
|
||||
senderFilters: ['openai'],
|
||||
subjectFilters: ['chatgpt'],
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
});
|
||||
assert.equal(result.code, '480382');
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `node --test tests/mail-163-content.test.js`
|
||||
Expected: FAIL because `handlePollEmail` never opens the message body.
|
||||
|
||||
### Task 2: Add the 163 opened-mail fallback
|
||||
|
||||
**Files:**
|
||||
- Modify: `D:\github\codex-oauth-automation-extension-Pro2.0\content\mail-163.js`
|
||||
- Test: `D:\github\codex-oauth-automation-extension-Pro2.0\tests\mail-163-content.test.js`
|
||||
|
||||
- [ ] **Step 1: Add minimal helpers**
|
||||
|
||||
```js
|
||||
function findInboxLink() { /* find the 163 inbox entry */ }
|
||||
async function returnToInbox() { /* restore inbox list view */ }
|
||||
function readOpenedMailText() { /* read visible detail text and iframe text */ }
|
||||
async function openMailAndGetMessageText(item) { /* open, read, return */ }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update `handlePollEmail`**
|
||||
|
||||
```js
|
||||
if (!code) {
|
||||
const openedText = await openMailAndGetMessageText(item);
|
||||
const bodyCode = extractVerificationCode(openedText);
|
||||
if (bodyCode) {
|
||||
return { ok: true, code: bodyCode, emailTimestamp: Date.now(), mailId: id };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the targeted test**
|
||||
|
||||
Run: `node --test tests/mail-163-content.test.js`
|
||||
Expected: PASS
|
||||
|
||||
### Task 3: Verify no regressions in adjacent polling logic
|
||||
|
||||
**Files:**
|
||||
- Test: `D:\github\codex-oauth-automation-extension-Pro2.0\tests\mail-163-content.test.js`
|
||||
- Test: `D:\github\codex-oauth-automation-extension-Pro2.0\tests\verification-flow-polling.test.js`
|
||||
|
||||
- [ ] **Step 1: Run the local regression slice**
|
||||
|
||||
Run: `node --test tests/mail-163-content.test.js tests/verification-flow-polling.test.js`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 2: Update docs only if behavior scope changed**
|
||||
|
||||
```md
|
||||
No root docs update is required if the change stays inside existing 163 polling behavior and does not alter the documented top-level flow.
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
# 163 Mail Body Fallback Design
|
||||
|
||||
## Goal
|
||||
|
||||
Make the `163` and `163-vip` mailbox polling flow able to read verification codes from the opened email body when the inbox list does not expose the six-digit code.
|
||||
|
||||
## Existing Problem
|
||||
|
||||
- `content/mail-163.js` currently matches candidate emails from the inbox list only.
|
||||
- It extracts the code from the row subject text and the row `aria-label`.
|
||||
- If the subject line only says something like "你的临时 ChatGPT 登录代码" and the row metadata does not include the code, polling fails even when the opened email body clearly shows the code.
|
||||
|
||||
## Approved Scope
|
||||
|
||||
- Add a body-reading fallback for `163` and `163-vip`.
|
||||
- Keep the current list-based fast path.
|
||||
- Do not add `targetEmail` filtering in this change.
|
||||
- Keep cleanup as best effort only.
|
||||
|
||||
## Design Summary
|
||||
|
||||
`content/mail-163.js` will move from a single-stage detector to a two-stage detector:
|
||||
|
||||
1. Scan inbox rows exactly as today.
|
||||
2. If a matching row does not contain a code in its subject or `aria-label`, open that email.
|
||||
3. Read visible detail text from the opened mail view, including same-origin iframe content when available.
|
||||
4. Extract the six-digit code from the opened content.
|
||||
5. Return to the inbox before continuing.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Row-first detection stays in place
|
||||
|
||||
The existing sender, subject, time-window, and seen-code checks stay as the first filter because they are cheap and already fit the current provider flow.
|
||||
|
||||
### Opened-mail fallback is local to `content/mail-163.js`
|
||||
|
||||
The background verification flow does not need to change. The mailbox content script already owns the provider-specific polling behavior, so the new logic should stay inside the 163 content script.
|
||||
|
||||
### No target-email filtering in this revision
|
||||
|
||||
Step 8 already passes `targetEmail` through the background payload, but this revision intentionally leaves that field unused for 163. The goal is to fix the missing body fallback without widening the scope of behavior changes.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If opening a candidate mail does not reveal readable detail text, polling should continue to the next candidate or next round.
|
||||
- If the helper opens a mail successfully, it should try to return to the inbox before continuing.
|
||||
- Cleanup and deletion remain best effort and must not block successful code extraction.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Add a focused regression test for `content/mail-163.js` that proves:
|
||||
|
||||
- a matching 163 inbox row without an inline code does not succeed from list text alone
|
||||
- the script opens the row
|
||||
- the script reads the opened body text
|
||||
- the code extracted from the body is returned to the background flow
|
||||
@@ -0,0 +1,148 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('content/mail-163.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);
|
||||
}
|
||||
|
||||
test('handlePollEmail opens a matching 163 message and reads the body when the list row has no inline code', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const seenCodes = new Set();
|
||||
const openedMailIds = [];
|
||||
let state = 'empty';
|
||||
const now = Date.now();
|
||||
|
||||
const inboxLink = {
|
||||
click() {
|
||||
state = 'ready';
|
||||
},
|
||||
};
|
||||
|
||||
const mailItem = {
|
||||
getAttribute(name) {
|
||||
if (name === 'id') return 'mail-1';
|
||||
if (name === 'aria-label') return 'OpenAI 发件人 你的临时 ChatGPT 登录代码';
|
||||
return '';
|
||||
},
|
||||
querySelector(selector) {
|
||||
if (selector === '.nui-user') return { textContent: 'OpenAI' };
|
||||
if (selector === 'span.da0') return { textContent: '你的临时 ChatGPT 登录代码' };
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
function findMailItems() {
|
||||
return state === 'ready' ? [mailItem] : [];
|
||||
}
|
||||
|
||||
function getCurrentMailIds() {
|
||||
return new Set(findMailItems().map((item) => item.getAttribute('id')));
|
||||
}
|
||||
|
||||
function normalizeMinuteTimestamp(timestamp) {
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) return 0;
|
||||
const date = new Date(timestamp);
|
||||
date.setSeconds(0, 0);
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
function getMailTimestamp() {
|
||||
return now;
|
||||
}
|
||||
|
||||
async function waitForElement() {
|
||||
return inboxLink;
|
||||
}
|
||||
|
||||
async function refreshInbox() {
|
||||
state = 'ready';
|
||||
}
|
||||
|
||||
async function sleep() {}
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
const match = String(text || '').match(/(\\d{6})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
async function openMailAndGetMessageText(item) {
|
||||
openedMailIds.push(item.getAttribute('id'));
|
||||
return '输入此临时验证码以继续:480382';
|
||||
}
|
||||
|
||||
function persistSeenCodes() {}
|
||||
function scheduleEmailCleanup() {}
|
||||
function log() {}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
handlePollEmail,
|
||||
getOpenedMailIds() {
|
||||
return openedMailIds.slice();
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.handlePollEmail(8, {
|
||||
senderFilters: ['openai'],
|
||||
subjectFilters: ['chatgpt'],
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
filterAfterTimestamp: Date.now(),
|
||||
});
|
||||
|
||||
assert.equal(result.code, '480382');
|
||||
assert.deepEqual(api.getOpenedMailIds(), ['mail-1']);
|
||||
});
|
||||
+14
@@ -471,6 +471,20 @@
|
||||
- [icloud-utils.js](c:/Users/projectf/Downloads/codex注册扩展/icloud-utils.js)
|
||||
- [content/icloud-mail.js](c:/Users/projectf/Downloads/codex注册扩展/content/icloud-mail.js)
|
||||
|
||||
### 7.5 163 / 163 VIP
|
||||
|
||||
组成:
|
||||
|
||||
- [content/mail-163.js](c:/Users/projectf/Downloads/codex注册扩展/content/mail-163.js)
|
||||
|
||||
当前行为:
|
||||
|
||||
1. 先在 163 收件箱列表中按发件人、主题、`aria-label` 和时间窗口筛选候选邮件
|
||||
2. 如果列表文本已经直接包含 6 位验证码,则直接返回验证码
|
||||
3. 如果列表文本未直接包含验证码,则自动打开候选邮件正文
|
||||
4. 从正文可见文本与同源 iframe 文本中提取验证码
|
||||
5. 读取后回到收件箱列表,继续后续轮询或清理流程
|
||||
|
||||
## 8. 自动运行完整链路
|
||||
|
||||
文件:
|
||||
|
||||
+4
-1
@@ -73,7 +73,7 @@
|
||||
- `content/gmail-mail.js`:Gmail 邮箱轮询脚本,负责在 Gmail 页面中匹配验证码邮件。
|
||||
- `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
|
||||
- `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。
|
||||
- `content/mail-163.js`:163 / 163 VIP 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。
|
||||
- `content/mail-163.js`:163 / 163 VIP 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理;当前会先扫描收件箱列表中的发件人/主题/`aria-label`,若列表未直接暴露验证码,则会打开候选邮件正文读取验证码后再回到列表继续轮询。
|
||||
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱收件轮询、按步骤会话隔离“已试验证码”、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理。
|
||||
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
|
||||
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击。
|
||||
@@ -95,7 +95,9 @@
|
||||
- `docs/images/支付宝.jpg`:README 中展示的支付宝收款码图片。
|
||||
- `docs/refactor/2026-04-16-architecture-refactor-plan.md`:本轮重构阶段的结构设计与迁移计划记录。
|
||||
- `docs/superpowers/plans/2026-04-10-hotmail-oauth-mail-pool.md`:Hotmail OAuth 邮箱池实现计划文档。
|
||||
- `docs/superpowers/plans/2026-04-19-163-mail-body-fallback.md`:163 邮箱“列表无验证码时回退读取正文”的实现计划文档。
|
||||
- `docs/superpowers/specs/2026-04-10-hotmail-oauth-design.md`:Hotmail OAuth + Graph Mail 的设计规格文档。
|
||||
- `docs/superpowers/specs/2026-04-19-163-mail-body-fallback-design.md`:163 邮箱正文兜底读取验证码的设计说明文档。
|
||||
|
||||
## `icons/`
|
||||
|
||||
@@ -155,6 +157,7 @@
|
||||
- `tests/hotmail-cors-headers.test.js`:测试 Microsoft token 请求相关 DNR 配置。
|
||||
- `tests/hotmail-utils.test.js`:测试 Hotmail 工具层的账号筛选、验证码提取和消息匹配逻辑。
|
||||
- `tests/icloud-mail-content.test.js`:测试 iCloud Mail 内容脚本读取邮件正文和选中状态逻辑。
|
||||
- `tests/mail-163-content.test.js`:测试 163 邮箱内容脚本在列表无验证码时会打开邮件正文读取验证码。
|
||||
- `tests/icloud-utils.test.js`:测试 iCloud 工具层的 host、别名列表、保留状态与筛选逻辑。
|
||||
- `tests/luckmail-utils.test.js`:测试 LuckMail 工具层的购买记录、邮件、游标和验证码匹配逻辑。
|
||||
- `tests/mail-2925-content.test.js`:测试 2925 邮箱内容脚本的轮询快照、忽略邮箱对比、按步骤会话隔离已试验证码、单封邮件删除与整箱删除逻辑。
|
||||
|
||||
Reference in New Issue
Block a user