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:
QLHazyCoder
2026-04-25 14:53:13 +08:00
105 changed files with 18673 additions and 1530 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 {
+663 -462
View File
File diff suppressed because it is too large Load Diff
+536
View File
@@ -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;
+453
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+269 -41
View File
@@ -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');