merge: 合并 dev 并解决 PR 冲突
This commit is contained in:
@@ -47,7 +47,15 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
return /(?:认证失败|回调 URL 提交失败):\s*/i.test(text);
|
||||
if (/请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(text)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(text)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return /(?:认证失败|回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::]?\s*/i.test(text);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
isActionEnabled,
|
||||
isVisibleElement,
|
||||
log,
|
||||
routeErrorPattern = null,
|
||||
simulateClick,
|
||||
sleep,
|
||||
throwIfStopped,
|
||||
@@ -65,9 +66,13 @@
|
||||
const detailMatched = detailPattern instanceof RegExp
|
||||
? detailPattern.test(text)
|
||||
: false;
|
||||
const routeErrorMatched = routeErrorPattern instanceof RegExp
|
||||
? routeErrorPattern.test(text)
|
||||
: false;
|
||||
const maxCheckAttemptsBlocked = /max_check_attempts/i.test(text);
|
||||
const userAlreadyExistsBlocked = /user_already_exists/i.test(text);
|
||||
|
||||
if (!titleMatched && !detailMatched && !maxCheckAttemptsBlocked) {
|
||||
if (!titleMatched && !detailMatched && !routeErrorMatched && !maxCheckAttemptsBlocked && !userAlreadyExistsBlocked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -78,7 +83,9 @@
|
||||
retryEnabled: isActionEnabled(retryButton),
|
||||
titleMatched,
|
||||
detailMatched,
|
||||
routeErrorMatched,
|
||||
maxCheckAttemptsBlocked,
|
||||
userAlreadyExistsBlocked,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -148,6 +155,12 @@
|
||||
);
|
||||
}
|
||||
|
||||
if (retryState.userAlreadyExistsBlocked) {
|
||||
throw new Error(
|
||||
'SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。'
|
||||
);
|
||||
}
|
||||
|
||||
if (retryState.retryButton && retryState.retryEnabled) {
|
||||
idlePollCount = 0;
|
||||
clickCount += 1;
|
||||
@@ -199,6 +212,12 @@
|
||||
);
|
||||
}
|
||||
|
||||
if (finalRetryState.userAlreadyExistsBlocked) {
|
||||
throw new Error(
|
||||
'SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。'
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}`
|
||||
);
|
||||
|
||||
+366
-31
@@ -69,15 +69,141 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
// Find mail items
|
||||
// ============================================================
|
||||
|
||||
function findMailItems() {
|
||||
return document.querySelectorAll('div[sign="letter"]');
|
||||
function normalizeText(value) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function getCurrentMailIds() {
|
||||
function isVisibleNode(node) {
|
||||
if (!node) return false;
|
||||
if (node.hidden) return false;
|
||||
|
||||
const style = typeof window.getComputedStyle === 'function'
|
||||
? window.getComputedStyle(node)
|
||||
: null;
|
||||
if (style && (style.display === 'none' || style.visibility === 'hidden')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rect = typeof node.getBoundingClientRect === 'function'
|
||||
? node.getBoundingClientRect()
|
||||
: null;
|
||||
if (rect && rect.width <= 0 && rect.height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isLikelyMailItemNode(node) {
|
||||
if (!isVisibleNode(node)) {
|
||||
return false;
|
||||
}
|
||||
if (node.matches?.('div[sign="letter"]')) {
|
||||
return true;
|
||||
}
|
||||
if (node.querySelector?.('.nui-user, span.da0, [sign="trash"], [title="删除邮件"], [class*="subject"], [class*="sender"]')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const summaryText = normalizeText(
|
||||
node.getAttribute?.('aria-label')
|
||||
|| node.getAttribute?.('title')
|
||||
|| node.textContent
|
||||
);
|
||||
if (!summaryText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /发件人|验证码|verification|chatgpt|openai|code/i.test(summaryText);
|
||||
}
|
||||
|
||||
function findMailItems() {
|
||||
const selectorGroups = [
|
||||
'div[sign="letter"]',
|
||||
'[role="option"][aria-label]',
|
||||
'[role="listitem"][aria-label]',
|
||||
'tr[aria-label]',
|
||||
'li[aria-label]',
|
||||
'div[aria-label]',
|
||||
];
|
||||
|
||||
for (const selector of selectorGroups) {
|
||||
const matches = Array.from(document.querySelectorAll(selector)).filter(isLikelyMailItemNode);
|
||||
if (matches.length > 0) {
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getMailTextBySelectors(item, selectors = []) {
|
||||
for (const selector of selectors) {
|
||||
const candidates = item.querySelectorAll(selector);
|
||||
for (const candidate of candidates) {
|
||||
const texts = [
|
||||
candidate.getAttribute?.('title'),
|
||||
candidate.getAttribute?.('aria-label'),
|
||||
candidate.textContent,
|
||||
];
|
||||
for (const text of texts) {
|
||||
const normalized = normalizeText(text);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getMailSenderText(item) {
|
||||
return getMailTextBySelectors(item, [
|
||||
'.nui-user',
|
||||
'[class*="sender"]',
|
||||
'[class*="from"]',
|
||||
'[data-sender]',
|
||||
]);
|
||||
}
|
||||
|
||||
function getMailSubjectText(item) {
|
||||
return getMailTextBySelectors(item, [
|
||||
'span.da0',
|
||||
'[class*="subject"]',
|
||||
'[data-subject]',
|
||||
'strong',
|
||||
]);
|
||||
}
|
||||
|
||||
function getMailRowText(item) {
|
||||
const ariaLabel = normalizeText(item.getAttribute('aria-label'));
|
||||
const sender = getMailSenderText(item);
|
||||
const subject = getMailSubjectText(item);
|
||||
const fullText = normalizeText(item.textContent || '');
|
||||
return normalizeText([ariaLabel, sender, subject, fullText].filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function getMailItemId(item, index = 0) {
|
||||
const candidates = [
|
||||
item.getAttribute('id'),
|
||||
item.getAttribute('data-id'),
|
||||
item.dataset?.id,
|
||||
item.getAttribute('data-key'),
|
||||
item.getAttribute('key'),
|
||||
].filter(Boolean);
|
||||
|
||||
if (candidates.length > 0) {
|
||||
return String(candidates[0]);
|
||||
}
|
||||
|
||||
return `${index}|${getMailRowText(item).slice(0, 240)}`;
|
||||
}
|
||||
|
||||
function getCurrentMailIds(items = []) {
|
||||
const ids = new Set();
|
||||
findMailItems().forEach(item => {
|
||||
const id = item.getAttribute('id') || '';
|
||||
if (id) ids.add(id);
|
||||
const sourceItems = items.length > 0 ? items : findMailItems();
|
||||
sourceItems.forEach((item, index) => {
|
||||
ids.add(getMailItemId(item, index));
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
@@ -90,7 +216,7 @@ function normalizeMinuteTimestamp(timestamp) {
|
||||
}
|
||||
|
||||
function parseMail163Timestamp(rawText) {
|
||||
const text = (rawText || '').replace(/\s+/g, ' ').trim();
|
||||
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})/);
|
||||
@@ -107,6 +233,37 @@ function parseMail163Timestamp(rawText) {
|
||||
).getTime();
|
||||
}
|
||||
|
||||
match = text.match(/今天\s*(\d{1,2}):(\d{2})/);
|
||||
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();
|
||||
}
|
||||
|
||||
match = text.match(/昨天\s*(\d{1,2}):(\d{2})/);
|
||||
if (match) {
|
||||
const [, hour, minute] = match;
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() - 1);
|
||||
return new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
Number(hour),
|
||||
Number(minute),
|
||||
0,
|
||||
0
|
||||
).getTime();
|
||||
}
|
||||
|
||||
match = text.match(/\b(\d{1,2}):(\d{2})\b/);
|
||||
if (match) {
|
||||
const [, hour, minute] = match;
|
||||
@@ -125,18 +282,48 @@ function parseMail163Timestamp(rawText) {
|
||||
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);
|
||||
function isLikelyMailTimestampText(value) {
|
||||
const text = normalizeText(value);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return /(\d{4}年\d{1,2}月\d{1,2}日\s+\d{1,2}:\d{2})|今天\s*\d{1,2}:\d{2}|昨天\s*\d{1,2}:\d{2}|\b\d{1,2}:\d{2}\b/.test(text);
|
||||
}
|
||||
|
||||
const titledNodes = item.querySelectorAll('[title]');
|
||||
titledNodes.forEach((node) => {
|
||||
const title = node.getAttribute('title');
|
||||
if (title) candidates.push(title);
|
||||
function collectMailTimestampCandidates(item) {
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
const pushCandidate = (value) => {
|
||||
const normalized = normalizeText(value);
|
||||
if (!normalized || !isLikelyMailTimestampText(normalized) || seen.has(normalized)) {
|
||||
return;
|
||||
}
|
||||
seen.add(normalized);
|
||||
candidates.push(normalized);
|
||||
};
|
||||
|
||||
const priorityNodes = item.querySelectorAll('.e00, [title], [aria-label], time, [class*="time"], [class*="date"]');
|
||||
priorityNodes.forEach((node) => {
|
||||
pushCandidate(node.getAttribute?.('title'));
|
||||
pushCandidate(node.getAttribute?.('aria-label'));
|
||||
pushCandidate(node.textContent);
|
||||
});
|
||||
|
||||
const textNodes = item.querySelectorAll('span, div, td, strong, b');
|
||||
textNodes.forEach((node) => {
|
||||
const text = normalizeText(node.textContent);
|
||||
if (text && text.length <= 24) {
|
||||
pushCandidate(text);
|
||||
}
|
||||
});
|
||||
|
||||
pushCandidate(item.getAttribute('aria-label'));
|
||||
pushCandidate(item.getAttribute('title'));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function getMailTimestamp(item) {
|
||||
const candidates = collectMailTimestampCandidates(item);
|
||||
for (const candidate of candidates) {
|
||||
const parsed = parseMail163Timestamp(candidate);
|
||||
if (parsed) return parsed;
|
||||
@@ -145,6 +332,134 @@ function getMailTimestamp(item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectOpenedMailTextCandidates() {
|
||||
const texts = [];
|
||||
const seen = new Set();
|
||||
const pushText = (value) => {
|
||||
const normalized = normalizeText(value);
|
||||
if (!normalized || seen.has(normalized)) {
|
||||
return;
|
||||
}
|
||||
seen.add(normalized);
|
||||
texts.push(normalized);
|
||||
};
|
||||
|
||||
const selectors = [
|
||||
'.readHtml',
|
||||
'[class*="readmail"]',
|
||||
'[class*="mailread"]',
|
||||
'[class*="mailBody"]',
|
||||
'[class*="mailbody"]',
|
||||
'[class*="mail-content"]',
|
||||
'[class*="mailContent"]',
|
||||
'[class*="mail-detail"]',
|
||||
'[class*="mailDetail"]',
|
||||
'[class*="detail"] [class*="content"]',
|
||||
'[class*="read"] [class*="content"]',
|
||||
'[role="main"]',
|
||||
];
|
||||
|
||||
selectors.forEach((selector) => {
|
||||
document.querySelectorAll(selector).forEach((node) => {
|
||||
pushText(node.innerText || node.textContent);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('iframe').forEach((frame) => {
|
||||
try {
|
||||
pushText(frame.contentDocument?.body?.innerText || frame.contentDocument?.body?.textContent);
|
||||
} catch {
|
||||
// Ignore cross-frame access errors and keep trying other candidates.
|
||||
}
|
||||
});
|
||||
|
||||
pushText(document.body?.innerText || document.body?.textContent);
|
||||
return texts.sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
function selectOpenedMailTextCandidate(item, candidates = [], options = {}) {
|
||||
const subject = normalizeText(getMailSubjectText(item)).toLowerCase();
|
||||
const sender = normalizeText(getMailSenderText(item)).toLowerCase();
|
||||
const excludedSet = new Set((options.excludedTexts || []).map((value) => normalizeText(value)));
|
||||
const allowExcludedFallback = options.allowExcludedFallback !== false;
|
||||
|
||||
const pickCandidate = (source) => source.find((candidate) => {
|
||||
const lower = candidate.toLowerCase();
|
||||
if (subject && lower.includes(subject)) {
|
||||
return true;
|
||||
}
|
||||
if (sender && lower.includes(sender)) {
|
||||
return true;
|
||||
}
|
||||
return Boolean(extractVerificationCode(candidate) && /chatgpt|openai|verification|验证码|login code/i.test(lower));
|
||||
}) || source[0] || '';
|
||||
|
||||
const filteredCandidates = candidates.filter((candidate) => !excludedSet.has(normalizeText(candidate)));
|
||||
const preferred = pickCandidate(filteredCandidates);
|
||||
if (preferred || !allowExcludedFallback) {
|
||||
return preferred;
|
||||
}
|
||||
|
||||
return pickCandidate(candidates);
|
||||
}
|
||||
|
||||
function readOpenedMailText(item, options = {}) {
|
||||
const candidates = collectOpenedMailTextCandidates();
|
||||
return selectOpenedMailTextCandidate(item, candidates, options);
|
||||
}
|
||||
|
||||
async function returnToInbox() {
|
||||
const inboxLink = document.querySelector('.nui-tree-item-text[title="收件箱"], [title="收件箱"]');
|
||||
if (inboxLink) {
|
||||
if (typeof simulateClick === 'function') {
|
||||
simulateClick(inboxLink);
|
||||
} else {
|
||||
inboxLink.click();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
if (findMailItems().length > 0) {
|
||||
return true;
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function openMailAndGetMessageText(item) {
|
||||
const beforeCandidates = collectOpenedMailTextCandidates();
|
||||
const beforeText = selectOpenedMailTextCandidate(item, beforeCandidates);
|
||||
if (typeof simulateClick === 'function') {
|
||||
simulateClick(item);
|
||||
} else {
|
||||
item.click();
|
||||
}
|
||||
|
||||
let openedText = '';
|
||||
for (let i = 0; i < 24; i += 1) {
|
||||
await sleep(250);
|
||||
const candidate = readOpenedMailText(item, {
|
||||
excludedTexts: beforeCandidates,
|
||||
allowExcludedFallback: false,
|
||||
});
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
openedText = candidate;
|
||||
if (extractVerificationCode(candidate)) {
|
||||
break;
|
||||
}
|
||||
if (candidate !== beforeText && candidate.length > beforeText.length + 24) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await returnToInbox();
|
||||
return openedText;
|
||||
}
|
||||
|
||||
function scheduleEmailCleanup(item, step) {
|
||||
setTimeout(() => {
|
||||
Promise.resolve(deleteEmail(item, step)).catch(() => {
|
||||
@@ -199,7 +514,7 @@ async function handlePollEmail(step, payload) {
|
||||
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
|
||||
|
||||
// Snapshot existing mail IDs
|
||||
const existingMailIds = getCurrentMailIds();
|
||||
const existingMailIds = getCurrentMailIds(items);
|
||||
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
|
||||
|
||||
const FALLBACK_AFTER = 3;
|
||||
@@ -215,38 +530,58 @@ async function handlePollEmail(step, payload) {
|
||||
const allItems = findMailItems();
|
||||
const useFallback = attempt > FALLBACK_AFTER;
|
||||
|
||||
for (const item of allItems) {
|
||||
const id = item.getAttribute('id') || '';
|
||||
for (let index = 0; index < allItems.length; index++) {
|
||||
const item = allItems[index];
|
||||
const id = getMailItemId(item, index);
|
||||
const mailTimestamp = getMailTimestamp(item);
|
||||
const mailMinute = normalizeMinuteTimestamp(mailTimestamp || 0);
|
||||
const passesTimeFilter = !filterAfterMinute || (mailMinute && mailMinute >= filterAfterMinute);
|
||||
const shouldBypassOldSnapshot = Boolean(filterAfterMinute && passesTimeFilter && mailMinute > 0);
|
||||
|
||||
if (!passesTimeFilter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!useFallback && !shouldBypassOldSnapshot && existingMailIds.has(id)) continue;
|
||||
if (!useFallback && existingMailIds.has(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const senderEl = item.querySelector('.nui-user');
|
||||
const sender = senderEl ? senderEl.textContent.toLowerCase() : '';
|
||||
const sender = getMailSenderText(item).toLowerCase();
|
||||
const subject = getMailSubjectText(item);
|
||||
const rowText = getMailRowText(item);
|
||||
const ariaLabel = normalizeText(item.getAttribute('aria-label')).toLowerCase();
|
||||
const combinedText = normalizeText([subject, ariaLabel, rowText].filter(Boolean).join(' '));
|
||||
|
||||
const subjectEl = item.querySelector('span.da0');
|
||||
const subject = subjectEl ? subjectEl.textContent : '';
|
||||
if (!mailTimestamp) {
|
||||
log(`步骤 ${step}:邮件 ${id.slice(0, 60)} 未读取到时间,已跳过时间窗口校验后的文本匹配阶段。`, 'info');
|
||||
}
|
||||
|
||||
const ariaLabel = (item.getAttribute('aria-label') || '').toLowerCase();
|
||||
|
||||
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 senderMatch = senderFilters.some((filter) => {
|
||||
const normalizedFilter = String(filter || '').toLowerCase();
|
||||
return normalizedFilter && (sender.includes(normalizedFilter) || ariaLabel.includes(normalizedFilter) || rowText.toLowerCase().includes(normalizedFilter));
|
||||
});
|
||||
const subjectMatch = subjectFilters.some((filter) => {
|
||||
const normalizedFilter = String(filter || '').toLowerCase();
|
||||
return normalizedFilter && (subject.toLowerCase().includes(normalizedFilter) || ariaLabel.includes(normalizedFilter) || rowText.toLowerCase().includes(normalizedFilter));
|
||||
});
|
||||
|
||||
if (senderMatch || subjectMatch) {
|
||||
const code = extractVerificationCode(subject + ' ' + ariaLabel);
|
||||
let code = extractVerificationCode(combinedText);
|
||||
let codeSource = '邮件列表';
|
||||
|
||||
if (!code) {
|
||||
const openedText = await openMailAndGetMessageText(item);
|
||||
code = extractVerificationCode(openedText);
|
||||
if (code) {
|
||||
codeSource = '邮件正文';
|
||||
}
|
||||
}
|
||||
|
||||
if (code && excludedCodeSet.has(code)) {
|
||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
||||
} else if (code && !seenCodes.has(code)) {
|
||||
seenCodes.add(code);
|
||||
persistSeenCodes();
|
||||
const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件' : '新邮件';
|
||||
const source = useFallback && existingMailIds.has(id) ? `回退匹配${codeSource}` : `新邮件${codeSource}`;
|
||||
const timeLabel = mailTimestamp ? `,时间:${new Date(mailTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
|
||||
log(`步骤 ${step}:已找到验证码:${code}(来源:${source}${timeLabel},主题:${subject.slice(0, 40)})`, 'ok');
|
||||
|
||||
|
||||
+555
-31
@@ -53,6 +53,10 @@ async function persistSeenCodes() {
|
||||
}
|
||||
|
||||
function buildSeenCodeSessionKey(step, payload = {}) {
|
||||
const explicitSessionKey = String(payload?.sessionKey || '').trim();
|
||||
if (explicitSessionKey) {
|
||||
return explicitSessionKey;
|
||||
}
|
||||
const timestamp = Number(payload?.filterAfterTimestamp);
|
||||
if (Number.isFinite(timestamp) && timestamp > 0) {
|
||||
return `${step}:${timestamp}`;
|
||||
@@ -77,6 +81,16 @@ async function ensureSeenCodesSession(step, payload = {}) {
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'ENSURE_MAIL2925_SESSION') {
|
||||
resetStopState();
|
||||
ensureMail2925Session(message.payload).then((result) => {
|
||||
sendResponse(result);
|
||||
}).catch((err) => {
|
||||
sendResponse({ error: err?.message || String(err || '2925 登录失败') });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'POLL_EMAIL') {
|
||||
resetStopState();
|
||||
handlePollEmail(message.step, message.payload).then((result) => {
|
||||
@@ -95,8 +109,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
}
|
||||
|
||||
if (message.type === 'DELETE_ALL_EMAILS') {
|
||||
Promise.resolve(deleteAllMailboxEmails(message.step)).catch(() => {});
|
||||
sendResponse({ ok: true });
|
||||
Promise.resolve(deleteAllMailboxEmails(message.step)).then((deleted) => {
|
||||
sendResponse({ ok: true, deleted });
|
||||
}).catch((err) => {
|
||||
sendResponse({ ok: false, error: err?.message || String(err || '删除邮件失败') });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -143,11 +160,76 @@ const MAIL_SELECT_ALL_SELECTORS = [
|
||||
'[class*="checkbox"]',
|
||||
];
|
||||
const MAIL_ACTION_CANDIDATE_SELECTORS = 'button, [role="button"], a, label, span, div';
|
||||
const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::';
|
||||
const MAIL2925_LOGIN_INPUT_SELECTORS = [
|
||||
'input[type="email"]',
|
||||
'input[name*="mail"]',
|
||||
'input[id*="mail"]',
|
||||
'input[name*="user"]',
|
||||
'input[id*="user"]',
|
||||
'input[placeholder*="邮箱"]',
|
||||
'input[placeholder*="账号"]',
|
||||
'input[placeholder*="用户名"]',
|
||||
'input[type="text"]',
|
||||
];
|
||||
const MAIL2925_PASSWORD_INPUT_SELECTORS = [
|
||||
'input[type="password"]',
|
||||
];
|
||||
const MAIL2925_LOGIN_BUTTON_SELECTORS = [
|
||||
'button[type="submit"]',
|
||||
'.ivu-btn-primary',
|
||||
'.el-button--primary',
|
||||
'[class*="login"]',
|
||||
'[class*="Login"]',
|
||||
];
|
||||
const MAIL2925_LOGIN_BUTTON_PATTERNS = [
|
||||
/登录/i,
|
||||
/login/i,
|
||||
/立即登录/i,
|
||||
/submit/i,
|
||||
];
|
||||
const MAIL2925_AGREEMENT_PATTERNS = [
|
||||
/我已阅读并同意/,
|
||||
/服务协议/,
|
||||
/隐私政策/,
|
||||
];
|
||||
const MAIL2925_REMEMBER_LOGIN_PATTERNS = [
|
||||
/30天内免登录/,
|
||||
/免登录/,
|
||||
/记住登录/,
|
||||
/保持登录/,
|
||||
];
|
||||
|
||||
function normalizeNodeText(value) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function buildMail2925LimitError(message = '') {
|
||||
const normalized = normalizeNodeText(message || '子邮箱已达上限邮箱') || '子邮箱已达上限邮箱';
|
||||
return new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${normalized}`);
|
||||
}
|
||||
|
||||
function getPageTextSample(limit = 4000) {
|
||||
return normalizeNodeText(document.body?.innerText || document.body?.textContent || '').slice(0, limit);
|
||||
}
|
||||
|
||||
function detectMail2925LimitMessage() {
|
||||
const text = getPageTextSample();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const match = text.match(/子邮箱.{0,12}已达上限|已达上限邮箱|子邮箱上限|邮箱已达上限/);
|
||||
return match ? match[0] : '';
|
||||
}
|
||||
|
||||
function throwIfMail2925LimitReached() {
|
||||
const limitMessage = detectMail2925LimitMessage();
|
||||
if (limitMessage) {
|
||||
throw buildMail2925LimitError(limitMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function isVisibleNode(node) {
|
||||
if (!node) return false;
|
||||
if (node.hidden) return false;
|
||||
@@ -201,6 +283,19 @@ function findActionBySelectors(selectors = []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findVisibleInputBySelectors(selectors = []) {
|
||||
for (const selector of selectors) {
|
||||
const candidates = document.querySelectorAll(selector);
|
||||
for (const candidate of candidates) {
|
||||
if (!isVisibleNode(candidate) || candidate.disabled || candidate.readOnly) {
|
||||
continue;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findToolbarActionButton(patterns = [], selectors = []) {
|
||||
const directMatch = findActionBySelectors(selectors);
|
||||
if (directMatch) {
|
||||
@@ -224,6 +319,29 @@ function findToolbarActionButton(patterns = [], selectors = []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLoginButton() {
|
||||
const directMatch = findActionBySelectors(MAIL2925_LOGIN_BUTTON_SELECTORS);
|
||||
if (directMatch) {
|
||||
return directMatch;
|
||||
}
|
||||
|
||||
const candidates = document.querySelectorAll(MAIL_ACTION_CANDIDATE_SELECTORS);
|
||||
for (const candidate of candidates) {
|
||||
const target = resolveActionTarget(candidate);
|
||||
if (!isVisibleNode(target) || isMailItemNode(target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const text = normalizeNodeText(target.innerText || target.textContent || '');
|
||||
const label = normalizeNodeText(target.getAttribute?.('aria-label') || target.getAttribute?.('title') || '');
|
||||
if (MAIL2925_LOGIN_BUTTON_PATTERNS.some((pattern) => pattern.test(text) || pattern.test(label))) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findRefreshButton() {
|
||||
return findToolbarActionButton([
|
||||
/刷新/i,
|
||||
@@ -246,6 +364,235 @@ function findSelectAllControl() {
|
||||
return findActionBySelectors(MAIL_SELECT_ALL_SELECTORS);
|
||||
}
|
||||
|
||||
function findMail2925LoginEmailInput() {
|
||||
const candidates = Array.from(document.querySelectorAll(MAIL2925_LOGIN_INPUT_SELECTORS.join(', ')));
|
||||
for (const candidate of candidates) {
|
||||
if (!isVisibleNode(candidate) || candidate.disabled || candidate.readOnly) {
|
||||
continue;
|
||||
}
|
||||
if (candidate.type === 'password') {
|
||||
continue;
|
||||
}
|
||||
const identifier = normalizeNodeText([
|
||||
candidate.name,
|
||||
candidate.id,
|
||||
candidate.placeholder,
|
||||
candidate.getAttribute?.('aria-label'),
|
||||
candidate.getAttribute?.('autocomplete'),
|
||||
].join(' ')).toLowerCase();
|
||||
if (/邮箱|账号|用户|mail|user|login/.test(identifier) || candidate.type === 'email') {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findMail2925LoginPasswordInput() {
|
||||
return findVisibleInputBySelectors(MAIL2925_PASSWORD_INPUT_SELECTORS);
|
||||
}
|
||||
|
||||
function findAgreementContainer() {
|
||||
const candidates = document.querySelectorAll('label, div, span, p, form');
|
||||
for (const candidate of candidates) {
|
||||
if (!isVisibleNode(candidate)) {
|
||||
continue;
|
||||
}
|
||||
const text = normalizeNodeText(candidate.innerText || candidate.textContent || '');
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
if (MAIL2925_AGREEMENT_PATTERNS.every((pattern) => pattern.test(text) || /我已阅读并同意/.test(text))) {
|
||||
return candidate;
|
||||
}
|
||||
if (/我已阅读并同意/.test(text) && (/服务协议/.test(text) || /隐私政策/.test(text))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isAgreementText(text = '') {
|
||||
const normalizedText = normalizeNodeText(text);
|
||||
if (!normalizedText || MAIL2925_REMEMBER_LOGIN_PATTERNS.some((pattern) => pattern.test(normalizedText))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /我已阅读并同意/.test(normalizedText)
|
||||
|| (/服务协议/.test(normalizedText) && /隐私政策/.test(normalizedText));
|
||||
}
|
||||
|
||||
function getCheckboxContextText(target) {
|
||||
if (!target) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const textParts = [];
|
||||
const candidates = [
|
||||
target,
|
||||
target.closest?.('label'),
|
||||
target.parentElement,
|
||||
target.parentElement?.parentElement,
|
||||
target.nextElementSibling,
|
||||
target.previousElementSibling,
|
||||
target.closest?.('label, div, span, p, li, form'),
|
||||
].filter(Boolean);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const text = normalizeNodeText(candidate.innerText || candidate.textContent || '');
|
||||
if (text) {
|
||||
textParts.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeNodeText(textParts.join(' '));
|
||||
}
|
||||
|
||||
function findAgreementCheckbox() {
|
||||
const genericCheckboxes = document.querySelectorAll('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox');
|
||||
for (const checkbox of genericCheckboxes) {
|
||||
const target = resolveActionTarget(checkbox);
|
||||
if (!isVisibleNode(target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const contextText = getCheckboxContextText(target);
|
||||
if (MAIL2925_REMEMBER_LOGIN_PATTERNS.some((pattern) => pattern.test(contextText))) {
|
||||
continue;
|
||||
}
|
||||
if (isAgreementText(contextText)) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
const agreementContainer = findAgreementContainer();
|
||||
if (agreementContainer) {
|
||||
const checkbox = agreementContainer.querySelector('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox');
|
||||
if (checkbox) {
|
||||
const target = resolveActionTarget(checkbox);
|
||||
const contextText = getCheckboxContextText(target);
|
||||
if (!MAIL2925_REMEMBER_LOGIN_PATTERNS.some((pattern) => pattern.test(contextText))) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
const nearbyCheckbox = agreementContainer.parentElement?.querySelector?.('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox');
|
||||
if (nearbyCheckbox) {
|
||||
const target = resolveActionTarget(nearbyCheckbox);
|
||||
const contextText = getCheckboxContextText(target);
|
||||
if (!MAIL2925_REMEMBER_LOGIN_PATTERNS.some((pattern) => pattern.test(contextText))) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function ensureAgreementChecked() {
|
||||
const checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox'))
|
||||
.map((checkbox) => resolveActionTarget(checkbox))
|
||||
.filter((target, index, list) => target && isVisibleNode(target) && list.indexOf(target) === index);
|
||||
|
||||
if (!checkboxes.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
for (const checkbox of checkboxes) {
|
||||
if (isCheckboxChecked(checkbox)) {
|
||||
continue;
|
||||
}
|
||||
simulateClick(checkbox);
|
||||
changed = true;
|
||||
await sleep(120);
|
||||
}
|
||||
|
||||
return changed || checkboxes.every((checkbox) => isCheckboxChecked(checkbox));
|
||||
}
|
||||
|
||||
function detectMail2925ViewState() {
|
||||
const limitMessage = detectMail2925LimitMessage();
|
||||
if (limitMessage) {
|
||||
return { view: 'limit', limitMessage };
|
||||
}
|
||||
|
||||
const mailboxEmail = getMail2925DisplayedMailboxEmail();
|
||||
if (findMailItems().length > 0 || mailboxEmail) {
|
||||
return { view: 'mailbox', limitMessage: '', mailboxEmail };
|
||||
}
|
||||
|
||||
if (findMail2925LoginPasswordInput() && findMail2925LoginEmailInput()) {
|
||||
return { view: 'login', limitMessage: '' };
|
||||
}
|
||||
|
||||
const pageText = getPageTextSample();
|
||||
if (/欢迎使用邮箱|登录|login/i.test(pageText) && /密码|password/i.test(pageText)) {
|
||||
return { view: 'login', limitMessage: '' };
|
||||
}
|
||||
|
||||
return { view: 'unknown', limitMessage: '' };
|
||||
}
|
||||
|
||||
function getMail2925DisplayedMailboxEmail() {
|
||||
const directSelectors = [
|
||||
'.right-header',
|
||||
'[class~="right-header"]',
|
||||
'[class*="right-header"]',
|
||||
'[class*="user"] [class*="mail"]',
|
||||
'[class*="user"] [class*="email"]',
|
||||
'[class*="account"] [class*="mail"]',
|
||||
'[class*="account"] [class*="email"]',
|
||||
'[class*="header"] [class*="mail"]',
|
||||
'[class*="header"] [class*="email"]',
|
||||
];
|
||||
|
||||
for (const selector of directSelectors) {
|
||||
const candidates = document.querySelectorAll(selector);
|
||||
for (const candidate of candidates) {
|
||||
if (!isVisibleNode(candidate) || isMailItemNode(candidate)) {
|
||||
continue;
|
||||
}
|
||||
const email = extractEmails(candidate.textContent || candidate.innerText || '')
|
||||
.find((value) => /@2925\.com$/i.test(String(value || '').trim())) || '';
|
||||
if (email) {
|
||||
return email;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const topCandidates = Array.from(document.querySelectorAll('body *'))
|
||||
.filter((node) => {
|
||||
if (!isVisibleNode(node) || isMailItemNode(node)) {
|
||||
return false;
|
||||
}
|
||||
const rect = typeof node.getBoundingClientRect === 'function'
|
||||
? node.getBoundingClientRect()
|
||||
: null;
|
||||
if (!rect) return false;
|
||||
return rect.top >= 0 && rect.top <= Math.max(window.innerHeight * 0.35, 280);
|
||||
})
|
||||
.map((node) => {
|
||||
const email = extractEmails(node.textContent || node.innerText || '')
|
||||
.find((value) => /@2925\.com$/i.test(String(value || '').trim())) || '';
|
||||
return { node, email };
|
||||
})
|
||||
.filter((entry) => entry.email);
|
||||
|
||||
if (!topCandidates.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
topCandidates.sort((left, right) => {
|
||||
const leftRect = left.node.getBoundingClientRect();
|
||||
const rightRect = right.node.getBoundingClientRect();
|
||||
if (leftRect.top !== rightRect.top) {
|
||||
return leftRect.top - rightRect.top;
|
||||
}
|
||||
return leftRect.left - rightRect.left;
|
||||
});
|
||||
|
||||
return topCandidates[0]?.email || '';
|
||||
}
|
||||
|
||||
function isCheckboxChecked(node) {
|
||||
const checkbox = node?.matches?.('input[type="checkbox"], [role="checkbox"]')
|
||||
? node
|
||||
@@ -344,6 +691,46 @@ function extractVerificationCode(text, strictChatGPTCodeOnly = false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractEmails(text = '') {
|
||||
const matches = String(text || '').match(/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi) || [];
|
||||
return [...new Set(matches.map((item) => item.toLowerCase()))];
|
||||
}
|
||||
|
||||
function emailMatchesTarget(candidate, targetEmail) {
|
||||
const normalizedCandidate = String(candidate || '').trim().toLowerCase();
|
||||
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
|
||||
return Boolean(normalizedCandidate && normalizedTarget && normalizedCandidate === normalizedTarget);
|
||||
}
|
||||
|
||||
function getTargetEmailMatchState(text, targetEmail) {
|
||||
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
|
||||
if (!normalizedTarget) {
|
||||
return { matches: true, hasExplicitEmail: false };
|
||||
}
|
||||
|
||||
const normalizedText = String(text || '').toLowerCase();
|
||||
if (normalizedText.includes(normalizedTarget)) {
|
||||
return { matches: true, hasExplicitEmail: true };
|
||||
}
|
||||
|
||||
const extractedEmails = extractEmails(normalizedText);
|
||||
if (!extractedEmails.length) {
|
||||
return { matches: true, hasExplicitEmail: false };
|
||||
}
|
||||
|
||||
return {
|
||||
matches: extractedEmails.some((candidate) => emailMatchesTarget(candidate, normalizedTarget)),
|
||||
hasExplicitEmail: true,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -474,6 +861,10 @@ async function openMailAndDeleteAfterRead(item, step) {
|
||||
async function deleteAllMailboxEmails(step) {
|
||||
try {
|
||||
await returnToInbox();
|
||||
const initialItems = findMailItems();
|
||||
if (initialItems.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const selectAllControl = findSelectAllControl();
|
||||
if (!selectAllControl) {
|
||||
@@ -491,8 +882,15 @@ async function deleteAllMailboxEmails(step) {
|
||||
}
|
||||
|
||||
simulateClick(deleteButton);
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
await sleep(250);
|
||||
if (findMailItems().length === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
await sleepRandom(200, 500);
|
||||
return true;
|
||||
return findMailItems().length === 0;
|
||||
} catch (err) {
|
||||
console.warn(MAIL2925_PREFIX, `Step ${step}: delete-all cleanup failed:`, err?.message || err);
|
||||
return false;
|
||||
@@ -500,6 +898,9 @@ async function deleteAllMailboxEmails(step) {
|
||||
}
|
||||
|
||||
async function refreshInbox() {
|
||||
if (typeof throwIfMail2925LimitReached === 'function') {
|
||||
throwIfMail2925LimitReached();
|
||||
}
|
||||
const refreshBtn = findRefreshButton();
|
||||
if (refreshBtn) {
|
||||
simulateClick(refreshBtn);
|
||||
@@ -514,6 +915,129 @@ async function refreshInbox() {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForMail2925View(targetView, timeoutMs = 45000) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt <= timeoutMs) {
|
||||
throwIfStopped();
|
||||
const currentState = detectMail2925ViewState();
|
||||
if (currentState.view === 'limit') {
|
||||
throw buildMail2925LimitError(currentState.limitMessage);
|
||||
}
|
||||
if (currentState.view === targetView) {
|
||||
return currentState;
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
return detectMail2925ViewState();
|
||||
}
|
||||
|
||||
async function ensureMail2925Session(payload = {}) {
|
||||
const email = String(payload?.email || '').trim();
|
||||
const password = String(payload?.password || '');
|
||||
const forceLogin = Boolean(payload?.forceLogin);
|
||||
const allowLoginWhenOnLoginPage = payload?.allowLoginWhenOnLoginPage !== false;
|
||||
log(`步骤 0:2925 登录态检查开始,当前地址 ${location.href},forceLogin=${forceLogin ? 'true' : 'false'}`, 'info');
|
||||
|
||||
for (let attempt = 0; attempt < 10; attempt += 1) {
|
||||
throwIfStopped();
|
||||
const currentState = detectMail2925ViewState();
|
||||
log(`步骤 0:2925 登录页状态探测,第 ${attempt + 1}/10 次,状态=${currentState.view},地址=${location.href}`, 'info');
|
||||
if (currentState.view === 'limit') {
|
||||
return {
|
||||
ok: false,
|
||||
loggedIn: false,
|
||||
currentView: 'limit',
|
||||
limitReached: true,
|
||||
limitMessage: currentState.limitMessage,
|
||||
};
|
||||
}
|
||||
if (currentState.view === 'mailbox' && !forceLogin) {
|
||||
return {
|
||||
ok: true,
|
||||
loggedIn: true,
|
||||
currentView: 'mailbox',
|
||||
mailboxEmail: currentState.mailboxEmail || '',
|
||||
};
|
||||
}
|
||||
if (currentState.view === 'login') {
|
||||
if (!forceLogin && !allowLoginWhenOnLoginPage) {
|
||||
return {
|
||||
ok: false,
|
||||
loggedIn: false,
|
||||
currentView: 'login',
|
||||
requiresLogin: true,
|
||||
mailboxEmail: '',
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
const loginState = detectMail2925ViewState();
|
||||
log(`步骤 0:2925 准备执行登录,当前状态=${loginState.view},地址=${location.href}`, 'info');
|
||||
if (loginState.view === 'mailbox') {
|
||||
return {
|
||||
ok: true,
|
||||
loggedIn: true,
|
||||
currentView: 'mailbox',
|
||||
mailboxEmail: loginState.mailboxEmail || '',
|
||||
};
|
||||
}
|
||||
if (loginState.view === 'limit') {
|
||||
return {
|
||||
ok: false,
|
||||
loggedIn: false,
|
||||
currentView: 'limit',
|
||||
limitReached: true,
|
||||
limitMessage: loginState.limitMessage,
|
||||
};
|
||||
}
|
||||
if (!forceLogin && !allowLoginWhenOnLoginPage && loginState.view === 'login') {
|
||||
return {
|
||||
ok: false,
|
||||
loggedIn: false,
|
||||
currentView: 'login',
|
||||
requiresLogin: true,
|
||||
mailboxEmail: '',
|
||||
};
|
||||
}
|
||||
|
||||
const emailInput = findMail2925LoginEmailInput();
|
||||
const passwordInput = findMail2925LoginPasswordInput();
|
||||
const loginButton = findLoginButton();
|
||||
if (!emailInput || !passwordInput || !loginButton) {
|
||||
throw new Error('2925:未识别到可用的登录表单,请确认当前页面处于 2925 登录页。');
|
||||
}
|
||||
if (!email || !password) {
|
||||
throw new Error('2925:当前账号缺少邮箱或密码,无法自动登录。');
|
||||
}
|
||||
|
||||
await ensureAgreementChecked();
|
||||
fillInput(emailInput, email);
|
||||
await sleep(150);
|
||||
fillInput(passwordInput, password);
|
||||
await sleep(200);
|
||||
await sleep(1000);
|
||||
log(`步骤 0:2925 已定位到登录表单,准备点击“登录”,当前地址 ${location.href}`, 'info');
|
||||
simulateClick(loginButton);
|
||||
log(`步骤 0:2925 已点击“登录”,点击后地址 ${location.href}`, 'info');
|
||||
|
||||
const finalState = await waitForMail2925View('mailbox', 40000);
|
||||
log(`步骤 0:2925 登录等待结束,状态=${finalState.view},地址=${location.href}`, 'info');
|
||||
if (finalState.view !== 'mailbox') {
|
||||
throw new Error('2925:提交账号密码后未进入收件箱。');
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
loggedIn: true,
|
||||
currentView: 'mailbox',
|
||||
usedCredentials: true,
|
||||
mailboxEmail: finalState.mailboxEmail || getMail2925DisplayedMailboxEmail() || '',
|
||||
};
|
||||
}
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
await ensureSeenCodesSession(step, payload);
|
||||
const {
|
||||
@@ -521,10 +1045,17 @@ async function handlePollEmail(step, payload) {
|
||||
subjectFilters,
|
||||
maxAttempts,
|
||||
intervalMs,
|
||||
filterAfterTimestamp = 0,
|
||||
excludeCodes = [],
|
||||
strictChatGPTCodeOnly = false,
|
||||
targetEmail = '',
|
||||
mail2925MatchTargetEmail = false,
|
||||
} = payload || {};
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
|
||||
if (typeof throwIfMail2925LimitReached === 'function') {
|
||||
throwIfMail2925LimitReached();
|
||||
}
|
||||
|
||||
log(`步骤 ${step}:开始轮询 2925 邮箱(最多 ${maxAttempts} 次)`);
|
||||
|
||||
@@ -544,6 +1075,9 @@ async function handlePollEmail(step, payload) {
|
||||
await returnToInbox();
|
||||
await refreshInbox();
|
||||
await sleep(2000);
|
||||
if (typeof throwIfMail2925LimitReached === 'function') {
|
||||
throwIfMail2925LimitReached();
|
||||
}
|
||||
initialItems = findMailItems();
|
||||
}
|
||||
|
||||
@@ -551,13 +1085,12 @@ async function handlePollEmail(step, payload) {
|
||||
throw new Error('2925 邮箱列表未加载完成,请确认当前已打开收件箱。');
|
||||
}
|
||||
|
||||
const knownMailIds = getCurrentMailIds(initialItems);
|
||||
log(`步骤 ${step}:邮件列表已加载,共 ${initialItems.length} 封邮件`);
|
||||
log(`步骤 ${step}:已记录当前 ${knownMailIds.size} 封旧邮件快照`);
|
||||
|
||||
const FALLBACK_AFTER = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
if (typeof throwIfMail2925LimitReached === 'function') {
|
||||
throwIfMail2925LimitReached();
|
||||
}
|
||||
log(`步骤 ${step}:正在轮询 2925 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
||||
|
||||
if (attempt > 1 || !initialLoadUsedRefresh) {
|
||||
@@ -568,23 +1101,12 @@ async function handlePollEmail(step, payload) {
|
||||
|
||||
const items = findMailItems();
|
||||
if (items.length > 0) {
|
||||
const useFallback = attempt > FALLBACK_AFTER;
|
||||
const newMailIds = new Set();
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const itemId = getMailItemId(item, index);
|
||||
if (!knownMailIds.has(itemId)) {
|
||||
newMailIds.add(itemId);
|
||||
}
|
||||
});
|
||||
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
const item = items[index];
|
||||
const itemId = getMailItemId(item, index);
|
||||
const isNewMail = newMailIds.has(itemId);
|
||||
const itemTimestamp = parseMailItemTimestamp(item);
|
||||
const itemMinute = normalizeMinuteTimestamp(itemTimestamp || 0);
|
||||
|
||||
if (!useFallback && !isNewMail) {
|
||||
if (filterAfterMinute && (!itemMinute || itemMinute < filterAfterMinute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -592,9 +1114,21 @@ async function handlePollEmail(step, payload) {
|
||||
if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) {
|
||||
continue;
|
||||
}
|
||||
const previewTargetState = mail2925MatchTargetEmail
|
||||
? getTargetEmailMatchState(previewText, targetEmail)
|
||||
: { matches: true, hasExplicitEmail: false };
|
||||
if (mail2925MatchTargetEmail && previewTargetState.hasExplicitEmail && !previewTargetState.matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previewCode = extractVerificationCode(previewText, strictChatGPTCodeOnly);
|
||||
const openedText = await openMailAndDeleteAfterRead(item, step);
|
||||
const openedTargetState = mail2925MatchTargetEmail
|
||||
? getTargetEmailMatchState(openedText, targetEmail)
|
||||
: { matches: true, hasExplicitEmail: false };
|
||||
if (mail2925MatchTargetEmail && openedTargetState.hasExplicitEmail && !openedTargetState.matches) {
|
||||
continue;
|
||||
}
|
||||
const bodyCode = extractVerificationCode(openedText, strictChatGPTCodeOnly);
|
||||
const candidateCode = bodyCode || previewCode;
|
||||
|
||||
@@ -613,21 +1147,11 @@ async function handlePollEmail(step, payload) {
|
||||
|
||||
seenCodes.add(candidateCode);
|
||||
persistSeenCodes();
|
||||
const source = useFallback && !isNewMail
|
||||
? (bodyCode ? '回退匹配邮件正文' : '回退匹配邮件')
|
||||
: (bodyCode ? '新邮件正文' : '新邮件');
|
||||
const source = bodyCode ? '邮件正文' : '邮件预览';
|
||||
const timeLabel = itemTimestamp ? `,时间:${new Date(itemTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
|
||||
log(`步骤 ${step}:已找到验证码:${candidateCode}(来源:${source}${timeLabel})`, 'ok');
|
||||
return { ok: true, code: candidateCode, emailTimestamp: Date.now() };
|
||||
}
|
||||
|
||||
items.forEach((item, index) => {
|
||||
knownMailIds.add(getMailItemId(item, index));
|
||||
});
|
||||
}
|
||||
|
||||
if (attempt === FALLBACK_AFTER + 1) {
|
||||
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
|
||||
+855
-136
File diff suppressed because it is too large
Load Diff
+270
-42
@@ -1,4 +1,4 @@
|
||||
// content/vps-panel.js — Content script for CPA panel (steps 1, 9)
|
||||
// content/vps-panel.js — Content script for CPA panel (steps 7, 10 / OAuth URL request)
|
||||
// Injected on: CPA panel (user-configured URL)
|
||||
//
|
||||
// Actual DOM structure (after login click):
|
||||
@@ -19,7 +19,10 @@
|
||||
// <div class="OAuthPage-module__callbackSection___8kA31">
|
||||
// <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=...">
|
||||
// <button class="btn btn-secondary btn-sm"><span>提交回调 URL</span></button>
|
||||
// <div class="status-badge success">回调 URL 已提交,等待认证中...</div>
|
||||
// <div class="status-badge error">回调 URL 提交失败: ...</div>
|
||||
// </div>
|
||||
// <div class="status-badge">等待认证中... / 认证成功! / 认证失败: ...</div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
@@ -192,19 +195,19 @@ function isLocalhostOAuthCallbackUrl(rawUrl) {
|
||||
|
||||
function getStatusBadgeSelectors() {
|
||||
return [
|
||||
'#root > div > div > div > main > div > div > div > div > div:nth-child(1) > div > div.OAuthPage-module__cardContent___1sXLA > div.status-badge',
|
||||
'#root .OAuthPage-module__cardContent___1sXLA > .status-badge',
|
||||
'.OAuthPage-module__cardContent___1sXLA > .status-badge',
|
||||
'#root .OAuthPage-module__cardContent___1sXLA .status-badge',
|
||||
'[class*="cardContent"] .status-badge',
|
||||
'.status-badge',
|
||||
];
|
||||
}
|
||||
|
||||
function getStatusBadgeEntries() {
|
||||
const searchRoot = findCodexOAuthCard() || document;
|
||||
const seen = new Set();
|
||||
const entries = [];
|
||||
|
||||
for (const selector of getStatusBadgeSelectors()) {
|
||||
const candidates = document.querySelectorAll(selector);
|
||||
const candidates = searchRoot.querySelectorAll(selector);
|
||||
for (const candidate of candidates) {
|
||||
if (seen.has(candidate)) continue;
|
||||
seen.add(candidate);
|
||||
@@ -238,21 +241,63 @@ function normalizeStep9StatusText(statusText) {
|
||||
}
|
||||
|
||||
function isOAuthCallbackTimeoutFailure(statusText) {
|
||||
return /认证失败:\s*(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded)/i.test(statusText || '');
|
||||
return /(?:认证失败\s*[::]?\s*)?(?:Timeout waiting for OAuth callback|timeout of \d+ms exceeded|OAuth flow timed out)/i.test(statusText || '');
|
||||
}
|
||||
|
||||
function getStep10StatusBadgeLocation(element) {
|
||||
if (element?.closest?.('[class*="callbackSection"]')) {
|
||||
return 'callback';
|
||||
}
|
||||
if (element?.closest?.('[class*="cardContent"]')) {
|
||||
return 'main';
|
||||
}
|
||||
return 'page';
|
||||
}
|
||||
|
||||
function isStep10CallbackSubmittedStatus(statusText) {
|
||||
const text = normalizeStep9StatusText(statusText);
|
||||
return /回调\s*url\s*已提交.*等待认证中/i.test(text)
|
||||
|| /callback\s*url\s*submitted.*waiting/i.test(text);
|
||||
}
|
||||
|
||||
function isStep10CallbackFailureText(statusText) {
|
||||
const text = normalizeStep9StatusText(statusText);
|
||||
if (!text) return false;
|
||||
return /(?:回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::,,]?\s*/i.test(text)
|
||||
|| /请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(text);
|
||||
}
|
||||
|
||||
function isStep10MainWaitingStatus(statusText) {
|
||||
const text = normalizeStep9StatusText(statusText);
|
||||
return /等待认证中/i.test(text);
|
||||
}
|
||||
|
||||
function isStep10MainFailureText(statusText) {
|
||||
const text = normalizeStep9StatusText(statusText);
|
||||
if (!text) return false;
|
||||
if (/^认证失败\s*[::]?\s*/i.test(text)) return true;
|
||||
return /bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out|request failed with status code \d+|timeout of \d+ms exceeded|network error|failed to fetch/i.test(text);
|
||||
}
|
||||
|
||||
function isStep9FailureText(statusText) {
|
||||
const text = normalizeStep9StatusText(statusText);
|
||||
if (!text) return false;
|
||||
if (isOAuthCallbackTimeoutFailure(text)) return true;
|
||||
if (isStep10CallbackFailureText(text)) return true;
|
||||
if (isStep10MainFailureText(text)) return true;
|
||||
if (typeof isRecoverableStep9AuthFailure === 'function' && isRecoverableStep9AuthFailure(text)) {
|
||||
return true;
|
||||
}
|
||||
return /回调\s*url\s*提交失败|callback\s*url\s*submit\s*failed|oauth flow is not pending/i.test(text);
|
||||
return /callback\s*url\s*submit\s*failed|oauth flow is not pending/i.test(text);
|
||||
}
|
||||
|
||||
function isStep9SuccessStatus(statusText) {
|
||||
return STEP9_SUCCESS_STATUSES.has(normalizeStep9StatusText(statusText));
|
||||
const text = normalizeStep9StatusText(statusText);
|
||||
if (!text) return false;
|
||||
return STEP9_SUCCESS_STATUSES.has(text)
|
||||
|| /^认证成功[!!]?$/i.test(text)
|
||||
|| /^Authentication successful!?$/i.test(text)
|
||||
|| /^Аутентификация успешна!?$/i.test(text);
|
||||
}
|
||||
|
||||
function isStep9SuccessLikeStatus(statusText) {
|
||||
@@ -340,6 +385,7 @@ function createStep9Entry(candidate, selector) {
|
||||
return {
|
||||
element: candidate,
|
||||
selector,
|
||||
location: getStep10StatusBadgeLocation(candidate),
|
||||
visible: isVisibleElement(candidate),
|
||||
text: normalizeStep9StatusText(candidate?.textContent || ''),
|
||||
className,
|
||||
@@ -364,36 +410,72 @@ function getStep9PageErrorSelectors() {
|
||||
}
|
||||
|
||||
function getStep9PageErrorEntries() {
|
||||
const cardRoot = findCodexOAuthCard();
|
||||
const searchRoots = [cardRoot, document].filter(Boolean);
|
||||
const seen = new Set();
|
||||
const entries = [];
|
||||
|
||||
for (const selector of getStep9PageErrorSelectors()) {
|
||||
const candidates = document.querySelectorAll(selector);
|
||||
for (const candidate of candidates) {
|
||||
if (seen.has(candidate)) continue;
|
||||
seen.add(candidate);
|
||||
if (!isVisibleElement(candidate)) continue;
|
||||
for (const root of searchRoots) {
|
||||
for (const selector of getStep9PageErrorSelectors()) {
|
||||
const candidates = root.querySelectorAll(selector);
|
||||
for (const candidate of candidates) {
|
||||
if (seen.has(candidate)) continue;
|
||||
seen.add(candidate);
|
||||
if (!isVisibleElement(candidate)) continue;
|
||||
|
||||
const entry = createStep9Entry(candidate, selector);
|
||||
if (!isStep9FailureText(entry.text)) continue;
|
||||
entries.push(entry);
|
||||
const entry = createStep9Entry(candidate, selector);
|
||||
if (/\bstatus-badge\b/i.test(entry.className)) continue;
|
||||
if (!isStep9FailureText(entry.text)) continue;
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function formatStep10StatusSummaryValue(text, emptyText = '无') {
|
||||
return text ? `"${getInlineTextSnippet(text, 80)}"` : emptyText;
|
||||
}
|
||||
|
||||
function isStep10BrowserSwitchRequiredConflict(diagnostics = {}) {
|
||||
return Boolean(diagnostics?.hasExactSuccessVisibleBadge)
|
||||
&& /请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(String(diagnostics?.callbackFailureText || ''));
|
||||
}
|
||||
|
||||
function getStep10BrowserSwitchRequiredMessage(diagnostics = {}) {
|
||||
const callbackFailureText = normalizeStep9StatusText(diagnostics?.callbackFailureText || '');
|
||||
return [
|
||||
'检测到 CPA 页面同时显示“认证成功”和“回调 URL 提交失败: 请更新CLI Proxy API或检查连接”。',
|
||||
'这类冲突状态通常通过更换浏览器可以解决,请更换浏览器后重新进行注册登录。',
|
||||
callbackFailureText ? `面板原文:${callbackFailureText}` : '',
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSnippet = '') {
|
||||
const visibleEntries = entries.filter((entry) => entry.visible);
|
||||
const successLikeEntries = visibleEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text));
|
||||
const exactSuccessEntries = visibleEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal);
|
||||
const failureEntries = visibleEntries.filter((entry) => isStep9FailureText(entry.text));
|
||||
const callbackEntries = visibleEntries.filter((entry) => entry.location === 'callback');
|
||||
const mainEntries = visibleEntries.filter((entry) => entry.location === 'main');
|
||||
const successLikeEntries = mainEntries.filter((entry) => isStep9SuccessLikeStatus(entry.text));
|
||||
const exactSuccessEntries = mainEntries.filter((entry) => isStep9SuccessStatus(entry.text) && !entry.hasErrorVisualSignal);
|
||||
const callbackSubmittedEntries = callbackEntries.filter((entry) => isStep10CallbackSubmittedStatus(entry.text) && !entry.hasErrorVisualSignal);
|
||||
const callbackFailureEntries = callbackEntries.filter((entry) => isStep10CallbackFailureText(entry.text));
|
||||
const mainWaitingEntries = mainEntries.filter((entry) => isStep10MainWaitingStatus(entry.text) && !entry.hasErrorVisualSignal);
|
||||
const mainFailureEntries = mainEntries.filter((entry) => isStep10MainFailureText(entry.text));
|
||||
const failureEntries = [...callbackFailureEntries, ...mainFailureEntries];
|
||||
const errorStyledEntries = visibleEntries.filter((entry) => entry.hasErrorVisualSignal);
|
||||
const allFailureEntries = [...failureEntries, ...pageErrorEntries];
|
||||
const decisiveFailureEntry = allFailureEntries[0] || null;
|
||||
const selectedEntry = decisiveFailureEntry || exactSuccessEntries[0] || visibleEntries[0] || null;
|
||||
const selectedEntry = decisiveFailureEntry
|
||||
|| exactSuccessEntries[0]
|
||||
|| callbackSubmittedEntries[0]
|
||||
|| mainWaitingEntries[0]
|
||||
|| visibleEntries[0]
|
||||
|| null;
|
||||
const selectedText = selectedEntry?.text || '';
|
||||
const visibleSummary = summarizeStatusBadgeEntries(visibleEntries);
|
||||
const callbackSummary = summarizeStatusBadgeEntries(callbackEntries);
|
||||
const mainSummary = summarizeStatusBadgeEntries(mainEntries);
|
||||
const successLikeSummary = summarizeStatusBadgeEntries(successLikeEntries);
|
||||
const exactSuccessSummary = summarizeStatusBadgeEntries(exactSuccessEntries);
|
||||
const failureSummary = summarizeStatusBadgeEntries(failureEntries);
|
||||
@@ -406,10 +488,20 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
|
||||
selectedText,
|
||||
exactSuccessText: exactSuccessEntries[0]?.text || '',
|
||||
failureText: decisiveFailureEntry?.text || '',
|
||||
failureSource: decisiveFailureEntry?.location || (pageErrorEntries.length ? 'page' : ''),
|
||||
visibleCount: visibleEntries.length,
|
||||
visibleSummary,
|
||||
callbackSummary,
|
||||
mainSummary,
|
||||
callbackStatusText: callbackEntries[0]?.text || '',
|
||||
callbackSubmittedText: callbackSubmittedEntries[0]?.text || '',
|
||||
callbackFailureText: callbackFailureEntries[0]?.text || '',
|
||||
mainStatusText: mainEntries[0]?.text || '',
|
||||
mainWaitingText: mainWaitingEntries[0]?.text || '',
|
||||
mainFailureText: mainFailureEntries[0]?.text || '',
|
||||
hasSuccessLikeVisibleBadge: successLikeEntries.length > 0,
|
||||
hasExactSuccessVisibleBadge: exactSuccessEntries.length > 0,
|
||||
hasCallbackSubmittedBadge: callbackSubmittedEntries.length > 0,
|
||||
hasFailureVisibleBadge: allFailureEntries.length > 0,
|
||||
hasErrorStyledVisibleBadge: errorStyledEntries.length > 0,
|
||||
successLikeSummary,
|
||||
@@ -422,6 +514,8 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
|
||||
selectedText,
|
||||
visibleCount: visibleEntries.length,
|
||||
visibleSummary,
|
||||
callbackSummary,
|
||||
mainSummary,
|
||||
successLikeSummary,
|
||||
exactSuccessSummary,
|
||||
failureSummary,
|
||||
@@ -429,8 +523,8 @@ function buildStep9StatusDiagnostics(entries = [], pageErrorEntries = [], pageSn
|
||||
errorStyledSummary,
|
||||
}),
|
||||
summary: selectedText
|
||||
? `当前聚焦状态="${getInlineTextSnippet(selectedText, 80)}";可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}`
|
||||
: `当前未选中任何可见状态徽标;可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
|
||||
? `当前聚焦状态=${formatStep10StatusSummaryValue(selectedText)};回调提示=${formatStep10StatusSummaryValue(callbackEntries[0]?.text || '')};主状态=${formatStep10StatusSummaryValue(mainEntries[0]?.text || '')};页面错误=${formatStep10StatusSummaryValue(pageErrorEntries[0]?.text || '')};可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix}`
|
||||
: `当前未选中任何可见状态;回调提示=${formatStep10StatusSummaryValue(callbackEntries[0]?.text || '')};主状态=${formatStep10StatusSummaryValue(mainEntries[0]?.text || '')};页面错误=${formatStep10StatusSummaryValue(pageErrorEntries[0]?.text || '')};可见徽标 ${visibleEntries.length} 个:${visibleSummary}${extraFailureSuffix}${errorStyledSuffix};页面片段="${getInlineTextSnippet(pageSnippet, 120)}"`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -452,11 +546,129 @@ function getStatusBadgeText() {
|
||||
return diagnostics.selectedText;
|
||||
}
|
||||
|
||||
function extractStep10FailureDetail(statusText, sourceKind = '') {
|
||||
const text = normalizeStep9StatusText(statusText);
|
||||
if (!text) return '';
|
||||
if (sourceKind === 'callback' || isStep10CallbackFailureText(text)) {
|
||||
return text.replace(/^(?:回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[::,,]?\s*/i, '').trim();
|
||||
}
|
||||
if (sourceKind === 'main' || isStep10MainFailureText(text)) {
|
||||
return text.replace(/^认证失败\s*[::]?\s*/i, '').trim();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function explainStep10Failure(statusText, sourceKind = 'unknown') {
|
||||
const rawText = normalizeStep9StatusText(statusText);
|
||||
const detail = extractStep10FailureDetail(rawText, sourceKind) || rawText;
|
||||
const phaseLabel = sourceKind === 'callback'
|
||||
? '回调提交阶段'
|
||||
: sourceKind === 'main'
|
||||
? '认证结果阶段'
|
||||
: '页面状态阶段';
|
||||
|
||||
const rules = [
|
||||
{
|
||||
code: 'callback_submit_api_unavailable',
|
||||
pattern: /请更新\s*cli\s*proxy\s*api\s*或检查连接/i,
|
||||
message: 'CPA 面板无法把回调提交给后台,通常是 CLI Proxy API 版本过旧、管理接口未启动,或当前面板与后端连接异常。',
|
||||
},
|
||||
{
|
||||
code: 'oauth_state_expired',
|
||||
pattern: /unknown or expired state/i,
|
||||
message: '当前 OAuth 会话在 CPA 中已不存在或已过期,通常是使用了旧回调链接、刷新过新的授权链接后仍提交旧链接,或 CPA 刚重启过。',
|
||||
},
|
||||
{
|
||||
code: 'oauth_not_pending',
|
||||
pattern: /oauth flow is not pending/i,
|
||||
message: '当前 OAuth 会话已经不在等待状态,通常是重复提交、提交过慢,或这轮认证此前已经结束。',
|
||||
},
|
||||
{
|
||||
code: 'callback_state_invalid',
|
||||
pattern: /invalid state|state is required|missing_state/i,
|
||||
message: '回调链接里的 state 缺失或无效,通常是复制了不完整的 localhost 回调链接,或提交了不属于这一轮的旧链接。',
|
||||
},
|
||||
{
|
||||
code: 'callback_missing_result',
|
||||
pattern: /code or error is required/i,
|
||||
message: '回调链接里既没有授权码,也没有错误信息,通常是复制的 localhost 回调链接不完整。',
|
||||
},
|
||||
{
|
||||
code: 'callback_invalid_url',
|
||||
pattern: /invalid redirect_url/i,
|
||||
message: '提交给 CPA 的回调链接格式无法解析,通常是粘贴内容不完整、带了多余字符,或并不是 localhost OAuth 回调地址。',
|
||||
},
|
||||
{
|
||||
code: 'callback_provider_mismatch',
|
||||
pattern: /provider does not match state/i,
|
||||
message: '这条回调不属于当前这次 Codex OAuth,会话与回调来源对不上,通常是混用了其他轮次或其他提供方的回调。',
|
||||
},
|
||||
{
|
||||
code: 'callback_persist_failed',
|
||||
pattern: /failed to persist oauth callback/i,
|
||||
message: 'CPA 已收到回调,但无法把回调结果写入本地缓存文件,通常是认证目录权限、磁盘或运行环境异常。',
|
||||
},
|
||||
{
|
||||
code: 'oauth_bad_request',
|
||||
pattern: /^bad request$/i,
|
||||
message: 'CPA 已收到回调,但 OpenAI OAuth 回调本身返回了错误。常见于用户取消授权、请求过期,或这条回调已经失效。',
|
||||
},
|
||||
{
|
||||
code: 'oauth_state_mismatch',
|
||||
pattern: /state code error/i,
|
||||
message: 'CPA 校验到回调里的 state 与当前 OAuth 会话不一致,通常是步骤 1 已刷新过新的授权链接,但步骤 10 仍提交旧回调。',
|
||||
},
|
||||
{
|
||||
code: 'oauth_code_exchange_failed',
|
||||
pattern: /failed to exchange authorization code for tokens/i,
|
||||
message: 'CPA 已收到授权码,但向 OpenAI 交换令牌失败。常见于 CPA 到 OpenAI 的网络或代理异常,或授权码已过期。',
|
||||
},
|
||||
{
|
||||
code: 'oauth_token_save_failed',
|
||||
pattern: /failed to save authentication tokens/i,
|
||||
message: 'CPA 已完成认证,但保存认证文件失败。常见于认证目录权限、磁盘写入,或 post-auth hook 异常。',
|
||||
},
|
||||
{
|
||||
code: 'oauth_callback_timeout',
|
||||
pattern: /timeout waiting for oauth callback|oauth flow timed out/i,
|
||||
message: 'CPA 长时间没有把这轮 OAuth 流程走完。常见于提交太晚、面板轮询异常,或后端状态没有及时刷新。',
|
||||
},
|
||||
{
|
||||
code: 'oauth_http_timeout',
|
||||
pattern: /timeout of \d+ms exceeded/i,
|
||||
message: 'CPA 面板在请求后台接口时超时,通常是 CLI Proxy API 响应过慢、接口未启动,或网络连接不稳定。',
|
||||
},
|
||||
{
|
||||
code: 'oauth_http_status_error',
|
||||
pattern: /request failed with status code \d+/i,
|
||||
message: 'CPA 面板请求后台接口时收到了异常 HTTP 状态码,通常是接口异常、反向代理配置错误,或当前会话已失效。',
|
||||
},
|
||||
{
|
||||
code: 'oauth_network_error',
|
||||
pattern: /network error|failed to fetch/i,
|
||||
message: 'CPA 面板与后台通信失败,通常是网络不通、管理接口未启动,或浏览器当前连接已断开。',
|
||||
},
|
||||
];
|
||||
|
||||
const matchedRule = rules.find((rule) => rule.pattern.test(detail) || rule.pattern.test(rawText));
|
||||
const message = matchedRule
|
||||
? matchedRule.message
|
||||
: `CPA 在${phaseLabel}返回了未归类的失败,请结合面板原文进一步排查。`;
|
||||
|
||||
return {
|
||||
code: matchedRule?.code || 'oauth_unknown_failure',
|
||||
phaseLabel,
|
||||
rawText,
|
||||
detail,
|
||||
userMessage: `CPA 在${phaseLabel}返回失败:${message} 面板原文:${rawText}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS) {
|
||||
const start = Date.now();
|
||||
let lastDiagnosticsSignature = '';
|
||||
let lastHeartbeatLoggedAt = 0;
|
||||
let lastSuccessLikeMismatchSignature = '';
|
||||
let lastCallbackSubmittedSignature = '';
|
||||
let lastSuccessFailureConflictSignature = '';
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
@@ -475,26 +687,28 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
|
||||
console.log(LOG_PREFIX, '[Step 9] still waiting for success badge', diagnostics);
|
||||
}
|
||||
|
||||
if (diagnostics.hasSuccessLikeVisibleBadge && !diagnostics.hasExactSuccessVisibleBadge) {
|
||||
const mismatchSignature = JSON.stringify({
|
||||
selectedText: diagnostics.selectedText,
|
||||
successLikeSummary: diagnostics.successLikeSummary,
|
||||
visibleSummary: diagnostics.visibleSummary,
|
||||
errorStyledSummary: diagnostics.errorStyledSummary,
|
||||
if (diagnostics.hasCallbackSubmittedBadge && !diagnostics.hasExactSuccessVisibleBadge) {
|
||||
const callbackSubmittedSignature = JSON.stringify({
|
||||
callbackStatusText: diagnostics.callbackStatusText,
|
||||
mainStatusText: diagnostics.mainStatusText,
|
||||
});
|
||||
if (mismatchSignature !== lastSuccessLikeMismatchSignature) {
|
||||
lastSuccessLikeMismatchSignature = mismatchSignature;
|
||||
const errorStyledSuffix = diagnostics.hasErrorStyledVisibleBadge
|
||||
? `;错误样式徽标:${diagnostics.errorStyledSummary}`
|
||||
: '';
|
||||
if (callbackSubmittedSignature !== lastCallbackSubmittedSignature) {
|
||||
lastCallbackSubmittedSignature = callbackSubmittedSignature;
|
||||
log(
|
||||
`步骤 10:检测到“认证成功”相关徽标,但未命中精确成功条件。当前聚焦="${getInlineTextSnippet(diagnostics.selectedText || '(空)', 80)}";成功相关徽标:${diagnostics.successLikeSummary}${errorStyledSuffix}`,
|
||||
'warn'
|
||||
`步骤 10:CPA 已接受 localhost 回调,正在等待后台完成认证。回调提示=${formatStep10StatusSummaryValue(diagnostics.callbackStatusText)};主状态=${formatStep10StatusSummaryValue(diagnostics.mainStatusText)}`,
|
||||
'info'
|
||||
);
|
||||
console.warn(LOG_PREFIX, '[Step 9] success-like badge detected without exact match', diagnostics);
|
||||
console.info(LOG_PREFIX, '[Step 9] callback accepted and waiting for auth completion', diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
if (isStep10BrowserSwitchRequiredConflict(diagnostics)) {
|
||||
const browserSwitchMessage = getStep10BrowserSwitchRequiredMessage(diagnostics);
|
||||
log(`步骤 10:${browserSwitchMessage}`, 'error');
|
||||
console.error(LOG_PREFIX, '[Step 9] browser-switch conflict detected', diagnostics);
|
||||
throw new Error(`BROWSER_SWITCH_REQUIRED::${browserSwitchMessage}`);
|
||||
}
|
||||
|
||||
if (diagnostics.hasExactSuccessVisibleBadge && diagnostics.hasFailureVisibleBadge) {
|
||||
const conflictSignature = JSON.stringify({
|
||||
exactSuccessSummary: diagnostics.exactSuccessSummary,
|
||||
@@ -515,10 +729,11 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
|
||||
}
|
||||
|
||||
if (diagnostics.failureText) {
|
||||
const failureExplanation = explainStep10Failure(diagnostics.failureText, diagnostics.failureSource || 'unknown');
|
||||
if (isOAuthCallbackTimeoutFailure(diagnostics.failureText)) {
|
||||
throw new Error(`STEP9_OAUTH_TIMEOUT::${diagnostics.failureText}`);
|
||||
throw new Error(`STEP9_OAUTH_TIMEOUT::${failureExplanation.userMessage}`);
|
||||
}
|
||||
throw new Error(`STEP9_OAUTH_RETRY::${diagnostics.failureText}`);
|
||||
throw new Error(`STEP9_OAUTH_RETRY::${failureExplanation.userMessage}`);
|
||||
}
|
||||
if (diagnostics.exactSuccessText) {
|
||||
return diagnostics.exactSuccessText;
|
||||
@@ -530,10 +745,18 @@ async function waitForExactSuccessBadge(timeout = STEP9_SUCCESS_BADGE_TIMEOUT_MS
|
||||
const finalText = finalDiagnostics.failureText || finalDiagnostics.selectedText;
|
||||
const diagnosticsSuffix = ` 当前诊断:${finalDiagnostics.summary}`;
|
||||
if (isOAuthCallbackTimeoutFailure(finalText)) {
|
||||
throw new Error(`STEP9_OAUTH_TIMEOUT::${finalText}${diagnosticsSuffix}`);
|
||||
const failureExplanation = explainStep10Failure(finalText, finalDiagnostics.failureSource || 'main');
|
||||
throw new Error(`STEP9_OAUTH_TIMEOUT::${failureExplanation.userMessage}${diagnosticsSuffix}`);
|
||||
}
|
||||
if (isStep9FailureText(finalText)) {
|
||||
throw new Error(`STEP9_OAUTH_RETRY::${finalText}${diagnosticsSuffix}`);
|
||||
const failureExplanation = explainStep10Failure(finalText, finalDiagnostics.failureSource || 'unknown');
|
||||
throw new Error(`STEP9_OAUTH_RETRY::${failureExplanation.userMessage}${diagnosticsSuffix}`);
|
||||
}
|
||||
if (finalDiagnostics.hasCallbackSubmittedBadge || finalDiagnostics.mainWaitingText) {
|
||||
throw new Error(
|
||||
'STEP9_OAUTH_TIMEOUT::CPA 已接受回调,但 120 秒内仍未进入认证成功状态。通常是 CPA 后台处理过慢、面板轮询异常,或 CPA 到 OpenAI 的网络/代理存在问题。'
|
||||
+ diagnosticsSuffix
|
||||
);
|
||||
}
|
||||
throw new Error(finalText
|
||||
? `CPA 面板状态未进入成功状态,当前为“${finalText}”。${diagnosticsSuffix}`
|
||||
@@ -583,6 +806,11 @@ function findCodexOAuthHeader() {
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findCodexOAuthCard() {
|
||||
const header = findCodexOAuthHeader();
|
||||
return header?.closest('.card, [class*="card"]') || header || null;
|
||||
}
|
||||
|
||||
function findOAuthCardLoginButton(header) {
|
||||
const card = header?.closest('.card, [class*="card"]') || header?.parentElement || document;
|
||||
const candidates = card.querySelectorAll('button.btn.btn-primary, button.btn-primary, button.btn');
|
||||
|
||||
Reference in New Issue
Block a user