feat: add Auto Run mode for full automated flow
- Background: add step completion waiting (promise-based), autoRun/resumeAutoRun chains steps 1→2 (pause for email) → 3→4→5→6→7→8→9 automatically - Side Panel: add "Auto Run" button and "Continue Auto" bar for email pause point - signup-page.js: add step 8 handler to click "继续" on OAuth consent page - Fix all steps to reportComplete before form submit (prevents connection loss on navigation) - qq-mail.js: fallback after 3 attempts to first matching email - vps-panel.js: step 9 waits for "认证成功!" status badge
This commit is contained in:
+197
-30
@@ -200,14 +200,15 @@ async function handleMessage(message, sender) {
|
||||
case 'STEP_COMPLETE': {
|
||||
await setStepStatus(message.step, 'completed');
|
||||
await addLog(`Step ${message.step} completed`, 'ok');
|
||||
// Store step-specific data
|
||||
await handleStepData(message.step, message.payload);
|
||||
notifyStepComplete(message.step, message.payload);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'STEP_ERROR': {
|
||||
await setStepStatus(message.step, 'failed');
|
||||
await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
|
||||
notifyStepError(message.step, message.error);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -231,6 +232,19 @@ async function handleMessage(message, sender) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'AUTO_RUN': {
|
||||
autoRun(); // fire-and-forget, runs in background
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'RESUME_AUTO_RUN': {
|
||||
if (message.payload.email) {
|
||||
await setState({ email: message.payload.email });
|
||||
}
|
||||
resumeAutoRun(); // fire-and-forget
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// Side panel data updates
|
||||
case 'SAVE_EMAIL': {
|
||||
await setState({ email: message.payload.email });
|
||||
@@ -277,6 +291,37 @@ async function handleStepData(step, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step Completion Waiting
|
||||
// ============================================================
|
||||
|
||||
// Map of step -> { resolve, reject } for waiting on step completion
|
||||
const stepWaiters = new Map();
|
||||
|
||||
function waitForStepComplete(step, timeoutMs = 120000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
stepWaiters.delete(step);
|
||||
reject(new Error(`Step ${step} timed out after ${timeoutMs / 1000}s`));
|
||||
}, timeoutMs);
|
||||
|
||||
stepWaiters.set(step, {
|
||||
resolve: (data) => { clearTimeout(timer); stepWaiters.delete(step); resolve(data); },
|
||||
reject: (err) => { clearTimeout(timer); stepWaiters.delete(step); reject(err); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function notifyStepComplete(step, payload) {
|
||||
const waiter = stepWaiters.get(step);
|
||||
if (waiter) waiter.resolve(payload);
|
||||
}
|
||||
|
||||
function notifyStepError(step, error) {
|
||||
const waiter = stepWaiters.get(step);
|
||||
if (waiter) waiter.reject(new Error(error));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step Execution
|
||||
// ============================================================
|
||||
@@ -313,11 +358,104 @@ async function executeStep(step) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a step and wait for it to complete before returning.
|
||||
* @param {number} step
|
||||
* @param {number} delayAfter - ms to wait after completion (for page transitions)
|
||||
*/
|
||||
async function executeStepAndWait(step, delayAfter = 2000) {
|
||||
const promise = waitForStepComplete(step, 120000);
|
||||
await executeStep(step);
|
||||
await promise;
|
||||
// Extra delay for page transitions / DOM updates
|
||||
if (delayAfter > 0) {
|
||||
await new Promise(r => setTimeout(r, delayAfter));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Auto Run Flow
|
||||
// ============================================================
|
||||
|
||||
let autoRunActive = false;
|
||||
|
||||
async function autoRun() {
|
||||
if (autoRunActive) {
|
||||
await addLog('Auto run already in progress', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
autoRunActive = true;
|
||||
await setState({ autoRunning: true });
|
||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'running' } }).catch(() => {});
|
||||
|
||||
try {
|
||||
// Phase 1: Steps 1-2 (get OAuth link, open signup)
|
||||
await addLog('=== Auto Run Phase 1: Get OAuth link & open signup ===', 'info');
|
||||
await executeStepAndWait(1, 2000);
|
||||
await executeStepAndWait(2, 2000);
|
||||
|
||||
// Pause: ask user to generate DuckDuckGo email
|
||||
await addLog('=== Auto Run PAUSED: Please paste DuckDuckGo email and click "Continue Auto" ===', 'warn');
|
||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'waiting_email' } }).catch(() => {});
|
||||
|
||||
// Wait here — resumed by RESUME_AUTO_RUN message from side panel
|
||||
|
||||
} catch (err) {
|
||||
await addLog(`Auto run failed at Phase 1: ${err.message}`, 'error');
|
||||
autoRunActive = false;
|
||||
await setState({ autoRunning: false });
|
||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped' } }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function resumeAutoRun() {
|
||||
try {
|
||||
const state = await getState();
|
||||
if (!state.email) {
|
||||
await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 2: Steps 3-9 (fill form, get codes, login, OAuth, verify)
|
||||
await addLog('=== Auto Run Phase 2: Register, verify, login, complete OAuth ===', 'info');
|
||||
|
||||
await executeStepAndWait(3, 3000); // Fill email/password → page navigates to code input
|
||||
await executeStepAndWait(4, 2000); // Get signup code from QQ Mail → fill in
|
||||
await executeStepAndWait(5, 3000); // Fill name/birthday → page navigates to add-phone
|
||||
await executeStepAndWait(6, 3000); // Login via OAuth URL → fill email/password
|
||||
await executeStepAndWait(7, 2000); // Get login code from QQ Mail → fill in
|
||||
await executeStepAndWait(8, 2000); // Click "继续" → localhost redirect captured
|
||||
await executeStepAndWait(9, 1000); // VPS verify → wait for "认证成功!"
|
||||
|
||||
await addLog('=== Auto Run COMPLETE! All 9 steps finished successfully ===', 'ok');
|
||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete' } }).catch(() => {});
|
||||
|
||||
} catch (err) {
|
||||
await addLog(`Auto run failed: ${err.message}`, 'error');
|
||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped' } }).catch(() => {});
|
||||
} finally {
|
||||
autoRunActive = false;
|
||||
await setState({ autoRunning: false });
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 1: Get OAuth Link (via vps-panel.js)
|
||||
// ============================================================
|
||||
|
||||
async function executeStep1(state) {
|
||||
// Ensure VPS panel tab is open
|
||||
const alive = await isTabAlive('vps-panel');
|
||||
if (!alive) {
|
||||
await addLog('Step 1: Opening VPS panel...');
|
||||
await chrome.tabs.create({ url: 'http://154.26.182.181:8317/management.html#/oauth', active: true });
|
||||
} else {
|
||||
const tabId = await getTabId('vps-panel');
|
||||
if (tabId) await chrome.tabs.update(tabId, { active: true });
|
||||
}
|
||||
|
||||
// Send command — will queue if content script not ready yet, flush on READY signal
|
||||
await sendToContentScript('vps-panel', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 1,
|
||||
@@ -439,20 +577,29 @@ async function executeStep5(state) {
|
||||
// ============================================================
|
||||
|
||||
async function executeStep6(state) {
|
||||
const alive = await isTabAlive('chatgpt');
|
||||
if (!alive) {
|
||||
await addLog('Step 6: Opening ChatGPT...');
|
||||
await chrome.tabs.create({ url: 'https://chatgpt.com/', active: true });
|
||||
} else {
|
||||
const tabId = await getTabId('chatgpt');
|
||||
if (tabId) await chrome.tabs.update(tabId, { active: true });
|
||||
if (!state.oauthUrl) {
|
||||
throw new Error('No OAuth URL. Complete step 1 first.');
|
||||
}
|
||||
if (!state.email) {
|
||||
throw new Error('No email. Complete step 3 first.');
|
||||
}
|
||||
|
||||
await sendToContentScript('chatgpt', {
|
||||
// Open the OAuth URL again in a new tab to start the login flow
|
||||
// Close the old signup tab first (it's on add-phone page, not needed)
|
||||
const oldSignupTabId = await getTabId('signup-page');
|
||||
if (oldSignupTabId) {
|
||||
try { await chrome.tabs.remove(oldSignupTabId); } catch {}
|
||||
}
|
||||
|
||||
await addLog(`Step 6: Opening OAuth URL for login: ${state.oauthUrl.slice(0, 60)}...`);
|
||||
await chrome.tabs.create({ url: state.oauthUrl, active: true });
|
||||
|
||||
// signup-page.js will inject (same auth.openai.com domain) and handle login
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 6,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
payload: { email: state.email, password: state.password || 'mimashisha0.0' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -490,18 +637,18 @@ async function executeStep7(state) {
|
||||
if (result && result.code) {
|
||||
await addLog(`Step 7: Got login verification code: ${result.code}`);
|
||||
|
||||
// Switch to ChatGPT tab and fill code
|
||||
const chatgptTabId = await getTabId('chatgpt');
|
||||
if (chatgptTabId) {
|
||||
await chrome.tabs.update(chatgptTabId, { active: true });
|
||||
await sendToContentScript('chatgpt', {
|
||||
// Switch to signup/auth tab and fill code
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
if (signupTabId) {
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'FILL_CODE',
|
||||
step: 7,
|
||||
source: 'background',
|
||||
payload: { code: result.code },
|
||||
});
|
||||
} else {
|
||||
throw new Error('ChatGPT tab was closed. Cannot fill verification code.');
|
||||
throw new Error('Auth page tab was closed. Cannot fill verification code.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,7 +688,7 @@ async function executeStep8(state) {
|
||||
setState({ localhostUrl: details.url }).then(() => {
|
||||
addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok');
|
||||
setStepStatus(8, 'completed');
|
||||
// Broadcast to side panel
|
||||
notifyStepComplete(8, { localhostUrl: details.url });
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'DATA_UPDATED',
|
||||
payload: { localhostUrl: details.url },
|
||||
@@ -553,20 +700,40 @@ async function executeStep8(state) {
|
||||
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
|
||||
|
||||
// Tell chatgpt.js to navigate to OAuth URL
|
||||
sendToContentScript('chatgpt', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 8,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
}).catch(err => {
|
||||
clearTimeout(timeout);
|
||||
if (webNavListener) {
|
||||
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
|
||||
webNavListener = null;
|
||||
// After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
|
||||
// with a "继续" button. We need to click it, which triggers the localhost redirect.
|
||||
(async () => {
|
||||
try {
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
if (signupTabId) {
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await addLog('Step 8: Switching to auth page, clicking "继续" to complete OAuth...');
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 8,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
} else {
|
||||
// Auth tab was closed, reopen OAuth URL
|
||||
await chrome.tabs.create({ url: state.oauthUrl, active: true });
|
||||
await addLog('Step 8: Auth tab closed, reopening OAuth URL...');
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 8,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
clearTimeout(timeout);
|
||||
if (webNavListener) {
|
||||
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
|
||||
webNavListener = null;
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+100
-186
@@ -1,64 +1,27 @@
|
||||
// content/qq-mail.js — Content script for QQ Mail (steps 4, 7)
|
||||
// Injected on: mail.qq.com, wx.mail.qq.com
|
||||
// NOTE: all_frames: true — this script runs in every frame on QQ Mail
|
||||
// NOTE: all_frames: true
|
||||
//
|
||||
// Strategy for avoiding stale codes:
|
||||
// 1. On poll start, snapshot all existing mail IDs as "old"
|
||||
// 2. On each poll cycle, refresh inbox and look for NEW items (not in snapshot)
|
||||
// 3. Only extract codes from NEW items that match sender/subject filters
|
||||
|
||||
const QQ_MAIL_PREFIX = '[MultiPage:qq-mail]';
|
||||
const isNewVersion = location.hostname === 'wx.mail.qq.com';
|
||||
const isTopFrame = window === window.top;
|
||||
|
||||
console.log(QQ_MAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
|
||||
|
||||
// For old QQ Mail with iframes, only report ready from the top frame
|
||||
// to avoid duplicate registrations. The inbox frame will handle email ops.
|
||||
if (!isTopFrame && isNewVersion) {
|
||||
console.log(QQ_MAIL_PREFIX, 'Skipping non-top frame on new QQ Mail');
|
||||
// Don't do anything in child frames of new version
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Frame detection
|
||||
// ============================================================
|
||||
|
||||
function isInboxFrame() {
|
||||
if (isNewVersion) return isTopFrame;
|
||||
// Old version: check if this frame has email list elements
|
||||
return !!document.querySelector(
|
||||
'#mailList, .mail-list, [id*="mailList"], #frm_main, .toarea, .mailList'
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Login State Check
|
||||
// ============================================================
|
||||
|
||||
function checkLoginState() {
|
||||
if (isNewVersion) {
|
||||
// wx.mail.qq.com: look for compose button or folder list
|
||||
return !!(
|
||||
document.querySelector('[class*="folder"], [class*="compose"], [class*="sidebar"]') ||
|
||||
document.querySelector('nav, [role="navigation"]') ||
|
||||
document.querySelector('[class*="mail-list"], [class*="mailList"]')
|
||||
);
|
||||
} else {
|
||||
// mail.qq.com: look for known logged-in elements
|
||||
return !!(
|
||||
document.querySelector('#folder_1, .folder_inbox, #composebtn, #mainFrameContainer')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Message Handler
|
||||
// ============================================================
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'POLL_EMAIL') {
|
||||
// For old QQ Mail, only handle in the inbox frame
|
||||
if (!isNewVersion && !isInboxFrame()) {
|
||||
if (!isTopFrame) {
|
||||
sendResponse({ ok: false, reason: 'wrong-frame' });
|
||||
return;
|
||||
}
|
||||
|
||||
handlePollEmail(message.step, message.payload).then(result => {
|
||||
sendResponse(result);
|
||||
}).catch(err => {
|
||||
@@ -67,60 +30,93 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
});
|
||||
return true; // async response
|
||||
}
|
||||
|
||||
if (message.type === 'CHECK_LOGIN') {
|
||||
if (!isTopFrame) {
|
||||
sendResponse({ loggedIn: false });
|
||||
return;
|
||||
}
|
||||
sendResponse({ loggedIn: checkLoginState() });
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Get all current mail IDs from the list
|
||||
// ============================================================
|
||||
|
||||
function getCurrentMailIds() {
|
||||
const ids = new Set();
|
||||
document.querySelectorAll('.mail-list-page-item[data-mailid]').forEach(item => {
|
||||
ids.add(item.getAttribute('data-mailid'));
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Email Polling
|
||||
// ============================================================
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
const { filterAfterTimestamp, senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
|
||||
|
||||
// Check login state first
|
||||
if (isTopFrame && !checkLoginState()) {
|
||||
throw new Error('QQ Mail not logged in. Please log in to QQ Mail and retry.');
|
||||
}
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
|
||||
|
||||
log(`Step ${step}: Starting email poll (max ${maxAttempts} attempts, every ${intervalMs / 1000}s)`);
|
||||
|
||||
// Wait for mail list to load
|
||||
try {
|
||||
await waitForElement('.mail-list-page-item', 10000);
|
||||
log(`Step ${step}: Mail list loaded`);
|
||||
} catch {
|
||||
throw new Error('Mail list did not load. Make sure QQ Mail inbox is open.');
|
||||
}
|
||||
|
||||
// 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"`);
|
||||
|
||||
// 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}`);
|
||||
|
||||
// Try to refresh inbox
|
||||
await refreshInbox();
|
||||
await sleep(800); // Wait for refresh to take effect
|
||||
// Refresh inbox (skip on first attempt, list is fresh)
|
||||
if (attempt > 1) {
|
||||
await refreshInbox();
|
||||
await sleep(800);
|
||||
}
|
||||
|
||||
// Search for matching email
|
||||
const result = await findMatchingEmail(senderFilters, subjectFilters);
|
||||
const allItems = document.querySelectorAll('.mail-list-page-item[data-mailid]');
|
||||
const useFallback = attempt > FALLBACK_AFTER;
|
||||
|
||||
if (result) {
|
||||
log(`Step ${step}: Found matching email! Extracting code...`);
|
||||
const code = extractVerificationCode(result.content);
|
||||
if (code) {
|
||||
log(`Step ${step}: Verification code found: ${code}`, 'ok');
|
||||
return { ok: true, code, emailTimestamp: Date.now() };
|
||||
} else {
|
||||
log(`Step ${step}: Email found but no 6-digit code in content. Preview: ${result.content.slice(0, 100)}`, 'warn');
|
||||
// Phase 1 (attempt 1~3): only look at NEW emails (not in snapshot)
|
||||
// Phase 2 (attempt 4+): fallback to first matching email in list
|
||||
for (const item of allItems) {
|
||||
const mailId = item.getAttribute('data-mailid');
|
||||
|
||||
if (!useFallback && existingMailIds.has(mailId)) continue;
|
||||
|
||||
const sender = (item.querySelector('.cmp-account-nick')?.textContent || '').toLowerCase();
|
||||
const subject = (item.querySelector('.mail-subject')?.textContent || '').toLowerCase();
|
||||
const digest = item.querySelector('.mail-digest')?.textContent || '';
|
||||
|
||||
const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()));
|
||||
const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()));
|
||||
|
||||
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');
|
||||
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');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No matching email found after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
|
||||
'Check QQ Mail manually. Email may be in spam folder.'
|
||||
`No new matching email found after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
|
||||
'Check QQ Mail manually. Email may be delayed or in spam folder.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,114 +125,33 @@ async function handlePollEmail(step, payload) {
|
||||
// ============================================================
|
||||
|
||||
async function refreshInbox() {
|
||||
if (isNewVersion) {
|
||||
// wx.mail.qq.com: try multiple refresh selectors
|
||||
const refreshBtn = document.querySelector(
|
||||
'[class*="refresh"], [title*="刷新"], button[aria-label*="refresh"], ' +
|
||||
'[data-action="refresh"], .toolbar-refresh'
|
||||
);
|
||||
if (refreshBtn) {
|
||||
simulateClick(refreshBtn);
|
||||
console.log(QQ_MAIL_PREFIX, 'Clicked refresh button (new version)');
|
||||
} else {
|
||||
// Fallback: click on "Inbox" folder to force reload
|
||||
const inboxLink = document.querySelector(
|
||||
'[class*="inbox"], [title*="收件箱"], a[href*="inbox"]'
|
||||
);
|
||||
if (inboxLink) {
|
||||
simulateClick(inboxLink);
|
||||
console.log(QQ_MAIL_PREFIX, 'Clicked inbox link to refresh');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// mail.qq.com: old version
|
||||
const refreshBtn = document.querySelector(
|
||||
'#refresh, .refresh_btn, [id*="refresh"], a[title*="刷新"]'
|
||||
);
|
||||
if (refreshBtn) {
|
||||
simulateClick(refreshBtn);
|
||||
console.log(QQ_MAIL_PREFIX, 'Clicked refresh button (old version)');
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try multiple strategies to refresh the mail list
|
||||
|
||||
// ============================================================
|
||||
// Find Matching Email
|
||||
// ============================================================
|
||||
|
||||
async function findMatchingEmail(senderFilters, subjectFilters) {
|
||||
let emailItems;
|
||||
|
||||
if (isNewVersion) {
|
||||
// wx.mail.qq.com: email list items
|
||||
emailItems = document.querySelectorAll(
|
||||
'[class*="mail-item"], [class*="list-item"], [class*="mail_item"], ' +
|
||||
'tr[class*="mail"], div[class*="letter"], [class*="thread"]'
|
||||
);
|
||||
} else {
|
||||
// mail.qq.com: email list inside table
|
||||
emailItems = document.querySelectorAll(
|
||||
'.toarea tr, #mailList tr, .mail_list tr, [id*="mail_"] tr'
|
||||
);
|
||||
// Strategy 1: Click any visible refresh button
|
||||
const refreshBtn = document.querySelector('[class*="refresh"], [title*="刷新"]');
|
||||
if (refreshBtn) {
|
||||
simulateClick(refreshBtn);
|
||||
console.log(QQ_MAIL_PREFIX, 'Clicked refresh button');
|
||||
await sleep(500);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(QQ_MAIL_PREFIX, `Found ${emailItems.length} email items to scan`);
|
||||
|
||||
for (const item of emailItems) {
|
||||
const text = (item.textContent || '').toLowerCase();
|
||||
|
||||
const senderMatch = senderFilters.some(f => text.includes(f.toLowerCase()));
|
||||
const subjectMatch = subjectFilters.some(f => text.includes(f.toLowerCase()));
|
||||
|
||||
if (senderMatch || subjectMatch) {
|
||||
console.log(QQ_MAIL_PREFIX, 'Found matching email item:', text.slice(0, 100));
|
||||
|
||||
// Try to get content from the visible text first
|
||||
let content = item.textContent || '';
|
||||
|
||||
// Check if we can already extract a code from the preview
|
||||
if (extractVerificationCode(content)) {
|
||||
return { content };
|
||||
}
|
||||
|
||||
// Need to click into the email for full body
|
||||
log('Clicking email to read full content...');
|
||||
simulateClick(item);
|
||||
await sleep(1500);
|
||||
|
||||
// Read email body
|
||||
const bodyEl = document.querySelector(
|
||||
'[class*="mail-body"], [class*="mail_body"], [class*="letter-body"], ' +
|
||||
'.body_content, #contentDiv, [class*="read-content"], [class*="mail-detail"]'
|
||||
);
|
||||
|
||||
if (bodyEl) {
|
||||
content = bodyEl.textContent || bodyEl.innerText || '';
|
||||
console.log(QQ_MAIL_PREFIX, 'Email body content:', content.slice(0, 200));
|
||||
}
|
||||
|
||||
// Go back to inbox after reading
|
||||
await goBackToInbox();
|
||||
|
||||
return { content };
|
||||
}
|
||||
// Strategy 2: Click inbox in sidebar to reload list
|
||||
const sidebarInbox = document.querySelector('a[href*="inbox"], [class*="folder-item"][class*="inbox"], [title="收件箱"]');
|
||||
if (sidebarInbox) {
|
||||
simulateClick(sidebarInbox);
|
||||
console.log(QQ_MAIL_PREFIX, 'Clicked sidebar inbox');
|
||||
await sleep(500);
|
||||
return;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function goBackToInbox() {
|
||||
if (isNewVersion) {
|
||||
// Click back or inbox link
|
||||
const backBtn = document.querySelector(
|
||||
'[class*="back"], [aria-label*="back"], [title*="返回"], [class*="return"]'
|
||||
);
|
||||
if (backBtn) {
|
||||
simulateClick(backBtn);
|
||||
await sleep(500);
|
||||
}
|
||||
// Strategy 3: Click the folder name in toolbar
|
||||
const folderName = document.querySelector('.toolbar-folder-name');
|
||||
if (folderName) {
|
||||
simulateClick(folderName);
|
||||
console.log(QQ_MAIL_PREFIX, 'Clicked toolbar folder name');
|
||||
await sleep(500);
|
||||
}
|
||||
// Old version typically uses frames, no need to go back
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -244,18 +159,17 @@ async function goBackToInbox() {
|
||||
// ============================================================
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
// Try various patterns for 6-digit verification codes
|
||||
// Pattern 1: standalone 6 digits
|
||||
// Pattern 1: Chinese format "代码为 370794" or "验证码...370794"
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[1];
|
||||
|
||||
// Pattern 2: English format "code is 370794" or "code: 370794"
|
||||
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
||||
if (matchEn) return matchEn[1] || matchEn[2];
|
||||
|
||||
// Pattern 3: standalone 6-digit number (first occurrence)
|
||||
const match6 = text.match(/\b(\d{6})\b/);
|
||||
if (match6) return match6[1];
|
||||
|
||||
// Pattern 2: code/verification followed by digits
|
||||
const matchLabeled = text.match(/(?:code|验证码|verification|verify)[:\s]*(\d{4,8})/i);
|
||||
if (matchLabeled) return matchLabeled[1];
|
||||
|
||||
// Pattern 3: digits followed by "code" label (Chinese: 是您的验证码)
|
||||
const matchReverse = text.match(/(\d{4,8})\s*(?:是|为|is)?\s*(?:您的)?(?:验证码|code)/i);
|
||||
if (matchReverse) return matchReverse[1];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
+190
-74
@@ -23,10 +23,13 @@ async function handleCommand(message) {
|
||||
case 2: return await step2_clickRegister();
|
||||
case 3: return await step3_fillEmailPassword(message.payload);
|
||||
case 5: return await step5_fillNameBirthday(message.payload);
|
||||
case 6: return await step6_login(message.payload);
|
||||
case 8: return await step8_clickContinue();
|
||||
default: throw new Error(`signup-page.js does not handle step ${message.step}`);
|
||||
}
|
||||
case 'FILL_CODE':
|
||||
return await step4_fillVerificationCode(message.payload);
|
||||
// Step 4 = signup code, Step 7 = login code (same handler)
|
||||
return await fillVerificationCode(message.step, message.payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +60,9 @@ async function step2_clickRegister() {
|
||||
}
|
||||
}
|
||||
|
||||
reportComplete(2);
|
||||
simulateClick(registerBtn);
|
||||
log('Step 2: Clicked Register button');
|
||||
|
||||
// Wait for page transition
|
||||
await sleep(2000);
|
||||
reportComplete(2);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -114,7 +114,11 @@ async function step3_fillEmailPassword(payload) {
|
||||
fillInput(passwordInput, 'mimashisha0.0');
|
||||
log('Step 3: Password filled');
|
||||
|
||||
// Submit the form
|
||||
// Report complete BEFORE submit, because submit causes page navigation
|
||||
// which kills the content script connection
|
||||
reportComplete(3, { email });
|
||||
|
||||
// Submit the form (page will navigate away after this)
|
||||
await sleep(500);
|
||||
const submitBtn = document.querySelector('button[type="submit"]')
|
||||
|| await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
|
||||
@@ -123,20 +127,17 @@ async function step3_fillEmailPassword(payload) {
|
||||
simulateClick(submitBtn);
|
||||
log('Step 3: Form submitted');
|
||||
}
|
||||
|
||||
await sleep(2000);
|
||||
reportComplete(3, { email });
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 4 (receiving end): Fill Verification Code
|
||||
// Fill Verification Code (used by step 4 and step 7)
|
||||
// ============================================================
|
||||
|
||||
async function step4_fillVerificationCode(payload) {
|
||||
async function fillVerificationCode(step, payload) {
|
||||
const { code } = payload;
|
||||
if (!code) throw new Error('No verification code provided.');
|
||||
|
||||
log(`Step 4: Filling verification code: ${code}`);
|
||||
log(`Step ${step}: Filling verification code: ${code}`);
|
||||
|
||||
// Find code input — could be a single input or multiple separate inputs
|
||||
let codeInput = null;
|
||||
@@ -149,20 +150,23 @@ async function step4_fillVerificationCode(payload) {
|
||||
// Check for multiple single-digit inputs (common pattern)
|
||||
const singleInputs = document.querySelectorAll('input[maxlength="1"]');
|
||||
if (singleInputs.length >= 6) {
|
||||
log('Step 4: Found single-digit code inputs, filling individually...');
|
||||
log(`Step ${step}: Found single-digit code inputs, filling individually...`);
|
||||
for (let i = 0; i < 6 && i < singleInputs.length; i++) {
|
||||
fillInput(singleInputs[i], code[i]);
|
||||
await sleep(100);
|
||||
}
|
||||
await sleep(1000);
|
||||
reportComplete(4);
|
||||
reportComplete(step);
|
||||
return;
|
||||
}
|
||||
throw new Error('Could not find verification code input. URL: ' + location.href);
|
||||
}
|
||||
|
||||
fillInput(codeInput, code);
|
||||
log('Step 4: Code filled');
|
||||
log(`Step ${step}: Code filled`);
|
||||
|
||||
// Report complete BEFORE submit (page may navigate away)
|
||||
reportComplete(step);
|
||||
|
||||
// Submit
|
||||
await sleep(500);
|
||||
@@ -171,12 +175,100 @@ async function step4_fillVerificationCode(payload) {
|
||||
|
||||
if (submitBtn) {
|
||||
simulateClick(submitBtn);
|
||||
log('Step 4: Verification submitted');
|
||||
log(`Step ${step}: Verification submitted`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 6: Login with registered account (on OAuth auth page)
|
||||
// ============================================================
|
||||
|
||||
async function step6_login(payload) {
|
||||
const { email, password } = payload;
|
||||
if (!email) throw new Error('No email provided for login.');
|
||||
|
||||
log(`Step 6: Logging in with ${email}...`);
|
||||
|
||||
// Wait for email input on the auth page
|
||||
let emailInput = null;
|
||||
try {
|
||||
emailInput = await waitForElement(
|
||||
'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]',
|
||||
15000
|
||||
);
|
||||
} catch {
|
||||
throw new Error('Could not find email input on login page. URL: ' + location.href);
|
||||
}
|
||||
|
||||
fillInput(emailInput, email);
|
||||
log('Step 6: Email filled');
|
||||
|
||||
// Submit email
|
||||
await sleep(500);
|
||||
const submitBtn1 = document.querySelector('button[type="submit"]')
|
||||
|| await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
|
||||
if (submitBtn1) {
|
||||
simulateClick(submitBtn1);
|
||||
log('Step 6: Submitted email');
|
||||
}
|
||||
|
||||
// Wait for page transition
|
||||
await sleep(2000);
|
||||
reportComplete(4);
|
||||
|
||||
// Check for password field
|
||||
const passwordInput = document.querySelector('input[type="password"]');
|
||||
if (passwordInput) {
|
||||
log('Step 6: Password field found, filling password...');
|
||||
fillInput(passwordInput, password);
|
||||
|
||||
await sleep(500);
|
||||
const submitBtn2 = document.querySelector('button[type="submit"]')
|
||||
|| await waitForElementByText('button', /continue|log\s*in|submit|sign\s*in|登录|继续/i, 5000).catch(() => null);
|
||||
// Report complete BEFORE submit in case page navigates
|
||||
reportComplete(6, { needsOTP: true });
|
||||
|
||||
if (submitBtn2) {
|
||||
simulateClick(submitBtn2);
|
||||
log('Step 6: Submitted password, may need verification code (step 7)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No password field — OTP flow
|
||||
log('Step 6: No password field. OTP flow or auto-redirect.');
|
||||
reportComplete(6, { needsOTP: true });
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 8: Click "继续" on OAuth consent page
|
||||
// ============================================================
|
||||
// After login + verification, page shows:
|
||||
// "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
|
||||
// Clicking it triggers redirect to localhost URL.
|
||||
|
||||
async function step8_clickContinue() {
|
||||
log('Step 8: Looking for OAuth consent "继续" button...');
|
||||
|
||||
// Wait for the consent page to be ready
|
||||
// Look for the submit button with text "继续" or data-dd-action-name="Continue"
|
||||
let continueBtn = null;
|
||||
try {
|
||||
continueBtn = await waitForElement(
|
||||
'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
|
||||
10000
|
||||
);
|
||||
} catch {
|
||||
try {
|
||||
continueBtn = await waitForElementByText('button', /继续|Continue/, 5000);
|
||||
} catch {
|
||||
throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href);
|
||||
}
|
||||
}
|
||||
|
||||
log('Step 8: Found "继续" button, clicking...');
|
||||
simulateClick(continueBtn);
|
||||
log('Step 8: Clicked "继续", redirecting to localhost... (background will capture URL)');
|
||||
|
||||
// Don't reportComplete — background handles it via webNavigation listener
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -187,80 +279,104 @@ async function step5_fillNameBirthday(payload) {
|
||||
const { firstName, lastName, year, month, day } = payload;
|
||||
if (!firstName || !lastName) throw new Error('No name data provided.');
|
||||
|
||||
log(`Step 5: Filling name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
|
||||
const fullName = `${firstName} ${lastName}`;
|
||||
log(`Step 5: Filling name: ${fullName}, Birthday: ${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}`);
|
||||
|
||||
// --- First name ---
|
||||
let firstNameInput = null;
|
||||
// Actual DOM structure:
|
||||
// - Full name: <input name="name" placeholder="全名" type="text">
|
||||
// - Birthday: React Aria DateField with 3 spinbutton divs (year/month/day)
|
||||
// + <input type="hidden" name="birthday" value="2026-04-05">
|
||||
|
||||
// --- Full Name (single field, not first+last) ---
|
||||
let nameInput = null;
|
||||
try {
|
||||
firstNameInput = await waitForElement(
|
||||
'input[name="firstName"], input[name="first_name"], input[name="fname"], input[placeholder*="first" i], input[placeholder*="First"], input[id*="first" i]',
|
||||
nameInput = await waitForElement(
|
||||
'input[name="name"], input[placeholder*="全名"], input[autocomplete="name"]',
|
||||
10000
|
||||
);
|
||||
} catch {
|
||||
throw new Error('Could not find first name input. URL: ' + location.href);
|
||||
throw new Error('Could not find name input. URL: ' + location.href);
|
||||
}
|
||||
fillInput(firstNameInput, firstName);
|
||||
log(`Step 5: First name filled: ${firstName}`);
|
||||
fillInput(nameInput, fullName);
|
||||
log(`Step 5: Name filled: ${fullName}`);
|
||||
|
||||
// --- Last name ---
|
||||
let lastNameInput = null;
|
||||
try {
|
||||
lastNameInput = await waitForElement(
|
||||
'input[name="lastName"], input[name="last_name"], input[name="lname"], input[placeholder*="last" i], input[placeholder*="Last"], input[id*="last" i]',
|
||||
5000
|
||||
);
|
||||
} catch {
|
||||
throw new Error('Could not find last name input. URL: ' + location.href);
|
||||
}
|
||||
fillInput(lastNameInput, lastName);
|
||||
log(`Step 5: Last name filled: ${lastName}`);
|
||||
// --- Birthday (React Aria DateField with spinbutton segments) ---
|
||||
// The date field has three contenteditable divs with role="spinbutton"
|
||||
// and data-type="year", data-type="month", data-type="day"
|
||||
// There's also a hidden input[name="birthday"] that stores the actual value
|
||||
|
||||
// --- Birthday ---
|
||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
|
||||
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
|
||||
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
|
||||
|
||||
// Try single date input first
|
||||
const dateInput = document.querySelector('input[type="date"], input[name="birthday"], input[name="dob"], input[name="birthdate"]');
|
||||
if (dateInput) {
|
||||
fillInput(dateInput, dateStr);
|
||||
log(`Step 5: Birthday filled (single input): ${dateStr}`);
|
||||
if (yearSpinner && monthSpinner && daySpinner) {
|
||||
log('Step 5: Found React Aria DateField spinbuttons');
|
||||
|
||||
// Helper to set a spinbutton value via focus + keyboard input
|
||||
async function setSpinButton(el, value) {
|
||||
el.focus();
|
||||
await sleep(100);
|
||||
|
||||
// Select all existing text
|
||||
document.execCommand('selectAll', false, null);
|
||||
await sleep(50);
|
||||
|
||||
// Type the new value digit by digit
|
||||
const valueStr = String(value);
|
||||
for (const char of valueStr) {
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true }));
|
||||
el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true }));
|
||||
// Also use InputEvent for React Aria
|
||||
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true }));
|
||||
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true }));
|
||||
await sleep(50);
|
||||
}
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true }));
|
||||
el.blur();
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
await setSpinButton(yearSpinner, year);
|
||||
log(`Step 5: Year set: ${year}`);
|
||||
|
||||
await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
|
||||
log(`Step 5: Month set: ${month}`);
|
||||
|
||||
await setSpinButton(daySpinner, String(day).padStart(2, '0'));
|
||||
log(`Step 5: Day set: ${day}`);
|
||||
|
||||
// Also update the hidden input directly as a safety measure
|
||||
const hiddenBirthday = document.querySelector('input[type="hidden"][name="birthday"]');
|
||||
if (hiddenBirthday) {
|
||||
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}`);
|
||||
}
|
||||
} else {
|
||||
// Try separate fields (month/day/year selects or inputs)
|
||||
log('Step 5: Looking for separate birthday fields...');
|
||||
|
||||
const monthEl = document.querySelector('select[name*="month" i], input[name*="month" i], input[placeholder*="month" i], select[id*="month" i]');
|
||||
const dayEl = document.querySelector('select[name*="day" i], input[name*="day" i], input[placeholder*="day" i], select[id*="day" i]');
|
||||
const yearEl = document.querySelector('select[name*="year" i], input[name*="year" i], input[placeholder*="year" i], select[id*="year" i]');
|
||||
|
||||
if (monthEl) {
|
||||
if (monthEl.tagName === 'SELECT') fillSelect(monthEl, String(month));
|
||||
else fillInput(monthEl, String(month).padStart(2, '0'));
|
||||
}
|
||||
if (dayEl) {
|
||||
if (dayEl.tagName === 'SELECT') fillSelect(dayEl, String(day));
|
||||
else fillInput(dayEl, String(day).padStart(2, '0'));
|
||||
}
|
||||
if (yearEl) {
|
||||
if (yearEl.tagName === 'SELECT') fillSelect(yearEl, String(year));
|
||||
else fillInput(yearEl, String(year));
|
||||
}
|
||||
|
||||
if (!monthEl && !dayEl && !yearEl) {
|
||||
log('Step 5: WARNING - Could not find any birthday fields. May need to adjust selectors.', 'warn');
|
||||
// Fallback: try setting hidden input directly
|
||||
const hiddenBirthday = document.querySelector('input[name="birthday"]');
|
||||
if (hiddenBirthday) {
|
||||
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: Birthday set via hidden input: ${dateStr}`);
|
||||
} else {
|
||||
log(`Step 5: Birthday filled (separate fields): ${year}-${month}-${day}`);
|
||||
log('Step 5: WARNING - Could not find birthday fields. May need to adjust selectors.', 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
// Submit / Complete
|
||||
// Click "完成帐户创建" button
|
||||
await sleep(500);
|
||||
const completeBtn = document.querySelector('button[type="submit"]')
|
||||
|| await waitForElementByText('button', /complete|continue|finish|done|create|agree|完成|创建|同意/i, 5000).catch(() => null);
|
||||
|| await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
|
||||
|
||||
// Report complete BEFORE submit (page navigates to add-phone after this)
|
||||
reportComplete(5);
|
||||
|
||||
if (completeBtn) {
|
||||
simulateClick(completeBtn);
|
||||
log('Step 5: Profile form submitted');
|
||||
log('Step 5: Clicked "完成帐户创建"');
|
||||
}
|
||||
|
||||
await sleep(2000);
|
||||
reportComplete(5);
|
||||
}
|
||||
|
||||
+100
-137
@@ -1,5 +1,27 @@
|
||||
// content/vps-panel.js — Content script for VPS panel (steps 1, 9)
|
||||
// Injected on: http://154.26.182.181:8317/*
|
||||
//
|
||||
// Actual DOM structure (after login click):
|
||||
// <div class="card">
|
||||
// <div class="card-header">
|
||||
// <span class="OAuthPage-module__cardTitle___yFaP0">Codex OAuth</span>
|
||||
// <button class="btn btn-primary"><span>登录</span></button>
|
||||
// </div>
|
||||
// <div class="OAuthPage-module__cardContent___1sXLA">
|
||||
// <div class="OAuthPage-module__authUrlBox___Iu1d4">
|
||||
// <div class="OAuthPage-module__authUrlLabel___mYFJB">授权链接:</div>
|
||||
// <div class="OAuthPage-module__authUrlValue___axvUJ">https://auth.openai.com/...</div>
|
||||
// <div class="OAuthPage-module__authUrlActions___venPj">
|
||||
// <button class="btn btn-secondary btn-sm"><span>复制链接</span></button>
|
||||
// <button class="btn btn-secondary btn-sm"><span>打开链接</span></button>
|
||||
// </div>
|
||||
// </div>
|
||||
// <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>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
console.log('[MultiPage:vps-panel] Content script loaded on', location.href);
|
||||
|
||||
@@ -30,178 +52,119 @@ async function handleStep(step, payload) {
|
||||
// ============================================================
|
||||
|
||||
async function step1_getOAuthLink() {
|
||||
log('Step 1: Checking VPS panel state...');
|
||||
log('Step 1: Waiting for VPS panel to load (auto-login may take a moment)...');
|
||||
|
||||
// --- Selector strategy ---
|
||||
// The VPS panel is at management.html#/oauth
|
||||
// We need to: 1) find OAuth login section, 2) click Codex login, 3) read auth URL
|
||||
// TODO: These selectors MUST be adjusted after inspecting the actual VPS panel DOM
|
||||
// The selectors below are best-guess placeholders.
|
||||
|
||||
// Try to find OAuth login button by text content
|
||||
log('Step 1: Looking for OAuth login button...');
|
||||
let oauthBtn = null;
|
||||
// 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 {
|
||||
oauthBtn = await waitForElementByText(
|
||||
'button, a, [role="button"], .el-button, div[class*="btn"]',
|
||||
/oauth|OAuth/i,
|
||||
10000
|
||||
);
|
||||
// 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 {
|
||||
// Fallback: try common UI framework selectors
|
||||
try {
|
||||
oauthBtn = await waitForElement('.oauth-login-btn, [data-action="oauth-login"]', 5000);
|
||||
} catch {
|
||||
throw new Error(
|
||||
'Could not find OAuth login button. ' +
|
||||
'Please inspect the VPS panel page in DevTools and update the selector in vps-panel.js. ' +
|
||||
'URL: ' + location.href
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
simulateClick(oauthBtn);
|
||||
log('Step 1: Clicked OAuth login, waiting for Codex login option...');
|
||||
await sleep(1500);
|
||||
|
||||
// Wait for Codex login option to appear
|
||||
let codexBtn = null;
|
||||
try {
|
||||
codexBtn = await waitForElementByText(
|
||||
'button, a, [role="button"], .el-button, div[class*="btn"], span',
|
||||
/codex/i,
|
||||
10000
|
||||
);
|
||||
} catch {
|
||||
try {
|
||||
codexBtn = await waitForElement('[data-action="codex-login"], .codex-login-btn', 5000);
|
||||
} catch {
|
||||
throw new Error(
|
||||
'Could not find Codex login button after clicking OAuth. ' +
|
||||
'Check the VPS panel DOM in DevTools. URL: ' + location.href
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
simulateClick(codexBtn);
|
||||
log('Step 1: Clicked Codex login, waiting for auth URL...');
|
||||
await sleep(2000);
|
||||
|
||||
// Extract the auth URL — could be in various elements
|
||||
let oauthUrl = null;
|
||||
|
||||
// Strategy 1: Look for an input/textarea with a URL value
|
||||
const inputs = document.querySelectorAll('input[readonly], input[type="text"], textarea, code, pre');
|
||||
for (const el of inputs) {
|
||||
const val = (el.value || el.textContent || '').trim();
|
||||
if (val.startsWith('http') && val.length > 30) {
|
||||
oauthUrl = val;
|
||||
log(`Step 1: Found URL in <${el.tagName}>: ${val.slice(0, 80)}...`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Look for any element containing a long URL
|
||||
if (!oauthUrl) {
|
||||
const allElements = document.querySelectorAll('span, p, div, a, code, pre');
|
||||
for (const el of allElements) {
|
||||
const text = (el.textContent || '').trim();
|
||||
// Match a URL that looks like an OAuth authorization URL
|
||||
const urlMatch = text.match(/(https?:\/\/[^\s<>"']{30,})/);
|
||||
if (urlMatch) {
|
||||
oauthUrl = urlMatch[1];
|
||||
log(`Step 1: Found URL in text: ${oauthUrl.slice(0, 80)}...`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Check clipboard (if the page auto-copies)
|
||||
if (!oauthUrl) {
|
||||
try {
|
||||
oauthUrl = await navigator.clipboard.readText();
|
||||
if (oauthUrl && oauthUrl.startsWith('http') && oauthUrl.length > 30) {
|
||||
log(`Step 1: Found URL in clipboard: ${oauthUrl.slice(0, 80)}...`);
|
||||
} else {
|
||||
oauthUrl = null;
|
||||
}
|
||||
} catch {
|
||||
// Clipboard access may be denied
|
||||
}
|
||||
}
|
||||
|
||||
if (!oauthUrl) {
|
||||
throw new Error(
|
||||
'Could not find auth URL. The URL may be displayed in a format we cannot detect. ' +
|
||||
'Please check the VPS panel page and copy the URL manually, or update the extraction logic in vps-panel.js. ' +
|
||||
'URL: ' + location.href
|
||||
'Codex OAuth card did not appear after 30s. Page may still be loading or not logged in. ' +
|
||||
'Current URL: ' + location.href
|
||||
);
|
||||
}
|
||||
|
||||
if (!loginBtn) {
|
||||
throw new Error('Found Codex OAuth card but no login button inside it. URL: ' + location.href);
|
||||
}
|
||||
|
||||
// Check if button is disabled (already clicked / loading)
|
||||
if (loginBtn.disabled) {
|
||||
log('Step 1: Login button is disabled (already loading), waiting for auth URL...');
|
||||
} else {
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
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.`);
|
||||
}
|
||||
|
||||
log(`Step 1: OAuth URL obtained: ${oauthUrl.slice(0, 80)}...`, 'ok');
|
||||
reportComplete(1, { oauthUrl: oauthUrl.trim() });
|
||||
reportComplete(1, { oauthUrl });
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 9: VPS Verify
|
||||
// Step 9: VPS Verify — paste localhost URL and submit
|
||||
// ============================================================
|
||||
|
||||
async function step9_vpsVerify() {
|
||||
log('Step 9: Getting localhost URL from storage...');
|
||||
|
||||
// Get localhostUrl from storage (via Background)
|
||||
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
|
||||
const localhostUrl = state.localhostUrl;
|
||||
if (!localhostUrl) {
|
||||
throw new Error('No localhost URL found. Complete step 8 first.');
|
||||
}
|
||||
|
||||
log(`Step 9: Looking for URL input field on VPS panel...`);
|
||||
log('Step 9: Looking for callback URL input...');
|
||||
|
||||
// Try to find URL input field
|
||||
// Find the callback URL input
|
||||
// Actual DOM: <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=...">
|
||||
let urlInput = null;
|
||||
try {
|
||||
urlInput = await waitForElement(
|
||||
'input[placeholder*="localhost"], input[placeholder*="callback"], input[placeholder*="URL"], input[placeholder*="url"], input[name*="callback"], input[name*="url"]',
|
||||
10000
|
||||
);
|
||||
urlInput = await waitForElement('[class*="callbackSection"] input.input', 10000);
|
||||
} catch {
|
||||
// Fallback: find any text input that's visible and empty
|
||||
const inputs = document.querySelectorAll('input[type="text"], input:not([type])');
|
||||
for (const input of inputs) {
|
||||
if (input.offsetParent !== null && !input.value) {
|
||||
urlInput = input;
|
||||
log('Step 9: Using fallback empty input field');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!urlInput) {
|
||||
throw new Error(
|
||||
'Could not find URL input field on VPS panel. ' +
|
||||
'Check DOM structure in DevTools. URL: ' + location.href
|
||||
);
|
||||
try {
|
||||
urlInput = await waitForElement('input[placeholder*="localhost"]', 5000);
|
||||
} catch {
|
||||
throw new Error('Could not find callback URL input on VPS panel. URL: ' + location.href);
|
||||
}
|
||||
}
|
||||
|
||||
fillInput(urlInput, localhostUrl);
|
||||
log(`Step 9: Filled URL input with: ${localhostUrl}`);
|
||||
log(`Step 9: Filled callback URL: ${localhostUrl.slice(0, 80)}...`);
|
||||
|
||||
// Find and click verify button
|
||||
let verifyBtn = null;
|
||||
// Find and click "提交回调 URL" button
|
||||
let submitBtn = null;
|
||||
try {
|
||||
verifyBtn = await waitForElementByText(
|
||||
'button, [role="button"], .el-button, a',
|
||||
/verif|确认|验证|submit|提交/i,
|
||||
10000
|
||||
submitBtn = await waitForElementByText(
|
||||
'[class*="callbackActions"] button, [class*="callbackSection"] button',
|
||||
/提交/,
|
||||
5000
|
||||
);
|
||||
} catch {
|
||||
throw new Error(
|
||||
'Could not find verify/submit button. ' +
|
||||
'Check VPS panel DOM in DevTools. URL: ' + location.href
|
||||
);
|
||||
try {
|
||||
submitBtn = await waitForElementByText('button.btn', /提交回调/, 5000);
|
||||
} catch {
|
||||
throw new Error('Could not find "提交回调 URL" button. URL: ' + location.href);
|
||||
}
|
||||
}
|
||||
|
||||
simulateClick(submitBtn);
|
||||
log('Step 9: Clicked "提交回调 URL", waiting for authentication result...');
|
||||
|
||||
// Wait for "认证成功!" status badge to appear
|
||||
try {
|
||||
await waitForElementByText('.status-badge, [class*="status"]', /认证成功/, 30000);
|
||||
log('Step 9: Authentication successful!', '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');
|
||||
} else {
|
||||
log(`Step 9: Status after submit: "${statusText}". May still be processing.`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
simulateClick(verifyBtn);
|
||||
log('Step 9: Clicked verify button', 'ok');
|
||||
reportComplete(9);
|
||||
}
|
||||
|
||||
+78
-2
@@ -23,6 +23,11 @@ header h1 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-btns {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
#btn-reset {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
@@ -34,6 +39,49 @@ header h1 {
|
||||
}
|
||||
#btn-reset:hover { background: #c82333; }
|
||||
|
||||
.btn-auto {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
background: #28a745;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-auto:hover { background: #218838; }
|
||||
.btn-auto:disabled { background: #6c757d; cursor: not-allowed; }
|
||||
|
||||
.auto-continue-bar {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.auto-hint {
|
||||
font-size: 11px;
|
||||
color: #856404;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-continue {
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-continue:hover { background: #0069d9; }
|
||||
|
||||
/* Data Section */
|
||||
#data-section {
|
||||
background: #fff;
|
||||
@@ -141,6 +189,34 @@ header h1 {
|
||||
}
|
||||
|
||||
.log-info { color: #d4d4d4; }
|
||||
.log-ok { color: #4ec9b0; }
|
||||
.log-ok { color: #4ec9b0; font-weight: bold; }
|
||||
.log-warn { color: #dcdcaa; }
|
||||
.log-error { color: #f44747; }
|
||||
.log-error { color: #f44747; font-weight: bold; }
|
||||
|
||||
.log-step-tag {
|
||||
display: inline-block;
|
||||
background: #3a3d41;
|
||||
color: #569cd6;
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
margin-right: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.log-step-tag.step-1 { color: #4fc1ff; }
|
||||
.log-step-tag.step-2 { color: #c586c0; }
|
||||
.log-step-tag.step-3 { color: #dcdcaa; }
|
||||
.log-step-tag.step-4 { color: #ce9178; }
|
||||
.log-step-tag.step-5 { color: #b5cea8; }
|
||||
.log-step-tag.step-6 { color: #4fc1ff; }
|
||||
.log-step-tag.step-7 { color: #ce9178; }
|
||||
.log-step-tag.step-8 { color: #c586c0; }
|
||||
.log-step-tag.step-9 { color: #b5cea8; }
|
||||
|
||||
.log-time { color: #6a9955; }
|
||||
.log-level { font-weight: bold; }
|
||||
.log-level-info { color: #569cd6; }
|
||||
.log-level-ok { color: #4ec9b0; }
|
||||
.log-level-warn { color: #dcdcaa; }
|
||||
.log-level-error { color: #f44747; }
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
<body>
|
||||
<header>
|
||||
<h1>Multi-Page Automation</h1>
|
||||
<button id="btn-reset" title="Reset all steps">Reset</button>
|
||||
<div class="header-btns">
|
||||
<button id="btn-auto-run" class="btn-auto" title="Run all steps automatically">Auto Run</button>
|
||||
<button id="btn-reset" title="Reset all steps">Reset</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="data-section">
|
||||
@@ -29,6 +32,10 @@
|
||||
<label>Status:</label>
|
||||
<span id="display-status" class="data-value">Waiting</span>
|
||||
</div>
|
||||
<div id="auto-continue-bar" class="auto-continue-bar" style="display:none;">
|
||||
<span class="auto-hint">Paste DuckDuckGo email above, then:</span>
|
||||
<button id="btn-auto-continue" class="btn-continue">Continue Auto</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="steps-section">
|
||||
|
||||
+70
-1
@@ -128,11 +128,30 @@ function appendLog(entry) {
|
||||
const levelLabel = entry.level.toUpperCase().padEnd(5);
|
||||
const line = document.createElement('div');
|
||||
line.className = `log-${entry.level}`;
|
||||
line.textContent = `${time} [${levelLabel}] ${entry.message}`;
|
||||
|
||||
// Extract step number from message (e.g., "Step 4: ..." or "[vps-panel] Step 1: ...")
|
||||
const stepMatch = entry.message.match(/Step (\d)/);
|
||||
const stepNum = stepMatch ? stepMatch[1] : null;
|
||||
|
||||
// Build rich HTML
|
||||
let html = `<span class="log-time">${time}</span> `;
|
||||
html += `<span class="log-level log-level-${entry.level}">[${levelLabel}]</span> `;
|
||||
if (stepNum) {
|
||||
html += `<span class="log-step-tag step-${stepNum}">S${stepNum}</span>`;
|
||||
}
|
||||
html += `<span>${escapeHtml(entry.message)}</span>`;
|
||||
|
||||
line.innerHTML = html;
|
||||
logArea.appendChild(line);
|
||||
logArea.scrollTop = logArea.scrollHeight;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Button Handlers
|
||||
// ============================================================
|
||||
@@ -163,6 +182,32 @@ document.querySelectorAll('.step-btn').forEach(btn => {
|
||||
});
|
||||
});
|
||||
|
||||
// Auto Run button
|
||||
const btnAutoRun = document.getElementById('btn-auto-run');
|
||||
const btnAutoContinue = document.getElementById('btn-auto-continue');
|
||||
const autoContinueBar = document.getElementById('auto-continue-bar');
|
||||
|
||||
btnAutoRun.addEventListener('click', async () => {
|
||||
btnAutoRun.disabled = true;
|
||||
btnAutoRun.textContent = 'Running...';
|
||||
await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel' });
|
||||
});
|
||||
|
||||
btnAutoContinue.addEventListener('click', async () => {
|
||||
const email = inputEmail.value.trim();
|
||||
if (!email) {
|
||||
appendLog({ message: 'Please paste DuckDuckGo email first!', level: 'error', timestamp: Date.now() });
|
||||
return;
|
||||
}
|
||||
autoContinueBar.style.display = 'none';
|
||||
btnAutoRun.textContent = 'Running...';
|
||||
await chrome.runtime.sendMessage({
|
||||
type: 'RESUME_AUTO_RUN',
|
||||
source: 'sidepanel',
|
||||
payload: { email },
|
||||
});
|
||||
});
|
||||
|
||||
// Reset button
|
||||
btnReset.addEventListener('click', async () => {
|
||||
if (confirm('Reset all steps and data?')) {
|
||||
@@ -177,6 +222,9 @@ btnReset.addEventListener('click', async () => {
|
||||
displayStatus.classList.remove('has-value');
|
||||
logArea.innerHTML = '';
|
||||
document.querySelectorAll('.step-status').forEach(el => el.textContent = '\u2B1A');
|
||||
btnAutoRun.disabled = false;
|
||||
btnAutoRun.textContent = 'Auto Run';
|
||||
autoContinueBar.style.display = 'none';
|
||||
updateButtonStates();
|
||||
}
|
||||
});
|
||||
@@ -222,6 +270,27 @@ chrome.runtime.onMessage.addListener((message) => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'AUTO_RUN_STATUS': {
|
||||
const { phase } = message.payload;
|
||||
switch (phase) {
|
||||
case 'waiting_email':
|
||||
autoContinueBar.style.display = 'flex';
|
||||
btnAutoRun.textContent = 'Waiting...';
|
||||
break;
|
||||
case 'complete':
|
||||
btnAutoRun.disabled = false;
|
||||
btnAutoRun.textContent = 'Auto Run';
|
||||
autoContinueBar.style.display = 'none';
|
||||
break;
|
||||
case 'stopped':
|
||||
btnAutoRun.disabled = false;
|
||||
btnAutoRun.textContent = 'Auto Run';
|
||||
autoContinueBar.style.display = 'none';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user