merge: 合并 dev 并解决 PR 冲突

This commit is contained in:
Ryan Liu
2026-04-23 10:05:23 +08:00
98 changed files with 17803 additions and 912 deletions
+9 -1
View File
@@ -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 {
+20 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+270 -42
View File
@@ -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'
`步骤 10CPA 已接受 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');