it ok
This commit is contained in:
+220
-97
@@ -1,23 +1,14 @@
|
|||||||
// content/mail-163.js — Content script for 163 Mail (steps 4, 7)
|
// content/mail-163.js - Content script for 163 Mail (steps 4, 7)
|
||||||
// Injected on: mail.163.com
|
// Injected on: mail.163.com / webmail.vip.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
|
|
||||||
|
|
||||||
const MAIL163_PREFIX = '[MultiPage:mail-163]';
|
const MAIL163_PREFIX = '[MultiPage:mail-163]';
|
||||||
const isTopFrame = window === window.top;
|
const isTopFrame = window === window.top;
|
||||||
|
|
||||||
console.log(MAIL163_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
|
console.log(MAIL163_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
|
||||||
|
|
||||||
// Only operate in the top frame
|
|
||||||
if (!isTopFrame) {
|
if (!isTopFrame) {
|
||||||
console.log(MAIL163_PREFIX, 'Skipping child frame');
|
console.log(MAIL163_PREFIX, 'Skipping child frame');
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Track codes we've already seen — persisted in chrome.storage.session to survive script re-injection
|
|
||||||
let seenCodes = new Set();
|
let seenCodes = new Set();
|
||||||
|
|
||||||
async function loadSeenCodes() {
|
async function loadSeenCodes() {
|
||||||
@@ -32,7 +23,6 @@ async function loadSeenCodes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load previously seen codes on startup
|
|
||||||
loadSeenCodes();
|
loadSeenCodes();
|
||||||
|
|
||||||
async function persistSeenCodes() {
|
async function persistSeenCodes() {
|
||||||
@@ -43,39 +33,41 @@ async function persistSeenCodes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Message Handler (top frame only)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (message.type === 'POLL_EMAIL') {
|
if (message.type !== 'POLL_EMAIL') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
resetStopState();
|
resetStopState();
|
||||||
handlePollEmail(message.step, message.payload).then(result => {
|
handlePollEmail(message.step, message.payload)
|
||||||
|
.then((result) => {
|
||||||
sendResponse(result);
|
sendResponse(result);
|
||||||
}).catch(err => {
|
})
|
||||||
|
.catch((err) => {
|
||||||
if (isStopError(err)) {
|
if (isStopError(err)) {
|
||||||
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
|
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
|
||||||
sendResponse({ stopped: true, error: err.message });
|
sendResponse({ stopped: true, error: err.message });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
|
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
|
||||||
sendResponse({ error: err.message });
|
sendResponse({ error: err.message });
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================
|
function normalizeText(value) {
|
||||||
// Find mail items
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||||
// ============================================================
|
}
|
||||||
|
|
||||||
function findMailItems() {
|
function findMailItems() {
|
||||||
return document.querySelectorAll('div[sign="letter"]');
|
return document.querySelectorAll('div[sign="letter"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentMailIds() {
|
function getCurrentMailIds(items = findMailItems()) {
|
||||||
const ids = new Set();
|
const ids = new Set();
|
||||||
findMailItems().forEach(item => {
|
Array.from(items).forEach((item) => {
|
||||||
const id = item.getAttribute('id') || '';
|
const id = item.getAttribute('id') || '';
|
||||||
if (id) ids.add(id);
|
if (id) ids.add(id);
|
||||||
});
|
});
|
||||||
@@ -90,7 +82,7 @@ function normalizeMinuteTimestamp(timestamp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseMail163Timestamp(rawText) {
|
function parseMail163Timestamp(rawText) {
|
||||||
const text = (rawText || '').replace(/\s+/g, ' ').trim();
|
const text = normalizeText(rawText);
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
|
|
||||||
let match = text.match(/(\d{4})年(\d{1,2})月(\d{1,2})日\s+(\d{1,2}):(\d{2})/);
|
let match = text.match(/(\d{4})年(\d{1,2})月(\d{1,2})日\s+(\d{1,2}):(\d{2})/);
|
||||||
@@ -131,8 +123,7 @@ function getMailTimestamp(item) {
|
|||||||
if (timeCell?.getAttribute('title')) candidates.push(timeCell.getAttribute('title'));
|
if (timeCell?.getAttribute('title')) candidates.push(timeCell.getAttribute('title'));
|
||||||
if (timeCell?.textContent) candidates.push(timeCell.textContent);
|
if (timeCell?.textContent) candidates.push(timeCell.textContent);
|
||||||
|
|
||||||
const titledNodes = item.querySelectorAll('[title]');
|
item.querySelectorAll('[title]').forEach((node) => {
|
||||||
titledNodes.forEach((node) => {
|
|
||||||
const title = node.getAttribute('title');
|
const title = node.getAttribute('title');
|
||||||
if (title) candidates.push(title);
|
if (title) candidates.push(title);
|
||||||
});
|
});
|
||||||
@@ -153,12 +144,135 @@ function scheduleEmailCleanup(item, step) {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
function findInboxLink() {
|
||||||
// Email Polling
|
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;
|
||||||
|
}
|
||||||
|
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 || '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!texts.length) {
|
||||||
|
pushText(document.body?.innerText || document.body?.textContent || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
texts.sort((left, right) => right.length - left.length);
|
||||||
|
return texts[0] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function returnToInbox() {
|
||||||
|
if (findMailItems().length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
async function handlePollEmail(step, payload) {
|
||||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [], filterAfterTimestamp = 0 } = payload;
|
const {
|
||||||
|
senderFilters,
|
||||||
|
subjectFilters,
|
||||||
|
maxAttempts,
|
||||||
|
intervalMs,
|
||||||
|
excludeCodes = [],
|
||||||
|
filterAfterTimestamp = 0,
|
||||||
|
} = payload;
|
||||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||||
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
|
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
|
||||||
|
|
||||||
@@ -167,7 +281,6 @@ async function handlePollEmail(step, payload) {
|
|||||||
log(`步骤 ${step}:仅尝试 ${new Date(filterAfterMinute).toLocaleString('zh-CN', { hour12: false })} 及之后时间的邮件。`);
|
log(`步骤 ${step}:仅尝试 ${new Date(filterAfterMinute).toLocaleString('zh-CN', { hour12: false })} 及之后时间的邮件。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click inbox in sidebar to ensure we're in inbox view
|
|
||||||
log(`步骤 ${step}:正在等待侧边栏加载...`);
|
log(`步骤 ${step}:正在等待侧边栏加载...`);
|
||||||
try {
|
try {
|
||||||
const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000);
|
const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000);
|
||||||
@@ -177,10 +290,9 @@ async function handlePollEmail(step, payload) {
|
|||||||
log(`步骤 ${step}:未找到收件箱入口,继续尝试后续流程...`, 'warn');
|
log(`步骤 ${step}:未找到收件箱入口,继续尝试后续流程...`, 'warn');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for mail list to appear
|
|
||||||
log(`步骤 ${step}:正在等待邮件列表加载...`);
|
log(`步骤 ${step}:正在等待邮件列表加载...`);
|
||||||
let items = [];
|
let items = [];
|
||||||
for (let i = 0; i < 20; i++) {
|
for (let i = 0; i < 20; i += 1) {
|
||||||
items = findMailItems();
|
items = findMailItems();
|
||||||
if (items.length > 0) break;
|
if (items.length > 0) break;
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
@@ -198,13 +310,12 @@ async function handlePollEmail(step, payload) {
|
|||||||
|
|
||||||
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
|
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
|
||||||
|
|
||||||
// Snapshot existing mail IDs
|
const existingMailIds = getCurrentMailIds(items);
|
||||||
const existingMailIds = getCurrentMailIds();
|
|
||||||
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
|
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
|
||||||
|
|
||||||
const FALLBACK_AFTER = 3;
|
const FALLBACK_AFTER = 3;
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||||
log(`步骤 ${step}:正在轮询 163 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
log(`步骤 ${step}:正在轮询 163 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
||||||
|
|
||||||
if (attempt > 1) {
|
if (attempt > 1) {
|
||||||
@@ -226,42 +337,68 @@ async function handlePollEmail(step, payload) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!useFallback && !shouldBypassOldSnapshot && existingMailIds.has(id)) continue;
|
if (!useFallback && !shouldBypassOldSnapshot && existingMailIds.has(id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const senderEl = item.querySelector('.nui-user');
|
const sender = normalizeText(item.querySelector('.nui-user')?.textContent || '').toLowerCase();
|
||||||
const sender = senderEl ? senderEl.textContent.toLowerCase() : '';
|
const subject = normalizeText(item.querySelector('span.da0')?.textContent || '');
|
||||||
|
const ariaLabel = normalizeText(item.getAttribute('aria-label') || '').toLowerCase();
|
||||||
|
|
||||||
const subjectEl = item.querySelector('span.da0');
|
const senderMatch = senderFilters.some((filter) => sender.includes(String(filter || '').toLowerCase()) || ariaLabel.includes(String(filter || '').toLowerCase()));
|
||||||
const subject = subjectEl ? subjectEl.textContent : '';
|
const subjectMatch = subjectFilters.some((filter) => subject.toLowerCase().includes(String(filter || '').toLowerCase()) || ariaLabel.includes(String(filter || '').toLowerCase()));
|
||||||
|
|
||||||
const ariaLabel = (item.getAttribute('aria-label') || '').toLowerCase();
|
if (!senderMatch && !subjectMatch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
|
const previewCode = extractVerificationCode(`${subject} ${ariaLabel}`);
|
||||||
const subjectMatch = subjectFilters.some(f => subject.toLowerCase().includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
|
if (previewCode) {
|
||||||
|
if (excludedCodeSet.has(previewCode)) {
|
||||||
|
log(`步骤 ${step}:跳过排除的验证码:${previewCode}`, 'info');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (seenCodes.has(previewCode)) {
|
||||||
|
log(`步骤 ${step}:跳过已处理过的验证码:${previewCode}`, 'info');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (senderMatch || subjectMatch) {
|
seenCodes.add(previewCode);
|
||||||
const code = extractVerificationCode(subject + ' ' + ariaLabel);
|
|
||||||
if (code && excludedCodeSet.has(code)) {
|
|
||||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
|
||||||
} else if (code && !seenCodes.has(code)) {
|
|
||||||
seenCodes.add(code);
|
|
||||||
persistSeenCodes();
|
persistSeenCodes();
|
||||||
const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件' : '新邮件';
|
const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件' : '新邮件';
|
||||||
const timeLabel = mailTimestamp ? `,时间:${new Date(mailTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
|
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);
|
scheduleEmailCleanup(item, step);
|
||||||
|
return { ok: true, code: previewCode, emailTimestamp: Date.now(), mailId: id };
|
||||||
|
}
|
||||||
|
|
||||||
return { ok: true, code, emailTimestamp: Date.now(), mailId: id };
|
const openedText = await openMailAndGetMessageText(item);
|
||||||
} else if (code && seenCodes.has(code)) {
|
const bodyCode = extractVerificationCode(openedText);
|
||||||
log(`步骤 ${step}:跳过已处理过的验证码:${code}`, 'info');
|
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) {
|
if (attempt === FALLBACK_AFTER + 1) {
|
||||||
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
|
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件。`, 'warn');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempt < maxAttempts) {
|
if (attempt < maxAttempts) {
|
||||||
@@ -275,30 +412,22 @@ async function handlePollEmail(step, payload) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Delete Email via Hover Trash / Toolbar Fallback
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
async function deleteEmail(item, step) {
|
async function deleteEmail(item, step) {
|
||||||
try {
|
try {
|
||||||
log(`步骤 ${step}:正在删除邮件...`);
|
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('mouseover', { bubbles: true }));
|
||||||
item.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
item.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
|
|
||||||
const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]');
|
const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]');
|
||||||
if (trashIcon) {
|
if (trashIcon) {
|
||||||
trashIcon.click();
|
simulateClick(trashIcon);
|
||||||
log(`步骤 ${step}:已点击删除图标`, 'ok');
|
log(`步骤 ${step}:已点击删除图标`, 'ok');
|
||||||
await sleep(1500);
|
await sleep(1500);
|
||||||
|
|
||||||
// Check if item disappeared (confirm deletion)
|
const stillExists = item.id ? document.getElementById(item.id) : item;
|
||||||
const stillExists = document.getElementById(item.id);
|
if (!stillExists || stillExists.style?.display === 'none') {
|
||||||
if (!stillExists || stillExists.style.display === 'none') {
|
|
||||||
log(`步骤 ${step}:邮件已成功删除`);
|
log(`步骤 ${step}:邮件已成功删除`);
|
||||||
} else {
|
} else {
|
||||||
log(`步骤 ${step}:邮件可能尚未删除,列表中仍可见`, 'warn');
|
log(`步骤 ${step}:邮件可能尚未删除,列表中仍可见`, 'warn');
|
||||||
@@ -306,18 +435,16 @@ async function deleteEmail(item, step) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy 2: Select checkbox then click toolbar delete button
|
|
||||||
log(`步骤 ${step}:未找到删除图标,尝试使用复选框加工具栏删除...`);
|
log(`步骤 ${step}:未找到删除图标,尝试使用复选框加工具栏删除...`);
|
||||||
const checkbox = item.querySelector('[sign="checkbox"], .nui-chk');
|
const checkbox = item.querySelector('[sign="checkbox"], .nui-chk');
|
||||||
if (checkbox) {
|
if (checkbox) {
|
||||||
checkbox.click();
|
simulateClick(checkbox);
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
|
|
||||||
// Click toolbar delete button
|
|
||||||
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
|
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
|
||||||
for (const btn of toolbarBtns) {
|
for (const btn of toolbarBtns) {
|
||||||
if (btn.textContent.replace(/\s/g, '').includes('删除')) {
|
if (normalizeText(btn.textContent || '').includes('删除')) {
|
||||||
btn.closest('.nui-btn').click();
|
simulateClick(btn.closest('.nui-btn') || btn);
|
||||||
log(`步骤 ${step}:已点击工具栏删除`, 'ok');
|
log(`步骤 ${step}:已点击工具栏删除`, 'ok');
|
||||||
await sleep(1500);
|
await sleep(1500);
|
||||||
return;
|
return;
|
||||||
@@ -331,51 +458,47 @@ async function deleteEmail(item, step) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Inbox Refresh
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
async function refreshInbox() {
|
async function refreshInbox() {
|
||||||
// Try toolbar "刷 新" button
|
|
||||||
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
|
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
|
||||||
for (const btn of toolbarBtns) {
|
for (const btn of toolbarBtns) {
|
||||||
if (btn.textContent.replace(/\s/g, '') === '刷新') {
|
if (normalizeText(btn.textContent || '') === '刷新') {
|
||||||
btn.closest('.nui-btn').click();
|
simulateClick(btn.closest('.nui-btn') || btn);
|
||||||
console.log(MAIL163_PREFIX, 'Clicked "刷新" button');
|
console.log(MAIL163_PREFIX, 'Clicked refresh button');
|
||||||
await sleep(800);
|
await sleep(800);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: click sidebar "收 信"
|
const receiveButtons = document.querySelectorAll('.ra0');
|
||||||
const shouXinBtns = document.querySelectorAll('.ra0');
|
for (const btn of receiveButtons) {
|
||||||
for (const btn of shouXinBtns) {
|
if (normalizeText(btn.textContent || '').includes('收信')) {
|
||||||
if (btn.textContent.replace(/\s/g, '').includes('收信')) {
|
simulateClick(btn);
|
||||||
btn.click();
|
console.log(MAIL163_PREFIX, 'Clicked receive button');
|
||||||
console.log(MAIL163_PREFIX, 'Clicked "收信" button');
|
|
||||||
await sleep(800);
|
await sleep(800);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inboxLink = findInboxLink();
|
||||||
|
if (inboxLink) {
|
||||||
|
simulateClick(inboxLink);
|
||||||
|
await sleep(800);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(MAIL163_PREFIX, 'Could not find refresh button');
|
console.log(MAIL163_PREFIX, 'Could not find refresh button');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Verification Code Extraction
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
function extractVerificationCode(text) {
|
function extractVerificationCode(text) {
|
||||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
const matchCn = String(text || '').match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||||
if (matchCn) return matchCn[1];
|
if (matchCn) return matchCn[1];
|
||||||
|
|
||||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
const matchEn = String(text || '').match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||||
if (matchEn) return matchEn[1] || matchEn[2];
|
if (matchEn) return matchEn[1] || matchEn[2];
|
||||||
|
|
||||||
const match6 = text.match(/\b(\d{6})\b/);
|
const match6 = String(text || '').match(/\b(\d{6})\b/);
|
||||||
if (match6) return match6[1];
|
if (match6) return match6[1];
|
||||||
|
|
||||||
return null;
|
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)
|
- [icloud-utils.js](c:/Users/projectf/Downloads/codex注册扩展/icloud-utils.js)
|
||||||
- [content/icloud-mail.js](c:/Users/projectf/Downloads/codex注册扩展/content/icloud-mail.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. 自动运行完整链路
|
## 8. 自动运行完整链路
|
||||||
|
|
||||||
文件:
|
文件:
|
||||||
|
|||||||
+4
-1
@@ -73,7 +73,7 @@
|
|||||||
- `content/gmail-mail.js`:Gmail 邮箱轮询脚本,负责在 Gmail 页面中匹配验证码邮件。
|
- `content/gmail-mail.js`:Gmail 邮箱轮询脚本,负责在 Gmail 页面中匹配验证码邮件。
|
||||||
- `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
|
- `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
|
||||||
- `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。
|
- `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/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱收件轮询、按步骤会话隔离“已试验证码”、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理。
|
||||||
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
|
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
|
||||||
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击。
|
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击。
|
||||||
@@ -95,7 +95,9 @@
|
|||||||
- `docs/images/支付宝.jpg`:README 中展示的支付宝收款码图片。
|
- `docs/images/支付宝.jpg`:README 中展示的支付宝收款码图片。
|
||||||
- `docs/refactor/2026-04-16-architecture-refactor-plan.md`:本轮重构阶段的结构设计与迁移计划记录。
|
- `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-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-10-hotmail-oauth-design.md`:Hotmail OAuth + Graph Mail 的设计规格文档。
|
||||||
|
- `docs/superpowers/specs/2026-04-19-163-mail-body-fallback-design.md`:163 邮箱正文兜底读取验证码的设计说明文档。
|
||||||
|
|
||||||
## `icons/`
|
## `icons/`
|
||||||
|
|
||||||
@@ -155,6 +157,7 @@
|
|||||||
- `tests/hotmail-cors-headers.test.js`:测试 Microsoft token 请求相关 DNR 配置。
|
- `tests/hotmail-cors-headers.test.js`:测试 Microsoft token 请求相关 DNR 配置。
|
||||||
- `tests/hotmail-utils.test.js`:测试 Hotmail 工具层的账号筛选、验证码提取和消息匹配逻辑。
|
- `tests/hotmail-utils.test.js`:测试 Hotmail 工具层的账号筛选、验证码提取和消息匹配逻辑。
|
||||||
- `tests/icloud-mail-content.test.js`:测试 iCloud Mail 内容脚本读取邮件正文和选中状态逻辑。
|
- `tests/icloud-mail-content.test.js`:测试 iCloud Mail 内容脚本读取邮件正文和选中状态逻辑。
|
||||||
|
- `tests/mail-163-content.test.js`:测试 163 邮箱内容脚本在列表无验证码时会打开邮件正文读取验证码。
|
||||||
- `tests/icloud-utils.test.js`:测试 iCloud 工具层的 host、别名列表、保留状态与筛选逻辑。
|
- `tests/icloud-utils.test.js`:测试 iCloud 工具层的 host、别名列表、保留状态与筛选逻辑。
|
||||||
- `tests/luckmail-utils.test.js`:测试 LuckMail 工具层的购买记录、邮件、游标和验证码匹配逻辑。
|
- `tests/luckmail-utils.test.js`:测试 LuckMail 工具层的购买记录、邮件、游标和验证码匹配逻辑。
|
||||||
- `tests/mail-2925-content.test.js`:测试 2925 邮箱内容脚本的轮询快照、忽略邮箱对比、按步骤会话隔离已试验证码、单封邮件删除与整箱删除逻辑。
|
- `tests/mail-2925-content.test.js`:测试 2925 邮箱内容脚本的轮询快照、忽略邮箱对比、按步骤会话隔离已试验证码、单封邮件删除与整箱删除逻辑。
|
||||||
|
|||||||
Reference in New Issue
Block a user