翻译界面和日志信息为中文,更新 OAuth 流程中的步骤描述,添加管理密钥输入框和相关功能,增强用户体验,优化 CSS 样式,修复部分逻辑错误,确保自动化流程的稳定性和可用性。

This commit is contained in:
QLHazyCoder
2026-04-08 01:14:22 +08:00
parent f79166b96d
commit 96867783dd
12 changed files with 1228 additions and 439 deletions
+8 -8
View File
@@ -10,7 +10,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse(result);
}).catch(err => {
if (isStopError(err)) {
log('Duck Mail: Stopped by user.', 'warn');
log('Duck 邮箱:已被用户停止。', 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
@@ -23,7 +23,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
async function fetchDuckEmail(payload = {}) {
const { generateNew = true } = payload;
log(`Duck Mail: ${generateNew ? 'Generating' : 'Reading'} private address...`);
log(`Duck 邮箱:正在${generateNew ? '生成' : '读取'}私有地址...`);
await waitForElement(
'input.AutofillSettingsPanel__PrivateDuckAddressValue, button.AutofillSettingsPanel__GeneratorButton',
@@ -46,12 +46,12 @@ async function fetchDuckEmail(payload = {}) {
}
await sleep(150);
}
throw new Error('Timed out waiting for Duck address to appear.');
throw new Error('等待 Duck 地址出现超时。');
};
const currentEmail = readEmail();
if (currentEmail && !generateNew) {
log(`Duck Mail: Found existing address ${currentEmail}`);
log(`Duck 邮箱:已发现现有地址 ${currentEmail}`);
return { email: currentEmail, generated: false };
}
@@ -59,16 +59,16 @@ async function fetchDuckEmail(payload = {}) {
const generatorButton = getGeneratorButton();
if (!generatorButton) {
if (currentEmail) {
log(`Duck Mail: Reusing existing address ${currentEmail}`, 'warn');
log(`Duck 邮箱:正在复用现有地址 ${currentEmail}`, 'warn');
return { email: currentEmail, generated: false };
}
throw new Error('Could not find "Generate Private Duck Address" button.');
throw new Error('未找到“生成 Duck 私有地址”按钮。');
}
generatorButton.click();
log('Duck Mail: Clicked "Generate Private Duck Address"');
log('Duck 邮箱:已点击“生成 Duck 私有地址”按钮');
const nextEmail = await waitForEmailValue(currentEmail);
log(`Duck Mail: Ready address ${nextEmail}`, 'ok');
log(`Duck 邮箱:地址已就绪 ${nextEmail}`, 'ok');
return { email: nextEmail, generated: true };
}
+21 -15
View File
@@ -45,11 +45,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse(result);
}).catch(err => {
if (isStopError(err)) {
log(`Step ${message.step}: Stopped by user.`, 'warn');
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
reportError(message.step, err.message);
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
sendResponse({ error: err.message });
});
return true;
@@ -158,10 +158,10 @@ async function deleteCurrentMailboxMessage(step) {
try {
const deleteButton = await waitForElement('.button-bar button.danger', 5000);
simulateClick(deleteButton);
log(`Step ${step}: Deleted mailbox message`, 'ok');
log(`步骤 ${step}:已删除邮箱消息`, 'ok');
await sleep(1200);
} catch (err) {
log(`Step ${step}: Failed to delete mailbox message: ${err.message}`, 'warn');
log(`步骤 ${step}:删除邮箱消息失败:${err.message}`, 'warn');
}
}
@@ -171,24 +171,26 @@ async function handleMailboxPollEmail(step, payload) {
subjectFilters = [],
maxAttempts = 20,
intervalMs = 3000,
excludeCodes = [],
} = payload || {};
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
log(`Step ${step}: Starting email poll on Inbucket mailbox page (max ${maxAttempts} attempts)`);
log(`步骤 ${step}:开始轮询 Inbucket 邮箱页面(最多 ${maxAttempts} 次)`);
try {
await waitForElement('.message-list, .message-list-entry', 15000);
log(`Step ${step}: Mailbox page loaded`);
log(`步骤 ${step}:邮箱页面已加载`);
} catch {
throw new Error('Inbucket mailbox page did not load. Make sure /m/<mailbox>/ is open.');
throw new Error('Inbucket 邮箱页面未加载完成,请确认已打开 /m/<mailbox>/ 页面。');
}
const existingMailIds = getCurrentMailboxIds();
log(`Step ${step}: Snapshotted ${existingMailIds.size} existing mailbox messages`);
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧消息快照`);
const FALLBACK_AFTER = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`Polling Inbucket mailbox... attempt ${attempt}/${maxAttempts}`);
log(`步骤 ${step}:正在轮询 Inbucket 邮箱,第 ${attempt}/${maxAttempts}`);
if (attempt > 1) {
await refreshMailbox();
@@ -212,6 +214,10 @@ async function handleMailboxPollEmail(step, payload) {
for (const mail of candidates) {
const code = mail.code || extractVerificationCode(mail.combinedText);
if (!code) continue;
if (excludedCodeSet.has(code)) {
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
continue;
}
await openMailboxEntry(mail.entry);
await deleteCurrentMailboxMessage(step);
@@ -219,9 +225,9 @@ async function handleMailboxPollEmail(step, payload) {
seenMailIds.add(mail.mailId);
await persistSeenMailIds();
const source = existingMailIds.has(mail.mailId) ? 'fallback' : 'new';
const source = existingMailIds.has(mail.mailId) ? '回退匹配邮件' : '新邮件';
log(
`Step ${step}: Code found: ${code} (${source}, sender: ${mail.sender || 'unknown'}, subject: ${(mail.subject || '').slice(0, 60)})`,
`步骤 ${step}:已找到验证码:${code}(来源:${source},发件人:${mail.sender || '未知'},主题:${(mail.subject || '').slice(0, 60)}`,
'ok'
);
@@ -234,7 +240,7 @@ async function handleMailboxPollEmail(step, payload) {
}
if (attempt === FALLBACK_AFTER + 1) {
log(`Step ${step}: No new mailbox messages yet, falling back to older matching messages`, 'warn');
log(`步骤 ${step}:暂未发现新消息,开始回退到较早的匹配邮件`, 'warn');
}
if (attempt < maxAttempts) {
@@ -243,14 +249,14 @@ async function handleMailboxPollEmail(step, payload) {
}
throw new Error(
`No matching verification email found in Inbucket mailbox after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
'Check the mailbox page manually.'
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Inbucket 邮箱中找到匹配的验证码邮件。` +
'请手动检查邮箱页面。'
);
}
async function handlePollEmail(step, payload) {
if (!location.pathname.startsWith('/m/')) {
throw new Error('Inbucket now only supports mailbox pages like /m/<mailbox>/.');
throw new Error('当前 Inbucket 仅支持 /m/<mailbox>/ 这种邮箱页面。');
}
return handleMailboxPollEmail(step, payload);
}
+30 -27
View File
@@ -54,11 +54,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse(result);
}).catch(err => {
if (isStopError(err)) {
log(`Step ${message.step}: Stopped by user.`, 'warn');
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
reportError(message.step, err.message);
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
sendResponse({ error: err.message });
});
return true;
@@ -87,22 +87,23 @@ function getCurrentMailIds() {
// ============================================================
async function handlePollEmail(step, payload) {
const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload;
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
log(`Step ${step}: Starting email poll on 163 Mail (max ${maxAttempts} attempts)`);
log(`步骤 ${step}:开始轮询 163 邮箱(最多 ${maxAttempts} 次)`);
// Click inbox in sidebar to ensure we're in inbox view
log(`Step ${step}: Waiting for sidebar...`);
log(`步骤 ${step}:正在等待侧边栏加载...`);
try {
const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000);
inboxLink.click();
log(`Step ${step}: Clicked inbox`);
log(`步骤 ${step}:已点击收件箱`);
} catch {
log(`Step ${step}: Inbox link not found, proceeding...`, 'warn');
log(`步骤 ${step}:未找到收件箱入口,继续尝试后续流程...`, 'warn');
}
// Wait for mail list to appear
log(`Step ${step}: Waiting for mail list...`);
log(`步骤 ${step}:正在等待邮件列表加载...`);
let items = [];
for (let i = 0; i < 20; i++) {
items = findMailItems();
@@ -117,19 +118,19 @@ async function handlePollEmail(step, payload) {
}
if (items.length === 0) {
throw new Error('163 Mail list did not load. Make sure inbox is open.');
throw new Error('163 邮箱列表未加载完成,请确认当前已打开收件箱。');
}
log(`Step ${step}: Mail list loaded, ${items.length} items`);
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
// Snapshot existing mail IDs
const existingMailIds = getCurrentMailIds();
log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails`);
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
const FALLBACK_AFTER = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`Polling 163 Mail... attempt ${attempt}/${maxAttempts}`);
log(`步骤 ${step}:正在轮询 163 邮箱,第 ${attempt}/${maxAttempts}`);
if (attempt > 1) {
await refreshInbox();
@@ -157,11 +158,13 @@ async function handlePollEmail(step, payload) {
if (senderMatch || subjectMatch) {
const code = extractVerificationCode(subject + ' ' + ariaLabel);
if (code && !seenCodes.has(code)) {
if (code && excludedCodeSet.has(code)) {
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
} else if (code && !seenCodes.has(code)) {
seenCodes.add(code);
persistSeenCodes();
const source = useFallback && existingMailIds.has(id) ? 'fallback' : 'new';
log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok');
const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件' : '新邮件';
log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)}`, 'ok');
// Delete this email via right-click menu, WAIT for it to finish before returning
await deleteEmail(item, step);
@@ -170,13 +173,13 @@ async function handlePollEmail(step, payload) {
return { ok: true, code, emailTimestamp: Date.now(), mailId: id };
} else if (code && seenCodes.has(code)) {
log(`Step ${step}: Skipping already-seen code: ${code}`, 'info');
log(`步骤 ${step}:跳过已处理过的验证码:${code}`, 'info');
}
}
}
if (attempt === FALLBACK_AFTER + 1) {
log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first match`, 'warn');
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
}
if (attempt < maxAttempts) {
@@ -185,8 +188,8 @@ async function handlePollEmail(step, payload) {
}
throw new Error(
`No new matching email found on 163 Mail after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
'Check inbox manually.'
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 163 邮箱中找到新的匹配邮件。` +
'请手动检查收件箱。'
);
}
@@ -196,7 +199,7 @@ async function handlePollEmail(step, payload) {
async function deleteEmail(item, step) {
try {
log(`Step ${step}: Deleting email...`);
log(`步骤 ${step}:正在删除邮件...`);
// Strategy 1: Click the trash icon inside the mail item
// Each mail item has: <b class="nui-ico nui-ico-delete" title="删除邮件" sign="trash">
@@ -208,21 +211,21 @@ async function deleteEmail(item, step) {
const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]');
if (trashIcon) {
trashIcon.click();
log(`Step ${step}: Clicked trash icon`, 'ok');
log(`步骤 ${step}:已点击删除图标`, 'ok');
await sleep(1500);
// Check if item disappeared (confirm deletion)
const stillExists = document.getElementById(item.id);
if (!stillExists || stillExists.style.display === 'none') {
log(`Step ${step}: Email deleted successfully`);
log(`步骤 ${step}:邮件已成功删除`);
} else {
log(`Step ${step}: Email may not have been deleted, item still visible`, 'warn');
log(`步骤 ${step}:邮件可能尚未删除,列表中仍可见`, 'warn');
}
return;
}
// Strategy 2: Select checkbox then click toolbar delete button
log(`Step ${step}: Trash icon not found, trying checkbox + toolbar delete...`);
log(`步骤 ${step}:未找到删除图标,尝试使用复选框加工具栏删除...`);
const checkbox = item.querySelector('[sign="checkbox"], .nui-chk');
if (checkbox) {
checkbox.click();
@@ -233,16 +236,16 @@ async function deleteEmail(item, step) {
for (const btn of toolbarBtns) {
if (btn.textContent.replace(/\s/g, '').includes('删除')) {
btn.closest('.nui-btn').click();
log(`Step ${step}: Clicked toolbar delete`, 'ok');
log(`步骤 ${step}:已点击工具栏删除`, 'ok');
await sleep(1500);
return;
}
}
}
log(`Step ${step}: Could not delete email (no delete button found)`, 'warn');
log(`步骤 ${step}:无法删除邮件(未找到删除按钮)`, 'warn');
} catch (err) {
log(`Step ${step}: Failed to delete email: ${err.message}`, 'warn');
log(`步骤 ${step}:删除邮件失败:${err.message}`, 'warn');
}
}
+18 -13
View File
@@ -27,11 +27,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse(result);
}).catch(err => {
if (isStopError(err)) {
log(`Step ${message.step}: Stopped by user.`, 'warn');
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
reportError(message.step, err.message);
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
sendResponse({ error: err.message });
});
return true; // async response
@@ -55,28 +55,29 @@ function getCurrentMailIds() {
// ============================================================
async function handlePollEmail(step, payload) {
const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload;
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
log(`Step ${step}: Starting email poll (max ${maxAttempts} attempts, every ${intervalMs / 1000}s)`);
log(`步骤 ${step}:开始轮询邮箱(最多 ${maxAttempts} 次,每 ${intervalMs / 1000} 秒一次)`);
// Wait for mail list to load
try {
await waitForElement('.mail-list-page-item', 10000);
log(`Step ${step}: Mail list loaded`);
log(`步骤 ${step}:邮件列表已加载`);
} catch {
throw new Error('Mail list did not load. Make sure QQ Mail inbox is open.');
throw new Error('邮件列表未加载完成,请确认 QQ 邮箱已打开收件箱。');
}
// Step 1: Snapshot existing mail IDs BEFORE we start waiting for new email
const existingMailIds = getCurrentMailIds();
log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails as "old"`);
log(`步骤 ${step}:已将当前 ${existingMailIds.size} 封邮件标记为旧邮件快照`);
// Fallback after just 3 attempts (~10s). In practice, the email is usually
// already in the list but has the same mailid (page was already open).
const FALLBACK_AFTER = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`Polling QQ Mail... attempt ${attempt}/${maxAttempts}`);
log(`步骤 ${step}:正在轮询 QQ 邮箱,第 ${attempt}/${maxAttempts}`);
// Refresh inbox (skip on first attempt, list is fresh)
if (attempt > 1) {
@@ -104,15 +105,19 @@ async function handlePollEmail(step, payload) {
if (senderMatch || subjectMatch) {
const code = extractVerificationCode(subject + ' ' + digest);
if (code) {
const source = useFallback && existingMailIds.has(mailId) ? 'fallback-first-match' : 'new';
log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok');
if (excludedCodeSet.has(code)) {
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
continue;
}
const source = useFallback && existingMailIds.has(mailId) ? '回退首封匹配邮件' : '新邮件';
log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)}`, 'ok');
return { ok: true, code, emailTimestamp: Date.now(), mailId };
}
}
}
if (attempt === FALLBACK_AFTER + 1) {
log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first matching email`, 'warn');
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
}
if (attempt < maxAttempts) {
@@ -121,8 +126,8 @@ async function handlePollEmail(step, payload) {
}
throw new Error(
`No new matching email found after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
'Check QQ Mail manually. Email may be delayed or in spam folder.'
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未找到新的匹配邮件。` +
'请手动检查 QQ 邮箱,邮件可能延迟到达或进入垃圾箱。'
);
}
+413 -67
View File
@@ -5,19 +5,25 @@ console.log('[MultiPage:signup-page] Content script loaded on', location.href);
// Listen for commands from Background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE' || message.type === 'STEP8_FIND_AND_CLICK') {
if (
message.type === 'EXECUTE_STEP'
|| message.type === 'FILL_CODE'
|| message.type === 'STEP8_FIND_AND_CLICK'
|| message.type === 'PREPARE_LOGIN_CODE'
|| message.type === 'RESEND_VERIFICATION_CODE'
) {
resetStopState();
handleCommand(message).then((result) => {
sendResponse({ ok: true, ...(result || {}) });
}).catch(err => {
if (isStopError(err)) {
log(`Step ${message.step || 8}: Stopped by user.`, 'warn');
log(`步骤 ${message.step || 8}:已被用户停止。`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
if (message.type === 'STEP8_FIND_AND_CLICK') {
log(`Step 8: ${err.message}`, 'error');
log(`步骤 8${err.message}`, 'error');
sendResponse({ error: err.message });
return;
}
@@ -38,22 +44,212 @@ async function handleCommand(message) {
case 5: return await step5_fillNameBirthday(message.payload);
case 6: return await step6_login(message.payload);
case 8: return await step8_findAndClick();
default: throw new Error(`signup-page.js does not handle step ${message.step}`);
default: throw new Error(`signup-page.js 不处理步骤 ${message.step}`);
}
case 'FILL_CODE':
// Step 4 = signup code, Step 7 = login code (same handler)
return await fillVerificationCode(message.step, message.payload);
case 'PREPARE_LOGIN_CODE':
return await prepareLoginCodeFlow();
case 'RESEND_VERIFICATION_CODE':
return await resendVerificationCode(message.step);
case 'STEP8_FIND_AND_CLICK':
return await step8_findAndClick();
}
}
const VERIFICATION_CODE_INPUT_SELECTOR = [
'input[name="code"]',
'input[name="otp"]',
'input[autocomplete="one-time-code"]',
'input[type="text"][maxlength="6"]',
'input[type="tel"][maxlength="6"]',
'input[aria-label*="code" i]',
'input[placeholder*="code" i]',
'input[inputmode="numeric"]',
].join(', ');
const ONE_TIME_CODE_LOGIN_PATTERN = /使用一次性验证码登录|改用(?:一次性)?验证码(?:登录)?|使用验证码登录|一次性验证码|验证码登录|one[-\s]*time\s*(?:passcode|password|code)|use\s+(?:a\s+)?one[-\s]*time\s*(?:passcode|password|code)(?:\s+instead)?|use\s+(?:a\s+)?code(?:\s+instead)?|sign\s+in\s+with\s+(?:email|code)|email\s+(?:me\s+)?(?:a\s+)?code/i;
const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i;
function isVisibleElement(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function getVerificationCodeTarget() {
const codeInput = document.querySelector(VERIFICATION_CODE_INPUT_SELECTOR);
if (codeInput && isVisibleElement(codeInput)) {
return { type: 'single', element: codeInput };
}
const singleInputs = Array.from(document.querySelectorAll('input[maxlength="1"]'))
.filter(isVisibleElement);
if (singleInputs.length >= 6) {
return { type: 'split', elements: singleInputs };
}
return null;
}
function getActionText(el) {
return [
el?.textContent,
el?.value,
el?.getAttribute?.('aria-label'),
el?.getAttribute?.('title'),
]
.filter(Boolean)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
}
function isActionEnabled(el) {
return Boolean(el)
&& !el.disabled
&& el.getAttribute('aria-disabled') !== 'true';
}
function findOneTimeCodeLoginTrigger() {
const candidates = document.querySelectorAll(
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
);
for (const el of candidates) {
if (!isVisibleElement(el)) continue;
if (el.disabled || el.getAttribute('aria-disabled') === 'true') continue;
const text = [
el.textContent,
el.value,
el.getAttribute('aria-label'),
el.getAttribute('title'),
]
.filter(Boolean)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
if (text && ONE_TIME_CODE_LOGIN_PATTERN.test(text)) {
return el;
}
}
return null;
}
function findResendVerificationCodeTrigger({ allowDisabled = false } = {}) {
const candidates = document.querySelectorAll(
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
);
for (const el of candidates) {
if (!isVisibleElement(el)) continue;
if (!allowDisabled && !isActionEnabled(el)) continue;
const text = getActionText(el);
if (text && RESEND_VERIFICATION_CODE_PATTERN.test(text)) {
return el;
}
}
return null;
}
async function prepareLoginCodeFlow(timeout = 15000) {
const readyTarget = getVerificationCodeTarget();
if (readyTarget) {
log('步骤 7:验证码输入框已就绪。');
return { ready: true, mode: readyTarget.type };
}
const start = Date.now();
let switchClickCount = 0;
let lastSwitchAttemptAt = 0;
let loggedPasswordPage = false;
while (Date.now() - start < timeout) {
throwIfStopped();
const target = getVerificationCodeTarget();
if (target) {
log('步骤 7:验证码页面已就绪。');
return { ready: true, mode: target.type };
}
const passwordInput = document.querySelector('input[type="password"]');
const switchTrigger = findOneTimeCodeLoginTrigger();
if (switchTrigger && (switchClickCount === 0 || Date.now() - lastSwitchAttemptAt > 1500)) {
switchClickCount += 1;
lastSwitchAttemptAt = Date.now();
loggedPasswordPage = false;
log('步骤 7:检测到密码页,正在切换到一次性验证码登录...');
await humanPause(350, 900);
simulateClick(switchTrigger);
await sleep(1200);
continue;
}
if (passwordInput && !loggedPasswordPage) {
loggedPasswordPage = true;
log('步骤 7:正在等待密码页上的一次性验证码登录入口...');
}
await sleep(200);
}
throw new Error('无法切换到一次性验证码验证页面。URL: ' + location.href);
}
async function resendVerificationCode(step, timeout = 45000) {
if (step === 7) {
await prepareLoginCodeFlow();
}
const start = Date.now();
let action = null;
let loggedWaiting = false;
while (Date.now() - start < timeout) {
throwIfStopped();
action = findResendVerificationCodeTrigger({ allowDisabled: true });
if (action && isActionEnabled(action)) {
log(`步骤 ${step}:重新发送验证码按钮已可用。`);
await humanPause(350, 900);
simulateClick(action);
await sleep(1200);
return {
resent: true,
buttonText: getActionText(action),
};
}
if (action && !loggedWaiting) {
loggedWaiting = true;
log(`步骤 ${step}:正在等待重新发送验证码按钮变为可点击...`);
}
await sleep(250);
}
throw new Error('无法点击重新发送验证码按钮。URL: ' + location.href);
}
// ============================================================
// Step 2: Click Register
// ============================================================
async function step2_clickRegister() {
log('Step 2: Looking for Register/Sign up button...');
log('步骤 2:正在查找注册按钮...');
let registerBtn = null;
try {
@@ -68,8 +264,8 @@ async function step2_clickRegister() {
registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000);
} catch {
throw new Error(
'Could not find Register/Sign up button. ' +
'Check auth page DOM in DevTools. URL: ' + location.href
'未找到注册按钮。' +
'请在 DevTools 中检查认证页面 DOM。URL: ' + location.href
);
}
}
@@ -77,7 +273,7 @@ async function step2_clickRegister() {
await humanPause(450, 1200);
reportComplete(2);
simulateClick(registerBtn);
log('Step 2: Clicked Register button');
log('步骤 2:已点击注册按钮');
}
// ============================================================
@@ -86,9 +282,9 @@ async function step2_clickRegister() {
async function step3_fillEmailPassword(payload) {
const { email } = payload;
if (!email) throw new Error('No email provided. Paste email in Side Panel first.');
if (!email) throw new Error('未提供邮箱地址,请先在侧边栏粘贴邮箱。');
log(`Step 3: Filling email: ${email}`);
log(`步骤 3:正在填写邮箱:${email}`);
// Find email input
let emailInput = null;
@@ -98,40 +294,40 @@ async function step3_fillEmailPassword(payload) {
10000
);
} catch {
throw new Error('Could not find email input field on signup page. URL: ' + location.href);
throw new Error('在注册页未找到邮箱输入框。URL: ' + location.href);
}
await humanPause(500, 1400);
fillInput(emailInput, email);
log('Step 3: Email filled');
log('步骤 3:邮箱已填写');
// Check if password field is on the same page
let passwordInput = document.querySelector('input[type="password"]');
if (!passwordInput) {
// Need to submit email first to get to password page
log('Step 3: No password field yet, submitting email first...');
log('步骤 3:暂未发现密码输入框,先提交邮箱...');
const submitBtn = document.querySelector('button[type="submit"]')
|| await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
if (submitBtn) {
await humanPause(400, 1100);
simulateClick(submitBtn);
log('Step 3: Submitted email, waiting for password field...');
log('步骤 3:邮箱已提交,正在等待密码输入框...');
await sleep(2000);
}
try {
passwordInput = await waitForElement('input[type="password"]', 10000);
} catch {
throw new Error('Could not find password input after submitting email. URL: ' + location.href);
throw new Error('提交邮箱后仍未找到密码输入框。URL: ' + location.href);
}
}
if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
if (!payload.password) throw new Error('未提供密码,步骤 3 需要可用密码。');
await humanPause(600, 1500);
fillInput(passwordInput, payload.password);
log('Step 3: Password filled');
log('步骤 3:密码已填写');
// Report complete BEFORE submit, because submit causes page navigation
// which kills the content script connection
@@ -145,7 +341,7 @@ async function step3_fillEmailPassword(payload) {
if (submitBtn) {
await humanPause(500, 1300);
simulateClick(submitBtn);
log('Step 3: Form submitted');
log('步骤 3:表单已提交');
}
}
@@ -153,40 +349,177 @@ async function step3_fillEmailPassword(payload) {
// Fill Verification Code (used by step 4 and step 7)
// ============================================================
const INVALID_VERIFICATION_CODE_PATTERN = /代码不正确|验证码不正确|验证码错误|code\s+(?:is\s+)?incorrect|invalid\s+code|incorrect\s+code|try\s+again/i;
const VERIFICATION_PAGE_PATTERN = /检查您的收件箱|输入我们刚刚向|重新发送电子邮件|重新发送验证码|验证码|代码不正确|email\s+verification/i;
const OAUTH_CONSENT_PAGE_PATTERN = /使用\s*ChatGPT\s*登录到\s*Codex|login\s+to\s+codex|log\s+in\s+to\s+codex|authorize|授权/i;
const ADD_PHONE_PAGE_PATTERN = /add[\s-]*phone|添加手机号|手机号码|手机号|phone\s+number|telephone/i;
function getVerificationErrorText() {
const messages = [];
const selectors = [
'.react-aria-FieldError',
'[slot="errorMessage"]',
'[id$="-error"]',
'[data-invalid="true"] + *',
'[aria-invalid="true"] + *',
'[class*="error"]',
];
for (const selector of selectors) {
document.querySelectorAll(selector).forEach((el) => {
const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
if (text) {
messages.push(text);
}
});
}
const invalidInput = document.querySelector(`${VERIFICATION_CODE_INPUT_SELECTOR}[aria-invalid="true"], ${VERIFICATION_CODE_INPUT_SELECTOR}[data-invalid="true"]`);
if (invalidInput) {
const wrapper = invalidInput.closest('form, [data-rac], ._root_18qcl_51, div');
if (wrapper) {
const text = (wrapper.textContent || '').replace(/\s+/g, ' ').trim();
if (text) {
messages.push(text);
}
}
}
return messages.find((text) => INVALID_VERIFICATION_CODE_PATTERN.test(text)) || '';
}
function isStep5Ready() {
return Boolean(
document.querySelector('input[name="name"], input[autocomplete="name"], input[name="birthday"], input[name="age"], [role="spinbutton"][data-type="year"]')
);
}
function getPageTextSnapshot() {
return (document.body?.innerText || document.body?.textContent || '')
.replace(/\s+/g, ' ')
.trim();
}
function getPrimaryContinueButton() {
const continueBtn = document.querySelector(
'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107'
);
if (continueBtn && isVisibleElement(continueBtn)) {
return continueBtn;
}
const buttons = document.querySelectorAll('button, [role="button"]');
return Array.from(buttons).find((el) => isVisibleElement(el) && /继续|Continue/i.test(el.textContent || '')) || null;
}
function isVerificationPageStillVisible() {
if (getVerificationCodeTarget()) return true;
if (findResendVerificationCodeTrigger({ allowDisabled: true })) return true;
if (document.querySelector('form[action*="email-verification" i]')) return true;
return VERIFICATION_PAGE_PATTERN.test(getPageTextSnapshot());
}
function isAddPhonePageReady() {
const path = `${location.pathname || ''} ${location.href || ''}`;
if (/\/add-phone(?:[/?#]|$)/i.test(path)) return true;
const phoneInput = document.querySelector(
'input[type="tel"]:not([maxlength="6"]), input[name*="phone" i], input[id*="phone" i], input[autocomplete="tel"]'
);
if (phoneInput && isVisibleElement(phoneInput)) {
return true;
}
return ADD_PHONE_PAGE_PATTERN.test(getPageTextSnapshot());
}
function isStep8Ready() {
const continueBtn = getPrimaryContinueButton();
if (!continueBtn) return false;
if (isVerificationPageStillVisible()) return false;
if (isAddPhonePageReady()) return false;
return OAUTH_CONSENT_PAGE_PATTERN.test(getPageTextSnapshot());
}
async function waitForVerificationSubmitOutcome(step, timeout) {
const resolvedTimeout = timeout ?? (step === 7 ? 30000 : 12000);
const start = Date.now();
while (Date.now() - start < resolvedTimeout) {
throwIfStopped();
const errorText = getVerificationErrorText();
if (errorText) {
return { invalidCode: true, errorText };
}
if (step === 4 && isStep5Ready()) {
return { success: true };
}
if (step === 7 && isStep8Ready()) {
return { success: true };
}
if (step === 7 && isAddPhonePageReady()) {
return { success: true, addPhonePage: true };
}
await sleep(150);
}
if (isVerificationPageStillVisible()) {
return {
invalidCode: true,
errorText: getVerificationErrorText() || '提交后仍停留在验证码页面,准备重新发送验证码。',
};
}
return { success: true, assumed: true };
}
async function fillVerificationCode(step, payload) {
const { code } = payload;
if (!code) throw new Error('No verification code provided.');
if (!code) throw new Error('未提供验证码。');
log(`Step ${step}: Filling verification code: ${code}`);
log(`步骤 ${step}:正在填写验证码:${code}`);
if (step === 7) {
await prepareLoginCodeFlow();
}
// Find code input — could be a single input or multiple separate inputs
let codeInput = null;
try {
codeInput = await waitForElement(
'input[name="code"], input[name="otp"], input[type="text"][maxlength="6"], input[aria-label*="code"], input[placeholder*="code"], input[placeholder*="Code"], input[inputmode="numeric"]',
10000
);
codeInput = await waitForElement(VERIFICATION_CODE_INPUT_SELECTOR, 10000);
} catch {
// Check for multiple single-digit inputs (common pattern)
const singleInputs = document.querySelectorAll('input[maxlength="1"]');
if (singleInputs.length >= 6) {
log(`Step ${step}: Found single-digit code inputs, filling individually...`);
log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`);
for (let i = 0; i < 6 && i < singleInputs.length; i++) {
fillInput(singleInputs[i], code[i]);
await sleep(100);
}
await sleep(1000);
reportComplete(step);
return;
const outcome = await waitForVerificationSubmitOutcome(step);
if (outcome.invalidCode) {
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
} else if (outcome.addPhonePage) {
log(`步骤 ${step}:验证码已通过,并已跳转到手机号页面。`, 'ok');
} else {
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}`, 'ok');
}
return outcome;
}
throw new Error('Could not find verification code input. URL: ' + location.href);
throw new Error('未找到验证码输入框。URL: ' + location.href);
}
fillInput(codeInput, code);
log(`Step ${step}: Code filled`);
log(`步骤 ${step}:验证码已填写`);
// Report complete BEFORE submit (page may navigate away)
reportComplete(step);
// Submit
await sleep(500);
@@ -196,8 +529,19 @@ async function fillVerificationCode(step, payload) {
if (submitBtn) {
await humanPause(450, 1200);
simulateClick(submitBtn);
log(`Step ${step}: Verification submitted`);
log(`步骤 ${step}:验证码已提交`);
}
const outcome = await waitForVerificationSubmitOutcome(step);
if (outcome.invalidCode) {
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
} else if (outcome.addPhonePage) {
log(`步骤 ${step}:验证码已通过,并已跳转到手机号页面。`, 'ok');
} else {
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}`, 'ok');
}
return outcome;
}
// ============================================================
@@ -206,9 +550,9 @@ async function fillVerificationCode(step, payload) {
async function step6_login(payload) {
const { email, password } = payload;
if (!email) throw new Error('No email provided for login.');
if (!email) throw new Error('登录时缺少邮箱地址。');
log(`Step 6: Logging in with ${email}...`);
log(`步骤 6:正在使用 ${email} 登录...`);
// Wait for email input on the auth page
let emailInput = null;
@@ -218,12 +562,12 @@ async function step6_login(payload) {
15000
);
} catch {
throw new Error('Could not find email input on login page. URL: ' + location.href);
throw new Error('在登录页未找到邮箱输入框。URL: ' + location.href);
}
await humanPause(500, 1400);
fillInput(emailInput, email);
log('Step 6: Email filled');
log('步骤 6:邮箱已填写');
// Submit email
await sleep(500);
@@ -232,7 +576,7 @@ async function step6_login(payload) {
if (submitBtn1) {
await humanPause(400, 1100);
simulateClick(submitBtn1);
log('Step 6: Submitted email');
log('步骤 6:邮箱已提交');
}
await sleep(2000);
@@ -240,7 +584,7 @@ async function step6_login(payload) {
// Check for password field
const passwordInput = document.querySelector('input[type="password"]');
if (passwordInput) {
log('Step 6: Password field found, filling password...');
log('步骤 6:已找到密码输入框,正在填写密码...');
await humanPause(550, 1450);
fillInput(passwordInput, password);
@@ -253,13 +597,13 @@ async function step6_login(payload) {
if (submitBtn2) {
await humanPause(450, 1200);
simulateClick(submitBtn2);
log('Step 6: Submitted password, may need verification code (step 7)');
log('步骤 6:密码已提交,可能还需要验证码(步骤 7');
}
return;
}
// No password field — OTP flow
log('Step 6: No password field. OTP flow or auto-redirect.');
log('步骤 6:未发现密码输入框,可能进入验证码流程或自动跳转。');
reportComplete(6, { needsOTP: true });
}
@@ -271,7 +615,7 @@ async function step6_login(payload) {
// Background performs the actual click through the debugger Input API.
async function step8_findAndClick() {
log('Step 8: Looking for OAuth consent "继续" button...');
log('步骤 8:正在查找 OAuth 同意页的“继续”按钮...');
const continueBtn = await findContinueButton();
await waitForButtonEnabled(continueBtn);
@@ -282,7 +626,7 @@ async function step8_findAndClick() {
await sleep(250);
const rect = getSerializableRect(continueBtn);
log('Step 8: Found "继续" button and prepared debugger click coordinates.');
log('步骤 8:已找到“继续”按钮并准备好调试器点击坐标。');
return {
rect,
buttonText: (continueBtn.textContent || '').trim(),
@@ -291,18 +635,20 @@ async function step8_findAndClick() {
}
async function findContinueButton() {
try {
return await waitForElement(
'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
10000
);
} catch {
try {
return await waitForElementByText('button', /继续|Continue/, 5000);
} catch {
throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href);
const start = Date.now();
while (Date.now() - start < 10000) {
throwIfStopped();
if (isAddPhonePageReady()) {
throw new Error('当前页面已进入手机号页面,不是 OAuth 授权同意页。URL: ' + location.href);
}
const button = getPrimaryContinueButton();
if (button && isStep8Ready()) {
return button;
}
await sleep(150);
}
throw new Error('在 OAuth 同意页未找到“继续”按钮,或页面尚未进入授权同意状态。URL: ' + location.href);
}
async function waitForButtonEnabled(button, timeout = 8000) {
@@ -312,7 +658,7 @@ async function waitForButtonEnabled(button, timeout = 8000) {
if (isButtonEnabled(button)) return;
await sleep(150);
}
throw new Error('"继续" button stayed disabled for too long. URL: ' + location.href);
throw new Error('继续”按钮长时间不可点击。URL: ' + location.href);
}
function isButtonEnabled(button) {
@@ -324,7 +670,7 @@ function isButtonEnabled(button) {
function getSerializableRect(el) {
const rect = el.getBoundingClientRect();
if (!rect.width || !rect.height) {
throw new Error('"继续" button has no clickable size after scrolling. URL: ' + location.href);
throw new Error('滚动后“继续”按钮没有可点击尺寸。URL: ' + location.href);
}
return {
@@ -343,16 +689,16 @@ function getSerializableRect(el) {
async function step5_fillNameBirthday(payload) {
const { firstName, lastName, age, year, month, day } = payload;
if (!firstName || !lastName) throw new Error('No name data provided.');
if (!firstName || !lastName) throw new Error('未提供姓名数据。');
const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null);
const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value)));
if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) {
throw new Error('No birthday or age data provided.');
throw new Error('未提供生日或年龄数据。');
}
const fullName = `${firstName} ${lastName}`;
log(`Step 5: Filling name: ${fullName}`);
log(`步骤 5:正在填写姓名:${fullName}`);
// Actual DOM structure:
// - Full name: <input name="name" placeholder="全名" type="text">
@@ -367,11 +713,11 @@ async function step5_fillNameBirthday(payload) {
10000
);
} catch {
throw new Error('Could not find name input. URL: ' + location.href);
throw new Error('未找到姓名输入框。URL: ' + location.href);
}
await humanPause(500, 1300);
fillInput(nameInput, fullName);
log(`Step 5: Name filled: ${fullName}`);
log(`步骤 5:姓名已填写:${fullName}`);
let birthdayMode = false;
let ageInput = null;
@@ -393,7 +739,7 @@ async function step5_fillNameBirthday(payload) {
if (birthdayMode) {
if (!hasBirthdayData) {
throw new Error('Birthday field detected, but no birthday data provided.');
throw new Error('检测到生日字段,但未提供生日数据。');
}
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
@@ -401,7 +747,7 @@ async function step5_fillNameBirthday(payload) {
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
if (yearSpinner && monthSpinner && daySpinner) {
log('Step 5: Birthday fields detected, filling birthday...');
log('步骤 5:检测到生日字段,正在填写生日...');
async function setSpinButton(el, value) {
el.focus();
@@ -429,7 +775,7 @@ async function step5_fillNameBirthday(payload) {
await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
await humanPause(250, 650);
await setSpinButton(daySpinner, String(day).padStart(2, '0'));
log(`Step 5: Birthday filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
log(`步骤 5:生日已填写:${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
}
const hiddenBirthday = document.querySelector('input[name="birthday"]');
@@ -437,17 +783,17 @@ async function step5_fillNameBirthday(payload) {
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
hiddenBirthday.value = dateStr;
hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
log(`Step 5: Hidden birthday input set: ${dateStr}`);
log(`步骤 5:已设置隐藏生日输入框:${dateStr}`);
}
} else if (ageInput) {
if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) {
throw new Error('Age field detected, but no age data provided.');
throw new Error('检测到年龄字段,但未提供年龄数据。');
}
await humanPause(500, 1300);
fillInput(ageInput, String(resolvedAge));
log(`Step 5: Age filled: ${resolvedAge}`);
log(`步骤 5:年龄已填写:${resolvedAge}`);
} else {
throw new Error('Could not find birthday or age input. URL: ' + location.href);
throw new Error('未找到生日或年龄输入项。URL: ' + location.href);
}
// Click "完成帐户创建" button
@@ -461,6 +807,6 @@ async function step5_fillNameBirthday(payload) {
if (completeBtn) {
await humanPause(500, 1300);
simulateClick(completeBtn);
log('Step 5: Clicked "完成帐户创建"');
log('步骤 5:已点击“完成帐户创建');
}
}
+26 -26
View File
@@ -13,7 +13,7 @@ const SCRIPT_SOURCE = (() => {
})();
const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
let flowStopped = false;
chrome.runtime.onMessage.addListener((message) => {
@@ -50,14 +50,14 @@ function waitForElement(selector, timeout = 10000) {
const existing = document.querySelector(selector);
if (existing) {
console.log(LOG_PREFIX, `Found immediately: ${selector}`);
log(`Found element: ${selector}`);
console.log(LOG_PREFIX, `立即找到元素: ${selector}`);
log(`已找到元素:${selector}`);
resolve(existing);
return;
}
console.log(LOG_PREFIX, `Waiting for: ${selector} (timeout: ${timeout}ms)`);
log(`Waiting for selector: ${selector}...`);
console.log(LOG_PREFIX, `等待元素: ${selector}(超时 ${timeout}ms`);
log(`正在等待选择器:${selector}...`);
let settled = false;
let stopTimer = null;
@@ -78,8 +78,8 @@ function waitForElement(selector, timeout = 10000) {
const el = document.querySelector(selector);
if (el) {
cleanup();
console.log(LOG_PREFIX, `Found after wait: ${selector}`);
log(`Found element: ${selector}`);
console.log(LOG_PREFIX, `等待后找到元素: ${selector}`);
log(`已找到元素:${selector}`);
resolve(el);
}
});
@@ -91,7 +91,7 @@ function waitForElement(selector, timeout = 10000) {
const timer = setTimeout(() => {
cleanup();
const msg = `Timeout waiting for ${selector} after ${timeout}ms on ${location.href}`;
const msg = ` ${location.href} 等待 ${selector} 超时,已超过 ${timeout}ms`;
console.error(LOG_PREFIX, msg);
reject(new Error(msg));
}, timeout);
@@ -132,14 +132,14 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
const existing = search();
if (existing) {
console.log(LOG_PREFIX, `Found by text immediately: ${containerSelector} matching ${textPattern}`);
log(`Found element by text: ${textPattern}`);
console.log(LOG_PREFIX, `立即按文本找到元素: ${containerSelector} 匹配 ${textPattern}`);
log(`已按文本找到元素:${textPattern}`);
resolve(existing);
return;
}
console.log(LOG_PREFIX, `Waiting for text match: ${containerSelector} / ${textPattern}`);
log(`Waiting for element with text: ${textPattern}...`);
console.log(LOG_PREFIX, `等待文本匹配: ${containerSelector} / ${textPattern}`);
log(`正在等待包含文本的元素:${textPattern}...`);
let settled = false;
let stopTimer = null;
@@ -160,8 +160,8 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
const el = search();
if (el) {
cleanup();
console.log(LOG_PREFIX, `Found by text after wait: ${textPattern}`);
log(`Found element by text: ${textPattern}`);
console.log(LOG_PREFIX, `等待后按文本找到元素: ${textPattern}`);
log(`已按文本找到元素:${textPattern}`);
resolve(el);
}
});
@@ -173,7 +173,7 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
const timer = setTimeout(() => {
cleanup();
const msg = `Timeout waiting for text "${textPattern}" in "${containerSelector}" after ${timeout}ms on ${location.href}`;
const msg = `${location.href}${containerSelector} 中等待文本 "${textPattern}" 超时,已超过 ${timeout}ms`;
console.error(LOG_PREFIX, msg);
reject(new Error(msg));
}, timeout);
@@ -206,8 +206,8 @@ function fillInput(el, value) {
nativeInputValueSetter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
console.log(LOG_PREFIX, `Filled input ${el.name || el.id || el.type} with: ${value}`);
log(`Filled input [${el.name || el.id || el.type || 'unknown'}]`);
console.log(LOG_PREFIX, `已填写输入框 ${el.name || el.id || el.type}: ${value}`);
log(`已填写输入框 [${el.name || el.id || el.type || '未知'}]`);
}
/**
@@ -219,8 +219,8 @@ function fillSelect(el, value) {
throwIfStopped();
el.value = value;
el.dispatchEvent(new Event('change', { bubbles: true }));
console.log(LOG_PREFIX, `Selected value ${value} in ${el.name || el.id}`);
log(`Selected [${el.name || el.id || 'unknown'}] = ${value}`);
console.log(LOG_PREFIX, `已在 ${el.name || el.id} 中选择值: ${value}`);
log(`已选择 [${el.name || el.id || '未知'}] = ${value}`);
}
/**
@@ -242,7 +242,7 @@ function log(message, level = 'info') {
* Report that this content script is loaded and ready.
*/
function reportReady() {
console.log(LOG_PREFIX, 'Content script ready');
console.log(LOG_PREFIX, '内容脚本已就绪');
chrome.runtime.sendMessage({
type: 'CONTENT_SCRIPT_READY',
source: SCRIPT_SOURCE,
@@ -258,8 +258,8 @@ function reportReady() {
* @param {Object} data - Step output data
*/
function reportComplete(step, data = {}) {
console.log(LOG_PREFIX, `Step ${step} completed`, data);
log(`Step ${step} completed successfully`, 'ok');
console.log(LOG_PREFIX, `步骤 ${step} 已完成`, data);
log(`步骤 ${step} 已成功完成`, 'ok');
chrome.runtime.sendMessage({
type: 'STEP_COMPLETE',
source: SCRIPT_SOURCE,
@@ -275,8 +275,8 @@ function reportComplete(step, data = {}) {
* @param {string} errorMessage
*/
function reportError(step, errorMessage) {
console.error(LOG_PREFIX, `Step ${step} failed: ${errorMessage}`);
log(`Step ${step} failed: ${errorMessage}`, 'error');
console.error(LOG_PREFIX, `步骤 ${step} 失败: ${errorMessage}`);
log(`步骤 ${step} 失败:${errorMessage}`, 'error');
chrome.runtime.sendMessage({
type: 'STEP_ERROR',
source: SCRIPT_SOURCE,
@@ -293,8 +293,8 @@ function reportError(step, errorMessage) {
function simulateClick(el) {
throwIfStopped();
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
console.log(LOG_PREFIX, `Clicked: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
log(`Clicked [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`);
console.log(LOG_PREFIX, `已点击: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
log(`已点击 [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`);
}
/**
+188 -52
View File
@@ -33,7 +33,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse({ ok: true });
}).catch(err => {
if (isStopError(err)) {
log(`Step ${message.step}: Stopped by user.`, 'warn');
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
@@ -46,65 +46,199 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
async function handleStep(step, payload) {
switch (step) {
case 1: return await step1_getOAuthLink();
case 1: return await step1_getOAuthLink(payload);
case 9: return await step9_vpsVerify(payload);
default:
throw new Error(`vps-panel.js does not handle step ${step}`);
throw new Error(`vps-panel.js 不处理步骤 ${step}`);
}
}
function isVisibleElement(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function getActionText(el) {
return [
el?.textContent,
el?.value,
el?.getAttribute?.('aria-label'),
el?.getAttribute?.('title'),
]
.filter(Boolean)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
}
function findManagementKeyInput() {
const candidates = document.querySelectorAll(
'.LoginPage-module__loginCard___OgP-R input[type="password"], input[placeholder*="管理密钥"], input[aria-label*="管理密钥"]'
);
return Array.from(candidates).find(isVisibleElement) || null;
}
function findManagementLoginButton() {
const candidates = document.querySelectorAll('.LoginPage-module__loginCard___OgP-R button, .LoginPage-module__loginCard___OgP-R .btn');
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el)) return false;
return /登录|login/i.test(getActionText(el));
}) || null;
}
function findRememberPasswordCheckbox() {
const candidates = document.querySelectorAll('.LoginPage-module__loginCard___OgP-R input[type="checkbox"]');
return Array.from(candidates).find((el) => {
const label = el.closest('label');
const text = getActionText(label || el);
return /记住密码|remember/i.test(text);
}) || null;
}
function findOAuthNavLink() {
const candidates = document.querySelectorAll('a[href*="#/oauth"], a.nav-item, button, [role="link"], [role="button"]');
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el)) return false;
const text = getActionText(el);
const href = el.getAttribute('href') || '';
return href.includes('#/oauth') || /oauth/i.test(text);
}) || null;
}
function findCodexOAuthHeader() {
const candidates = document.querySelectorAll('.card-header, [class*="cardHeader"], .card, [class*="card"]');
return Array.from(candidates).find((el) => {
if (!isVisibleElement(el)) return false;
const text = (el.textContent || '').toLowerCase();
return text.includes('codex') && text.includes('oauth');
}) || 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');
return Array.from(candidates).find((el) => isVisibleElement(el) && /登录|login/i.test(getActionText(el))) || null;
}
function findAuthUrlElement() {
const candidates = document.querySelectorAll('[class*="authUrlValue"], .OAuthPage-module__authUrlValue___axvUJ');
return Array.from(candidates).find((el) => isVisibleElement(el) && /^https?:\/\//i.test((el.textContent || '').trim())) || null;
}
async function ensureOAuthManagementPage(vpsPassword, step = 1, timeout = 45000) {
const start = Date.now();
let lastLoginAttemptAt = 0;
let lastOauthNavAttemptAt = 0;
while (Date.now() - start < timeout) {
throwIfStopped();
const authUrlEl = findAuthUrlElement();
if (authUrlEl) {
return { header: findCodexOAuthHeader(), authUrlEl };
}
const oauthHeader = findCodexOAuthHeader();
if (oauthHeader) {
return { header: oauthHeader, authUrlEl: null };
}
const managementKeyInput = findManagementKeyInput();
const managementLoginButton = findManagementLoginButton();
if (managementKeyInput && managementLoginButton) {
if (!vpsPassword) {
throw new Error('VPS 面板需要管理密钥,请先在侧边栏填写 VPS Key(管理密钥)。');
}
if ((managementKeyInput.value || '') !== vpsPassword) {
await humanPause(350, 900);
fillInput(managementKeyInput, vpsPassword);
log(`步骤 ${step}:已填写 VPS 管理密钥。`);
}
const rememberCheckbox = findRememberPasswordCheckbox();
if (rememberCheckbox && !rememberCheckbox.checked) {
simulateClick(rememberCheckbox);
log(`步骤 ${step}:已勾选 VPS 面板“记住密码”。`);
await sleep(300);
}
if (Date.now() - lastLoginAttemptAt > 3000) {
lastLoginAttemptAt = Date.now();
await humanPause(350, 900);
simulateClick(managementLoginButton);
log(`步骤 ${step}:已提交 VPS 管理登录。`);
}
await sleep(1500);
continue;
}
const oauthNavLink = findOAuthNavLink();
if (oauthNavLink && Date.now() - lastOauthNavAttemptAt > 2000) {
lastOauthNavAttemptAt = Date.now();
await humanPause(300, 800);
simulateClick(oauthNavLink);
log(`步骤 ${step}:已打开“OAuth 登录”导航。`);
await sleep(1200);
continue;
}
await sleep(250);
}
throw new Error('无法进入 VPS 的 OAuth 管理页面,请检查面板是否正常加载。URL: ' + location.href);
}
// ============================================================
// Step 1: Get OAuth Link
// ============================================================
async function step1_getOAuthLink() {
log('Step 1: Waiting for VPS panel to load (auto-login may take a moment)...');
async function step1_getOAuthLink(payload) {
const { vpsPassword } = payload || {};
// The page may start at #/login and auto-redirect to #/oauth.
// Wait for the Codex OAuth card to appear (up to 30s for auto-login + redirect).
let loginBtn = null;
try {
// Wait for any card-header containing "Codex" to appear
const header = await waitForElementByText('.card-header', /codex/i, 30000);
loginBtn = header.querySelector('button.btn.btn-primary, button.btn');
log('Step 1: Found Codex OAuth card');
} catch {
throw new Error(
'Codex OAuth card did not appear after 30s. Page may still be loading or not logged in. ' +
'Current URL: ' + location.href
);
}
log('步骤 1:正在等待 VPS 面板加载并进入 OAuth 页面...');
if (!loginBtn) {
throw new Error('Found Codex OAuth card but no login button inside it. URL: ' + location.href);
}
const { header, authUrlEl: existingAuthUrlEl } = await ensureOAuthManagementPage(vpsPassword, 1);
let authUrlEl = existingAuthUrlEl;
// Check if button is disabled (already clicked / loading)
if (loginBtn.disabled) {
log('Step 1: Login button is disabled (already loading), waiting for auth URL...');
if (!authUrlEl) {
const loginBtn = findOAuthCardLoginButton(header);
if (!loginBtn) {
throw new Error('已找到 Codex OAuth 卡片,但卡片内没有登录按钮。URL: ' + location.href);
}
if (loginBtn.disabled) {
log('步骤 1:OAuth 登录按钮当前不可用,正在等待授权链接出现...');
} else {
await humanPause(500, 1400);
simulateClick(loginBtn);
log('步骤 1:已点击 OAuth 登录按钮,正在等待授权链接...');
}
try {
authUrlEl = await waitForElement('[class*="authUrlValue"]', 15000);
} catch {
throw new Error(
'点击 OAuth 登录按钮后未出现授权链接。' +
'请检查 VPS 面板服务是否正在运行。URL: ' + location.href
);
}
} else {
await humanPause(500, 1400);
simulateClick(loginBtn);
log('Step 1: Clicked login button, waiting for auth URL...');
}
// Wait for the auth URL to appear in the specific div
let authUrlEl = null;
try {
authUrlEl = await waitForElement('[class*="authUrlValue"]', 15000);
} catch {
throw new Error(
'Auth URL did not appear after clicking login. ' +
'Check if VPS panel is logged in and Codex service is running. URL: ' + location.href
);
log('步骤 1:VPS 面板上已显示授权链接。');
}
const oauthUrl = (authUrlEl.textContent || '').trim();
if (!oauthUrl || !oauthUrl.startsWith('http')) {
throw new Error(`Invalid OAuth URL found: "${oauthUrl.slice(0, 50)}". Expected URL starting with http.`);
throw new Error(`拿到的 OAuth 链接无效:\"${oauthUrl.slice(0, 50)}\"。应为 http 开头的 URL。`);
}
log(`Step 1: OAuth URL obtained: ${oauthUrl.slice(0, 80)}...`, 'ok');
log(`步骤 1:已获取 OAuth 链接:${oauthUrl.slice(0, 80)}...`, 'ok');
reportComplete(1, { oauthUrl });
}
@@ -113,19 +247,21 @@ async function step1_getOAuthLink() {
// ============================================================
async function step9_vpsVerify(payload) {
await ensureOAuthManagementPage(payload?.vpsPassword, 9);
// Get localhostUrl from payload (passed directly by background) or fallback to state
let localhostUrl = payload?.localhostUrl;
if (!localhostUrl) {
log('Step 9: localhostUrl not in payload, fetching from state...');
log('步骤 9payload 中没有 localhostUrl,正在从状态中读取...');
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
localhostUrl = state.localhostUrl;
}
if (!localhostUrl) {
throw new Error('No localhost URL found. Complete step 8 first.');
throw new Error('未找到 localhost 回调地址,请先完成步骤 8。');
}
log(`Step 9: Got localhostUrl: ${localhostUrl.slice(0, 60)}...`);
log(`步骤 9:已获取 localhostUrl${localhostUrl.slice(0, 60)}...`);
log('Step 9: Looking for callback URL input...');
log('步骤 9:正在查找回调地址输入框...');
// Find the callback URL input
// Actual DOM: <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=...">
@@ -136,13 +272,13 @@ async function step9_vpsVerify(payload) {
try {
urlInput = await waitForElement('input[placeholder*="localhost"]', 5000);
} catch {
throw new Error('Could not find callback URL input on VPS panel. URL: ' + location.href);
throw new Error('在 VPS 面板中未找到回调地址输入框。URL: ' + location.href);
}
}
await humanPause(600, 1500);
fillInput(urlInput, localhostUrl);
log(`Step 9: Filled callback URL: ${localhostUrl.slice(0, 80)}...`);
log(`步骤 9:已填写回调地址:${localhostUrl.slice(0, 80)}...`);
// Find and click "提交回调 URL" button
let submitBtn = null;
@@ -156,26 +292,26 @@ async function step9_vpsVerify(payload) {
try {
submitBtn = await waitForElementByText('button.btn', /提交回调/, 5000);
} catch {
throw new Error('Could not find "提交回调 URL" button. URL: ' + location.href);
throw new Error('未找到“提交回调 URL”按钮。URL: ' + location.href);
}
}
await humanPause(450, 1200);
simulateClick(submitBtn);
log('Step 9: Clicked "提交回调 URL", waiting for authentication result...');
log('步骤 9:已点击“提交回调 URL”,正在等待认证结果...');
// Wait for "认证成功!" status badge to appear
try {
await waitForElementByText('.status-badge, [class*="status"]', /认证成功/, 30000);
log('Step 9: Authentication successful!', 'ok');
log('步骤 9:认证成功!', 'ok');
} catch {
// Check if there's an error message instead
const statusEl = document.querySelector('.status-badge, [class*="status"]');
const statusText = statusEl ? statusEl.textContent : 'unknown';
if (/成功|success/i.test(statusText)) {
log('Step 9: Authentication successful!', 'ok');
log('步骤 9:认证成功!', 'ok');
} else {
log(`Step 9: Status after submit: "${statusText}". May still be processing.`, 'warn');
log(`步骤 9:提交后的状态为“${statusText}”,可能仍在处理中。`, 'warn');
}
}