fix: handle signup phone entry mode before email submission
- 同步最新 dev 到 PR #113,并保留 dev 上更完整的 163 邮箱实现\n- 补充 Step 2 的手机号入口切邮箱逻辑与本地化邮箱输入识别\n- 避免把 Step 2 的 phone entry 提示误判成 auth add-phone 致命错误
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 {
|
||||
|
||||
+663
-462
File diff suppressed because it is too large
Load Diff
@@ -81,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) => {
|
||||
@@ -150,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;
|
||||
@@ -208,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) {
|
||||
@@ -231,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,
|
||||
@@ -253,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
|
||||
@@ -351,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;
|
||||
@@ -518,6 +898,9 @@ async function deleteAllMailboxEmails(step) {
|
||||
}
|
||||
|
||||
async function refreshInbox() {
|
||||
if (typeof throwIfMail2925LimitReached === 'function') {
|
||||
throwIfMail2925LimitReached();
|
||||
}
|
||||
const refreshBtn = findRefreshButton();
|
||||
if (refreshBtn) {
|
||||
simulateClick(refreshBtn);
|
||||
@@ -532,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 {
|
||||
@@ -539,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} 次)`);
|
||||
|
||||
@@ -562,6 +1075,9 @@ async function handlePollEmail(step, payload) {
|
||||
await returnToInbox();
|
||||
await refreshInbox();
|
||||
await sleep(2000);
|
||||
if (typeof throwIfMail2925LimitReached === 'function') {
|
||||
throwIfMail2925LimitReached();
|
||||
}
|
||||
initialItems = findMailItems();
|
||||
}
|
||||
|
||||
@@ -572,6 +1088,9 @@ async function handlePollEmail(step, payload) {
|
||||
log(`步骤 ${step}:邮件列表已加载,共 ${initialItems.length} 封邮件`);
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
if (typeof throwIfMail2925LimitReached === 'function') {
|
||||
throwIfMail2925LimitReached();
|
||||
}
|
||||
log(`步骤 ${step}:正在轮询 2925 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
||||
|
||||
if (attempt > 1 || !initialLoadUsedRefresh) {
|
||||
@@ -585,14 +1104,31 @@ async function handlePollEmail(step, payload) {
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
const item = items[index];
|
||||
const itemTimestamp = parseMailItemTimestamp(item);
|
||||
const itemMinute = normalizeMinuteTimestamp(itemTimestamp || 0);
|
||||
|
||||
if (filterAfterMinute && (!itemMinute || itemMinute < filterAfterMinute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previewText = getMailItemText(item);
|
||||
if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) {
|
||||
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;
|
||||
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
(function attachPhoneAuthModule(root, factory) {
|
||||
root.MultiPagePhoneAuth = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createPhoneAuthModule() {
|
||||
function createPhoneAuthHelpers(deps = {}) {
|
||||
const {
|
||||
fillInput,
|
||||
getActionText,
|
||||
getPageTextSnapshot,
|
||||
getVerificationErrorText,
|
||||
humanPause,
|
||||
isActionEnabled,
|
||||
isAddPhonePageReady,
|
||||
isConsentReady,
|
||||
isPhoneVerificationPageReady,
|
||||
isVisibleElement,
|
||||
simulateClick,
|
||||
sleep,
|
||||
throwIfStopped,
|
||||
waitForElement,
|
||||
} = deps;
|
||||
|
||||
function dispatchInputEvents(element) {
|
||||
if (!element) return;
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
function normalizePhoneDigits(value) {
|
||||
let digits = String(value || '').replace(/\D+/g, '');
|
||||
if (digits.startsWith('00')) {
|
||||
digits = digits.slice(2);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
function normalizeCountryLabel(value) {
|
||||
return String(value || '')
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/&/g, ' and ')
|
||||
.replace(/[^\w\s]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function getOptionLabel(option) {
|
||||
return String(option?.textContent || option?.label || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function extractDialCodeFromText(value) {
|
||||
const match = String(value || '').match(/\(\+\s*(\d{1,4})\s*\)|\+\s*(\d{1,4})\b/);
|
||||
return String(match?.[1] || match?.[2] || '').trim();
|
||||
}
|
||||
|
||||
function getCountryButtonText() {
|
||||
const form = getAddPhoneForm();
|
||||
if (!form) return '';
|
||||
const button = form.querySelector('button[aria-haspopup="listbox"]');
|
||||
if (!button) return '';
|
||||
const valueNode = button.querySelector('.react-aria-SelectValue');
|
||||
return String(valueNode?.textContent || button.textContent || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getDisplayedDialCode() {
|
||||
const buttonDialCode = extractDialCodeFromText(getCountryButtonText());
|
||||
if (buttonDialCode) {
|
||||
return buttonDialCode;
|
||||
}
|
||||
|
||||
const phoneInput = getPhoneInput();
|
||||
const fieldRoot = phoneInput?.closest('fieldset') || phoneInput?.closest('form') || getAddPhoneForm();
|
||||
if (!fieldRoot) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const visibleSpan = Array.from(fieldRoot.querySelectorAll('span'))
|
||||
.find((element) => isVisibleElement(element) && /^\d{1,4}$/.test(String(element.textContent || '').trim()));
|
||||
return String(visibleSpan?.textContent || '').trim();
|
||||
}
|
||||
|
||||
function toNationalPhoneNumber(value, dialCode) {
|
||||
const digits = normalizePhoneDigits(value);
|
||||
const normalizedDialCode = normalizePhoneDigits(dialCode);
|
||||
if (!digits) {
|
||||
return '';
|
||||
}
|
||||
if (normalizedDialCode && digits.startsWith(normalizedDialCode) && digits.length > normalizedDialCode.length) {
|
||||
return digits.slice(normalizedDialCode.length);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
function toE164PhoneNumber(value, dialCode) {
|
||||
const digits = normalizePhoneDigits(value);
|
||||
const normalizedDialCode = normalizePhoneDigits(dialCode);
|
||||
if (!digits) {
|
||||
return '';
|
||||
}
|
||||
if (!normalizedDialCode) {
|
||||
return digits.startsWith('+') ? digits : `+${digits}`;
|
||||
}
|
||||
if (digits.startsWith(normalizedDialCode)) {
|
||||
return `+${digits}`;
|
||||
}
|
||||
if (digits.startsWith('0')) {
|
||||
return `+${normalizedDialCode}${digits.slice(1)}`;
|
||||
}
|
||||
return `+${normalizedDialCode}${digits}`;
|
||||
}
|
||||
|
||||
function getAddPhoneForm() {
|
||||
return document.querySelector('form[action*="/add-phone" i]');
|
||||
}
|
||||
|
||||
function getPhoneVerificationForm() {
|
||||
return document.querySelector('form[action*="/phone-verification" i]');
|
||||
}
|
||||
|
||||
function getPhoneInput() {
|
||||
const form = getAddPhoneForm();
|
||||
if (!form) return null;
|
||||
const input = form.querySelector(
|
||||
'input[type="tel"], input[name="__reservedForPhoneNumberInput_tel"], input[autocomplete="tel"]'
|
||||
);
|
||||
return input && isVisibleElement(input) ? input : null;
|
||||
}
|
||||
|
||||
function getHiddenPhoneNumberInput() {
|
||||
const form = getAddPhoneForm();
|
||||
if (!form) return null;
|
||||
return form.querySelector('input[name="phoneNumber"]');
|
||||
}
|
||||
|
||||
function getCountrySelect() {
|
||||
const form = getAddPhoneForm();
|
||||
if (!form) return null;
|
||||
return form.querySelector('select');
|
||||
}
|
||||
|
||||
function getSelectedCountryOption() {
|
||||
const select = getCountrySelect();
|
||||
if (!select || select.selectedIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
return select.options[select.selectedIndex] || null;
|
||||
}
|
||||
|
||||
function findCountryOptionByLabel(countryLabel) {
|
||||
const select = getCountrySelect();
|
||||
if (!select) {
|
||||
return null;
|
||||
}
|
||||
const normalizedTarget = normalizeCountryLabel(countryLabel);
|
||||
if (!normalizedTarget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = Array.from(select.options);
|
||||
return options.find((option) => normalizeCountryLabel(getOptionLabel(option)) === normalizedTarget)
|
||||
|| options.find((option) => {
|
||||
const optionLabel = normalizeCountryLabel(getOptionLabel(option));
|
||||
return optionLabel && (optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel));
|
||||
})
|
||||
|| null;
|
||||
}
|
||||
|
||||
async function ensureCountrySelected(countryLabel) {
|
||||
const select = getCountrySelect();
|
||||
if (!select) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetOption = findCountryOptionByLabel(countryLabel);
|
||||
if (!targetOption) {
|
||||
throw new Error(`Add-phone page is missing the country option for "${countryLabel}".`);
|
||||
}
|
||||
|
||||
const selectedOption = getSelectedCountryOption();
|
||||
if (selectedOption && normalizeCountryLabel(getOptionLabel(selectedOption)) === normalizeCountryLabel(getOptionLabel(targetOption))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
select.value = String(targetOption.value || '');
|
||||
dispatchInputEvents(select);
|
||||
await sleep(250);
|
||||
|
||||
const nextSelectedOption = getSelectedCountryOption();
|
||||
return Boolean(
|
||||
nextSelectedOption
|
||||
&& normalizeCountryLabel(getOptionLabel(nextSelectedOption)) === normalizeCountryLabel(getOptionLabel(targetOption))
|
||||
);
|
||||
}
|
||||
|
||||
function getAddPhoneSubmitButton() {
|
||||
const form = getAddPhoneForm();
|
||||
if (!form) return null;
|
||||
const buttons = Array.from(form.querySelectorAll('button[type="submit"], input[type="submit"]'));
|
||||
return buttons.find((button) => isVisibleElement(button) && isActionEnabled(button))
|
||||
|| buttons.find((button) => isVisibleElement(button))
|
||||
|| null;
|
||||
}
|
||||
|
||||
function getPhoneVerificationCodeInput() {
|
||||
const form = getPhoneVerificationForm();
|
||||
if (!form) return null;
|
||||
const input = form.querySelector(
|
||||
'input[name="code"], input[autocomplete="one-time-code"], input[inputmode="numeric"]'
|
||||
);
|
||||
return input && isVisibleElement(input) ? input : null;
|
||||
}
|
||||
|
||||
function getPhoneVerificationSubmitButton() {
|
||||
const form = getPhoneVerificationForm();
|
||||
if (!form) return null;
|
||||
const buttons = Array.from(form.querySelectorAll('button[type="submit"], input[type="submit"]'));
|
||||
return buttons.find((button) => {
|
||||
if (!isVisibleElement(button) || !isActionEnabled(button)) return false;
|
||||
const intent = String(button.getAttribute('value') || '').trim().toLowerCase();
|
||||
if (intent === 'resend') return false;
|
||||
return true;
|
||||
}) || buttons.find((button) => isVisibleElement(button));
|
||||
}
|
||||
|
||||
function getPhoneVerificationResendButton(options = {}) {
|
||||
const { allowDisabled = false } = options;
|
||||
const form = getPhoneVerificationForm();
|
||||
if (!form) return null;
|
||||
const buttons = Array.from(form.querySelectorAll('button, input[type="submit"], input[type="button"]'));
|
||||
return buttons.find((button) => {
|
||||
if (!isVisibleElement(button)) return false;
|
||||
if (!allowDisabled && !isActionEnabled(button)) return false;
|
||||
const intent = String(button.getAttribute('value') || '').trim().toLowerCase();
|
||||
if (intent === 'resend') return true;
|
||||
return /resend/i.test(getActionText(button));
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getPhoneVerificationDisplayedPhone() {
|
||||
const text = getPageTextSnapshot();
|
||||
const matches = text.match(/\+\d[\d\s-]{6,}\d/g);
|
||||
return matches?.[0] ? matches[0].replace(/\s+/g, ' ').trim() : '';
|
||||
}
|
||||
|
||||
async function waitForAddPhoneReady(timeout = 20000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
if (isAddPhonePageReady()) {
|
||||
return true;
|
||||
}
|
||||
await sleep(150);
|
||||
}
|
||||
throw new Error('Timed out waiting for add-phone page.');
|
||||
}
|
||||
|
||||
async function waitForPhoneVerificationReady(timeout = 20000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
if (isPhoneVerificationPageReady()) {
|
||||
return {
|
||||
phoneVerificationPage: true,
|
||||
displayedPhone: getPhoneVerificationDisplayedPhone(),
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
await sleep(150);
|
||||
}
|
||||
throw new Error('Timed out waiting for phone verification page.');
|
||||
}
|
||||
|
||||
async function submitPhoneNumber(payload = {}) {
|
||||
const countryLabel = String(payload.countryLabel || '').trim();
|
||||
if (!countryLabel) {
|
||||
throw new Error('Missing country label for add-phone submission.');
|
||||
}
|
||||
|
||||
await waitForAddPhoneReady();
|
||||
const countrySelected = await ensureCountrySelected(countryLabel);
|
||||
if (!countrySelected) {
|
||||
throw new Error(`Failed to select "${countryLabel}" on the add-phone page.`);
|
||||
}
|
||||
|
||||
const dialCode = getDisplayedDialCode();
|
||||
if (!dialCode) {
|
||||
throw new Error(`Could not determine the dial code for "${countryLabel}" on the add-phone page.`);
|
||||
}
|
||||
|
||||
const phoneNumber = toE164PhoneNumber(payload.phoneNumber, dialCode);
|
||||
const nationalPhoneNumber = toNationalPhoneNumber(payload.phoneNumber, dialCode);
|
||||
if (!phoneNumber || !nationalPhoneNumber) {
|
||||
throw new Error('Missing phone number for add-phone submission.');
|
||||
}
|
||||
|
||||
const phoneInput = getPhoneInput() || await waitForElement(
|
||||
'input[type="tel"], input[name="__reservedForPhoneNumberInput_tel"], input[autocomplete="tel"]',
|
||||
10000
|
||||
);
|
||||
const hiddenPhoneNumberInput = getHiddenPhoneNumberInput();
|
||||
const submitButton = getAddPhoneSubmitButton();
|
||||
|
||||
if (!phoneInput) {
|
||||
throw new Error('Add-phone page is missing the phone number input.');
|
||||
}
|
||||
if (!submitButton) {
|
||||
throw new Error('Add-phone page is missing the submit button.');
|
||||
}
|
||||
|
||||
await humanPause(250, 700);
|
||||
fillInput(phoneInput, nationalPhoneNumber);
|
||||
if (hiddenPhoneNumberInput) {
|
||||
hiddenPhoneNumberInput.value = phoneNumber;
|
||||
dispatchInputEvents(hiddenPhoneNumberInput);
|
||||
}
|
||||
await sleep(250);
|
||||
simulateClick(submitButton);
|
||||
return waitForPhoneVerificationReady();
|
||||
}
|
||||
|
||||
async function waitForPhoneVerificationOutcome(timeout = 30000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
|
||||
const errorText = getVerificationErrorText();
|
||||
if (errorText) {
|
||||
return {
|
||||
invalidCode: true,
|
||||
errorText,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
if (isConsentReady()) {
|
||||
return {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
if (isAddPhonePageReady()) {
|
||||
return {
|
||||
returnedToAddPhone: true,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
await sleep(150);
|
||||
}
|
||||
|
||||
if (isPhoneVerificationPageReady()) {
|
||||
return {
|
||||
invalidCode: true,
|
||||
errorText: getVerificationErrorText() || 'Phone verification page stayed in place after code submission.',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
assumed: true,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
async function submitPhoneVerificationCode(payload = {}) {
|
||||
const code = String(payload.code || '').trim();
|
||||
if (!code) {
|
||||
throw new Error('Missing phone verification code.');
|
||||
}
|
||||
|
||||
await waitForPhoneVerificationReady();
|
||||
const codeInput = getPhoneVerificationCodeInput() || await waitForElement(
|
||||
'input[name="code"], input[autocomplete="one-time-code"], input[inputmode="numeric"]',
|
||||
10000
|
||||
);
|
||||
const submitButton = getPhoneVerificationSubmitButton();
|
||||
|
||||
if (!codeInput) {
|
||||
throw new Error('Phone verification page is missing the code input.');
|
||||
}
|
||||
if (!submitButton) {
|
||||
throw new Error('Phone verification page is missing the submit button.');
|
||||
}
|
||||
|
||||
await humanPause(250, 700);
|
||||
fillInput(codeInput, code);
|
||||
await sleep(250);
|
||||
simulateClick(submitButton);
|
||||
return waitForPhoneVerificationOutcome();
|
||||
}
|
||||
|
||||
async function resendPhoneVerificationCode(timeout = 45000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
const resendButton = getPhoneVerificationResendButton({ allowDisabled: true });
|
||||
if (resendButton && isActionEnabled(resendButton)) {
|
||||
await humanPause(250, 700);
|
||||
simulateClick(resendButton);
|
||||
await sleep(1000);
|
||||
return {
|
||||
resent: true,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
throw new Error('Timed out waiting for the phone verification resend button.');
|
||||
}
|
||||
|
||||
async function returnToAddPhone(timeout = 20000) {
|
||||
if (isAddPhonePageReady()) {
|
||||
return {
|
||||
addPhonePage: true,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isPhoneVerificationPageReady()) {
|
||||
throw new Error('The auth page is not currently on phone verification or add-phone page.');
|
||||
}
|
||||
|
||||
location.assign('/add-phone');
|
||||
await waitForAddPhoneReady(timeout);
|
||||
return {
|
||||
addPhonePage: true,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
getPhoneVerificationDisplayedPhone,
|
||||
isPhoneVerificationPageReady,
|
||||
resendPhoneVerificationCode,
|
||||
returnToAddPhone,
|
||||
submitPhoneNumber,
|
||||
submitPhoneVerificationCode,
|
||||
toE164PhoneNumber,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createPhoneAuthHelpers,
|
||||
};
|
||||
});
|
||||
+892
-101
File diff suppressed because it is too large
Load Diff
+269
-41
@@ -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