feat: initial implementation of Multi-Page Automation Chrome extension

- manifest.json: MV3 config with sidePanel, tabs, webNavigation, storage, scripting permissions
- background.js: service worker with state management (chrome.storage.session), tab registry,
  message routing, command queue, and step orchestration for all 9 steps
- sidepanel/: persistent side panel UI with 9 step buttons, interlock logic, log area,
  state restore, and reset functionality
- content/utils.js: shared utilities (waitForElement, fillInput, simulateClick, log, report*)
- content/vps-panel.js: VPS panel automation (steps 1, 9)
- content/signup-page.js: OpenAI auth page automation (steps 2, 3, 4-fill, 5)
- content/qq-mail.js: QQ Mail email polling and code extraction (steps 4, 7)
- content/chatgpt.js: ChatGPT login automation (steps 6, 7-fill, 8)
- data/names.js: random English name and birthday generation
This commit is contained in:
unknown
2026-04-05 08:59:03 +08:00
commit 887709e824
14 changed files with 2354 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
// content/chatgpt.js — Content script for ChatGPT (steps 6, 7-receive, 8)
// Injected on: chatgpt.com
console.log('[MultiPage:chatgpt] 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') {
handleCommand(message).then(() => {
sendResponse({ ok: true });
}).catch(err => {
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
return true;
}
});
async function handleCommand(message) {
switch (message.type) {
case 'EXECUTE_STEP':
switch (message.step) {
case 6: return await step6_loginChatGPT();
case 8: return await step8_navigateOAuth();
default: throw new Error(`chatgpt.js does not handle step ${message.step}`);
}
case 'FILL_CODE':
return await step7_fillLoginCode(message.payload);
}
}
// ============================================================
// Step 6: Login ChatGPT
// ============================================================
async function step6_loginChatGPT() {
log('Step 6: Looking for login button on ChatGPT...');
// Get state for email and password
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
const email = state.email;
const password = state.password || 'mimashisha0.0';
if (!email) throw new Error('No email found in state. Complete earlier steps first.');
// Find login button on ChatGPT homepage
let loginBtn = null;
try {
loginBtn = await waitForElementByText(
'a, button, [role="button"], [role="link"]',
/log\s*in|sign\s*in|登录/i,
10000
);
} catch {
try {
loginBtn = await waitForElement(
'[data-testid="login-button"], a[href*="auth"], a[href*="login"]',
5000
);
} catch {
throw new Error('Could not find Login button on ChatGPT. URL: ' + location.href);
}
}
simulateClick(loginBtn);
log('Step 6: Clicked Login button, waiting for auth page...');
await sleep(3000);
// We may be redirected to an auth page (auth0.openai.com etc.)
// Or the login form may appear on the same page
// Try to find email input
let emailInput = null;
try {
emailInput = await waitForElement(
'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i]',
15000
);
} catch {
throw new Error('Could not find email input on login page. URL: ' + location.href);
}
fillInput(emailInput, email);
log(`Step 6: Filled email: ${email}`);
// 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');
}
await sleep(2000);
// Check: password field or OTP?
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);
if (submitBtn2) {
simulateClick(submitBtn2);
log('Step 6: Submitted password');
}
await sleep(3000);
// Check if we need OTP after password
const codeInput = document.querySelector(
'input[name="code"], input[maxlength="6"], input[inputmode="numeric"], input[type="text"][maxlength="6"]'
);
if (codeInput) {
log('Step 6: OTP code input found after password. Waiting for step 7...');
reportComplete(6, { needsOTP: true });
} else {
// Check if we're actually logged in (redirected to chatgpt.com main page)
log('Step 6: Login appears successful (no OTP needed)');
reportComplete(6, { needsOTP: false });
}
} else {
// No password field — check for OTP input or "check your email" message
const codeInput = document.querySelector(
'input[name="code"], input[maxlength="6"], input[inputmode="numeric"]'
);
if (codeInput) {
log('Step 6: OTP flow detected (no password). Waiting for step 7...');
reportComplete(6, { needsOTP: true });
} else {
// Maybe the page is still loading or transitioning
log('Step 6: No password or OTP field found. May need email verification. Waiting for step 7...');
reportComplete(6, { needsOTP: true });
}
}
}
// ============================================================
// Step 7 (receiving end): Fill Login Verification Code
// ============================================================
async function step7_fillLoginCode(payload) {
const { code } = payload;
if (!code) throw new Error('No verification code provided.');
log(`Step 7: Filling login verification code: ${code}`);
// Find code input — single input or multiple single-digit inputs
let codeInput = null;
try {
codeInput = await waitForElement(
'input[name="code"], input[name="otp"], input[maxlength="6"], input[type="text"][inputmode="numeric"], input[aria-label*="code" i], input[placeholder*="code" i]',
10000
);
} catch {
// Check for multiple single-digit inputs
const singleInputs = document.querySelectorAll('input[maxlength="1"]');
if (singleInputs.length >= 6) {
log('Step 7: 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);
// Auto-submit may happen; if not, try clicking submit
const submitBtn = document.querySelector('button[type="submit"]')
|| await waitForElementByText('button', /continue|verify|submit|confirm/i, 3000).catch(() => null);
if (submitBtn) simulateClick(submitBtn);
await sleep(3000);
reportComplete(7);
return;
}
throw new Error('Could not find verification code input on ChatGPT login. URL: ' + location.href);
}
fillInput(codeInput, code);
await sleep(500);
const submitBtn = document.querySelector('button[type="submit"]')
|| await waitForElementByText('button', /continue|verify|submit|confirm|确认/i, 5000).catch(() => null);
if (submitBtn) {
simulateClick(submitBtn);
log('Step 7: Login verification code submitted');
}
await sleep(3000);
reportComplete(7);
}
// ============================================================
// Step 8: Navigate to OAuth URL
// ============================================================
async function step8_navigateOAuth() {
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
const oauthUrl = state.oauthUrl;
if (!oauthUrl) throw new Error('No OAuth URL found. Complete step 1 first.');
log(`Step 8: Navigating to OAuth URL: ${oauthUrl.slice(0, 80)}...`);
log('Step 8: Waiting for localhost redirect (captured by background)...');
// Navigate — the webNavigation listener in background will capture localhost redirect
window.location.href = oauthUrl;
// Don't reportComplete here — background handles it via webNavigation
}
+261
View File
@@ -0,0 +1,261 @@
// 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
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()) {
sendResponse({ ok: false, reason: 'wrong-frame' });
return;
}
handlePollEmail(message.step, message.payload).then(result => {
sendResponse(result);
}).catch(err => {
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
return true; // async response
}
if (message.type === 'CHECK_LOGIN') {
if (!isTopFrame) {
sendResponse({ loggedIn: false });
return;
}
sendResponse({ loggedIn: checkLoginState() });
return;
}
});
// ============================================================
// 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.');
}
log(`Step ${step}: Starting email poll (max ${maxAttempts} attempts, every ${intervalMs / 1000}s)`);
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
// Search for matching email
const result = await findMatchingEmail(senderFilters, subjectFilters);
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');
}
}
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.'
);
}
// ============================================================
// Inbox Refresh
// ============================================================
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)');
}
}
}
// ============================================================
// 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'
);
}
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 };
}
}
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);
}
}
// Old version typically uses frames, no need to go back
}
// ============================================================
// Verification Code Extraction
// ============================================================
function extractVerificationCode(text) {
// Try various patterns for 6-digit verification codes
// Pattern 1: standalone 6 digits
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;
}
+266
View File
@@ -0,0 +1,266 @@
// content/signup-page.js — Content script for OpenAI auth pages (steps 2, 3, 4-receive, 5)
// Injected on: auth0.openai.com, auth.openai.com, accounts.openai.com
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') {
handleCommand(message).then(() => {
sendResponse({ ok: true });
}).catch(err => {
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
return true;
}
});
async function handleCommand(message) {
switch (message.type) {
case 'EXECUTE_STEP':
switch (message.step) {
case 2: return await step2_clickRegister();
case 3: return await step3_fillEmailPassword(message.payload);
case 5: return await step5_fillNameBirthday(message.payload);
default: throw new Error(`signup-page.js does not handle step ${message.step}`);
}
case 'FILL_CODE':
return await step4_fillVerificationCode(message.payload);
}
}
// ============================================================
// Step 2: Click Register
// ============================================================
async function step2_clickRegister() {
log('Step 2: Looking for Register/Sign up button...');
// TODO: Adjust selectors based on actual OpenAI auth page
let registerBtn = null;
try {
registerBtn = await waitForElementByText(
'a, button, [role="button"], [role="link"]',
/sign\s*up|register|create\s*account|注册/i,
10000
);
} catch {
// Some pages may have a direct link
try {
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
);
}
}
simulateClick(registerBtn);
log('Step 2: Clicked Register button');
// Wait for page transition
await sleep(2000);
reportComplete(2);
}
// ============================================================
// Step 3: Fill Email & Password
// ============================================================
async function step3_fillEmailPassword(payload) {
const { email } = payload;
if (!email) throw new Error('No email provided. Paste email in Side Panel first.');
log(`Step 3: Filling email: ${email}`);
// Find email input
let emailInput = null;
try {
emailInput = await waitForElement(
'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email"], input[placeholder*="Email"]',
10000
);
} catch {
throw new Error('Could not find email input field on signup page. URL: ' + location.href);
}
fillInput(emailInput, email);
log('Step 3: Email filled');
// 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...');
const submitBtn = document.querySelector('button[type="submit"]')
|| await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
if (submitBtn) {
simulateClick(submitBtn);
log('Step 3: Submitted email, waiting for password field...');
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);
}
}
fillInput(passwordInput, 'mimashisha0.0');
log('Step 3: Password filled');
// Submit the form
await sleep(500);
const submitBtn = document.querySelector('button[type="submit"]')
|| await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
if (submitBtn) {
simulateClick(submitBtn);
log('Step 3: Form submitted');
}
await sleep(2000);
reportComplete(3, { email });
}
// ============================================================
// Step 4 (receiving end): Fill Verification Code
// ============================================================
async function step4_fillVerificationCode(payload) {
const { code } = payload;
if (!code) throw new Error('No verification code provided.');
log(`Step 4: Filling verification code: ${code}`);
// 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
);
} catch {
// 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...');
for (let i = 0; i < 6 && i < singleInputs.length; i++) {
fillInput(singleInputs[i], code[i]);
await sleep(100);
}
await sleep(1000);
reportComplete(4);
return;
}
throw new Error('Could not find verification code input. URL: ' + location.href);
}
fillInput(codeInput, code);
log('Step 4: Code filled');
// Submit
await sleep(500);
const submitBtn = document.querySelector('button[type="submit"]')
|| await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证/i, 5000).catch(() => null);
if (submitBtn) {
simulateClick(submitBtn);
log('Step 4: Verification submitted');
}
// Wait for page transition
await sleep(2000);
reportComplete(4);
}
// ============================================================
// Step 5: Fill Name & Birthday
// ============================================================
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}`);
// --- First name ---
let firstNameInput = 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]',
10000
);
} catch {
throw new Error('Could not find first name input. URL: ' + location.href);
}
fillInput(firstNameInput, firstName);
log(`Step 5: First name filled: ${firstName}`);
// --- 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 ---
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
// 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}`);
} 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');
} else {
log(`Step 5: Birthday filled (separate fields): ${year}-${month}-${day}`);
}
}
// Submit / Complete
await sleep(500);
const completeBtn = document.querySelector('button[type="submit"]')
|| await waitForElementByText('button', /complete|continue|finish|done|create|agree|完成|创建|同意/i, 5000).catch(() => null);
if (completeBtn) {
simulateClick(completeBtn);
log('Step 5: Profile form submitted');
}
await sleep(2000);
reportComplete(5);
}
+226
View File
@@ -0,0 +1,226 @@
// content/utils.js — Shared utilities for all content scripts
const SCRIPT_SOURCE = (() => {
const url = location.href;
if (url.includes('154.26.182.181')) return 'vps-panel';
if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
if (url.includes('mail.qq.com')) return 'qq-mail';
if (url.includes('chatgpt.com')) return 'chatgpt';
return 'unknown';
})();
const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
/**
* Wait for a DOM element to appear.
* @param {string} selector - CSS selector
* @param {number} timeout - Max wait time in ms (default 10000)
* @returns {Promise<Element>}
*/
function waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
const existing = document.querySelector(selector);
if (existing) {
console.log(LOG_PREFIX, `Found immediately: ${selector}`);
log(`Found element: ${selector}`);
resolve(existing);
return;
}
console.log(LOG_PREFIX, `Waiting for: ${selector} (timeout: ${timeout}ms)`);
log(`Waiting for selector: ${selector}...`);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
clearTimeout(timer);
console.log(LOG_PREFIX, `Found after wait: ${selector}`);
log(`Found element: ${selector}`);
resolve(el);
}
});
observer.observe(document.body || document.documentElement, {
childList: true,
subtree: true,
});
const timer = setTimeout(() => {
observer.disconnect();
const msg = `Timeout waiting for ${selector} after ${timeout}ms on ${location.href}`;
console.error(LOG_PREFIX, msg);
reject(new Error(msg));
}, timeout);
});
}
/**
* Wait for an element matching a text pattern among multiple candidates.
* @param {string} containerSelector - Selector for candidate elements
* @param {RegExp} textPattern - Regex to match against textContent
* @param {number} timeout - Max wait time in ms
* @returns {Promise<Element>}
*/
function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
return new Promise((resolve, reject) => {
function search() {
const candidates = document.querySelectorAll(containerSelector);
for (const el of candidates) {
if (textPattern.test(el.textContent)) {
return el;
}
}
return null;
}
const existing = search();
if (existing) {
console.log(LOG_PREFIX, `Found by text immediately: ${containerSelector} matching ${textPattern}`);
log(`Found element by text: ${textPattern}`);
resolve(existing);
return;
}
console.log(LOG_PREFIX, `Waiting for text match: ${containerSelector} / ${textPattern}`);
log(`Waiting for element with text: ${textPattern}...`);
const observer = new MutationObserver(() => {
const el = search();
if (el) {
observer.disconnect();
clearTimeout(timer);
console.log(LOG_PREFIX, `Found by text after wait: ${textPattern}`);
log(`Found element by text: ${textPattern}`);
resolve(el);
}
});
observer.observe(document.body || document.documentElement, {
childList: true,
subtree: true,
});
const timer = setTimeout(() => {
observer.disconnect();
const msg = `Timeout waiting for text "${textPattern}" in "${containerSelector}" after ${timeout}ms on ${location.href}`;
console.error(LOG_PREFIX, msg);
reject(new Error(msg));
}, timeout);
});
}
/**
* React-compatible form filling.
* Sets value via native setter and dispatches input + change events.
* @param {HTMLInputElement} el
* @param {string} value
*/
function fillInput(el, value) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
).set;
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'}]`);
}
/**
* Fill a select element by setting its value and triggering change.
* @param {HTMLSelectElement} el
* @param {string} value
*/
function fillSelect(el, value) {
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}`);
}
/**
* Send a log message to Side Panel via Background.
* @param {string} message
* @param {string} level - 'info' | 'ok' | 'warn' | 'error'
*/
function log(message, level = 'info') {
chrome.runtime.sendMessage({
type: 'LOG',
source: SCRIPT_SOURCE,
step: null,
payload: { message, level, timestamp: Date.now() },
error: null,
});
}
/**
* Report that this content script is loaded and ready.
*/
function reportReady() {
console.log(LOG_PREFIX, 'Content script ready');
chrome.runtime.sendMessage({
type: 'CONTENT_SCRIPT_READY',
source: SCRIPT_SOURCE,
step: null,
payload: {},
error: null,
});
}
/**
* Report step completion.
* @param {number} step
* @param {Object} data - Step output data
*/
function reportComplete(step, data = {}) {
console.log(LOG_PREFIX, `Step ${step} completed`, data);
log(`Step ${step} completed successfully`, 'ok');
chrome.runtime.sendMessage({
type: 'STEP_COMPLETE',
source: SCRIPT_SOURCE,
step,
payload: data,
error: null,
});
}
/**
* Report step error.
* @param {number} step
* @param {string} errorMessage
*/
function reportError(step, errorMessage) {
console.error(LOG_PREFIX, `Step ${step} failed: ${errorMessage}`);
log(`Step ${step} failed: ${errorMessage}`, 'error');
chrome.runtime.sendMessage({
type: 'STEP_ERROR',
source: SCRIPT_SOURCE,
step,
payload: {},
error: errorMessage,
});
}
/**
* Simulate a click with proper event dispatching.
* @param {Element} el
*/
function simulateClick(el) {
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) || ''}"`);
}
/**
* Wait a specified number of milliseconds.
* @param {number} ms
* @returns {Promise<void>}
*/
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
// Auto-report ready on load
reportReady();
+207
View File
@@ -0,0 +1,207 @@
// content/vps-panel.js — Content script for VPS panel (steps 1, 9)
// Injected on: http://154.26.182.181:8317/*
console.log('[MultiPage:vps-panel] Content script loaded on', location.href);
// Listen for commands from Background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'EXECUTE_STEP') {
handleStep(message.step, message.payload).then(() => {
sendResponse({ ok: true });
}).catch(err => {
reportError(message.step, err.message);
sendResponse({ error: err.message });
});
return true;
}
});
async function handleStep(step, payload) {
switch (step) {
case 1: return await step1_getOAuthLink();
case 9: return await step9_vpsVerify();
default:
throw new Error(`vps-panel.js does not handle step ${step}`);
}
}
// ============================================================
// Step 1: Get OAuth Link
// ============================================================
async function step1_getOAuthLink() {
log('Step 1: Checking VPS panel state...');
// --- 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;
try {
oauthBtn = await waitForElementByText(
'button, a, [role="button"], .el-button, div[class*="btn"]',
/oauth|OAuth/i,
10000
);
} 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
);
}
log(`Step 1: OAuth URL obtained: ${oauthUrl.slice(0, 80)}...`, 'ok');
reportComplete(1, { oauthUrl: oauthUrl.trim() });
}
// ============================================================
// Step 9: VPS Verify
// ============================================================
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...`);
// Try to find URL input field
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
);
} 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
);
}
}
fillInput(urlInput, localhostUrl);
log(`Step 9: Filled URL input with: ${localhostUrl}`);
// Find and click verify button
let verifyBtn = null;
try {
verifyBtn = await waitForElementByText(
'button, [role="button"], .el-button, a',
/verif|确认|验证|submit|提交/i,
10000
);
} catch {
throw new Error(
'Could not find verify/submit button. ' +
'Check VPS panel DOM in DevTools. URL: ' + location.href
);
}
simulateClick(verifyBtn);
log('Step 9: Clicked verify button', 'ok');
reportComplete(9);
}