From 887709e824b9ee602ce29774d951039c00e66f9e Mon Sep 17 00:00:00 2001 From: unknown <1249156074@qq.com> Date: Sun, 5 Apr 2026 08:59:03 +0800 Subject: [PATCH] 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 --- background.js | 604 +++++++++++++++++++++++++++++++++++++++ content/chatgpt.js | 211 ++++++++++++++ content/qq-mail.js | 261 +++++++++++++++++ content/signup-page.js | 266 +++++++++++++++++ content/utils.js | 226 +++++++++++++++ content/vps-panel.js | 207 ++++++++++++++ data/names.js | 38 +++ icons/icon128.png | Bin 0 -> 360 bytes icons/icon16.png | Bin 0 -> 84 bytes icons/icon48.png | Bin 0 -> 160 bytes manifest.json | 72 +++++ sidepanel/sidepanel.css | 146 ++++++++++ sidepanel/sidepanel.html | 89 ++++++ sidepanel/sidepanel.js | 234 +++++++++++++++ 14 files changed, 2354 insertions(+) create mode 100644 background.js create mode 100644 content/chatgpt.js create mode 100644 content/qq-mail.js create mode 100644 content/signup-page.js create mode 100644 content/utils.js create mode 100644 content/vps-panel.js create mode 100644 data/names.js create mode 100644 icons/icon128.png create mode 100644 icons/icon16.png create mode 100644 icons/icon48.png create mode 100644 manifest.json create mode 100644 sidepanel/sidepanel.css create mode 100644 sidepanel/sidepanel.html create mode 100644 sidepanel/sidepanel.js diff --git a/background.js b/background.js new file mode 100644 index 0000000..4de8f6e --- /dev/null +++ b/background.js @@ -0,0 +1,604 @@ +// background.js — Service Worker: orchestration, state, tab management, message routing + +importScripts('data/names.js'); + +const LOG_PREFIX = '[MultiPage:bg]'; + +// ============================================================ +// State Management (chrome.storage.session) +// ============================================================ + +const DEFAULT_STATE = { + currentStep: 0, + stepStatuses: { + 1: 'pending', 2: 'pending', 3: 'pending', 4: 'pending', 5: 'pending', + 6: 'pending', 7: 'pending', 8: 'pending', 9: 'pending', + }, + oauthUrl: null, + email: null, + password: 'mimashisha0.0', + lastEmailTimestamp: null, + localhostUrl: null, + flowStartTime: null, + tabRegistry: {}, + logs: [], +}; + +async function getState() { + const state = await chrome.storage.session.get(null); + return { ...DEFAULT_STATE, ...state }; +} + +async function setState(updates) { + console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200)); + await chrome.storage.session.set(updates); +} + +async function resetState() { + console.log(LOG_PREFIX, 'Resetting all state'); + await chrome.storage.session.clear(); + await chrome.storage.session.set({ ...DEFAULT_STATE }); +} + +// ============================================================ +// Tab Registry +// ============================================================ + +async function getTabRegistry() { + const state = await getState(); + return state.tabRegistry || {}; +} + +async function registerTab(source, tabId) { + const registry = await getTabRegistry(); + registry[source] = { tabId, ready: true }; + await setState({ tabRegistry: registry }); + console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`); +} + +async function isTabAlive(source) { + const registry = await getTabRegistry(); + const entry = registry[source]; + if (!entry) return false; + try { + await chrome.tabs.get(entry.tabId); + return true; + } catch { + // Tab no longer exists — clean up registry + registry[source] = null; + await setState({ tabRegistry: registry }); + return false; + } +} + +async function getTabId(source) { + const registry = await getTabRegistry(); + return registry[source]?.tabId || null; +} + +// ============================================================ +// Command Queue (for content scripts not yet ready) +// ============================================================ + +const pendingCommands = new Map(); // source -> { message, resolve, reject, timer } + +function queueCommand(source, message, timeout = 15000) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + pendingCommands.delete(source); + const err = `Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`; + console.error(LOG_PREFIX, err); + reject(new Error(err)); + }, timeout); + pendingCommands.set(source, { message, resolve, reject, timer }); + console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`); + }); +} + +function flushCommand(source, tabId) { + const pending = pendingCommands.get(source); + if (pending) { + clearTimeout(pending.timer); + pendingCommands.delete(source); + chrome.tabs.sendMessage(tabId, pending.message).then(pending.resolve).catch(pending.reject); + console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`); + } +} + +// ============================================================ +// Send command to content script (with readiness check) +// ============================================================ + +async function sendToContentScript(source, message) { + const registry = await getTabRegistry(); + const entry = registry[source]; + + if (!entry || !entry.ready) { + console.log(LOG_PREFIX, `${source} not ready, queuing command`); + return queueCommand(source, message); + } + + // Verify tab is still alive + const alive = await isTabAlive(source); + if (!alive) { + // Tab was closed — queue the command, it will be sent when tab is reopened + console.log(LOG_PREFIX, `${source} tab was closed, queuing command`); + return queueCommand(source, message); + } + + console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type); + return chrome.tabs.sendMessage(entry.tabId, message); +} + +// ============================================================ +// Logging +// ============================================================ + +async function addLog(message, level = 'info') { + const state = await getState(); + const logs = state.logs || []; + const entry = { message, level, timestamp: Date.now() }; + logs.push(entry); + // Keep last 500 logs + if (logs.length > 500) logs.splice(0, logs.length - 500); + await setState({ logs }); + // Broadcast to side panel + chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {}); +} + +// ============================================================ +// Step Status Management +// ============================================================ + +async function setStepStatus(step, status) { + const state = await getState(); + const statuses = { ...state.stepStatuses }; + statuses[step] = status; + await setState({ stepStatuses: statuses, currentStep: step }); + // Broadcast to side panel + chrome.runtime.sendMessage({ + type: 'STEP_STATUS_CHANGED', + payload: { step, status }, + }).catch(() => {}); +} + +// ============================================================ +// Message Handler (central router) +// ============================================================ + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message); + + handleMessage(message, sender).then(response => { + sendResponse(response); + }).catch(err => { + console.error(LOG_PREFIX, 'Handler error:', err); + sendResponse({ error: err.message }); + }); + + return true; // async response +}); + +async function handleMessage(message, sender) { + switch (message.type) { + case 'CONTENT_SCRIPT_READY': { + const tabId = sender.tab?.id; + if (tabId && message.source) { + await registerTab(message.source, tabId); + flushCommand(message.source, tabId); + await addLog(`Content script ready: ${message.source} (tab ${tabId})`); + } + return { ok: true }; + } + + case 'LOG': { + const { message: msg, level } = message.payload; + await addLog(`[${message.source}] ${msg}`, level); + return { ok: true }; + } + + 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); + return { ok: true }; + } + + case 'STEP_ERROR': { + await setStepStatus(message.step, 'failed'); + await addLog(`Step ${message.step} failed: ${message.error}`, 'error'); + return { ok: true }; + } + + case 'GET_STATE': { + return await getState(); + } + + case 'RESET': { + await resetState(); + await addLog('Flow reset', 'info'); + return { ok: true }; + } + + case 'EXECUTE_STEP': { + const step = message.payload.step; + // Save email if provided (from side panel step 3) + if (message.payload.email) { + await setState({ email: message.payload.email }); + } + await executeStep(step); + return { ok: true }; + } + + // Side panel data updates + case 'SAVE_EMAIL': { + await setState({ email: message.payload.email }); + return { ok: true }; + } + + default: + console.warn(LOG_PREFIX, `Unknown message type: ${message.type}`); + return { error: `Unknown message type: ${message.type}` }; + } +} + +// ============================================================ +// Step Data Handlers +// ============================================================ + +async function handleStepData(step, payload) { + switch (step) { + case 1: + if (payload.oauthUrl) { + await setState({ oauthUrl: payload.oauthUrl }); + // Broadcast OAuth URL to side panel + chrome.runtime.sendMessage({ + type: 'DATA_UPDATED', + payload: { oauthUrl: payload.oauthUrl }, + }).catch(() => {}); + } + break; + case 3: + if (payload.email) await setState({ email: payload.email }); + break; + case 4: + if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp }); + break; + case 8: + if (payload.localhostUrl) { + await setState({ localhostUrl: payload.localhostUrl }); + chrome.runtime.sendMessage({ + type: 'DATA_UPDATED', + payload: { localhostUrl: payload.localhostUrl }, + }).catch(() => {}); + } + break; + } +} + +// ============================================================ +// Step Execution +// ============================================================ + +async function executeStep(step) { + console.log(LOG_PREFIX, `Executing step ${step}`); + await setStepStatus(step, 'running'); + await addLog(`Step ${step} started`); + + const state = await getState(); + + // Set flow start time on first step + if (step === 1 && !state.flowStartTime) { + await setState({ flowStartTime: Date.now() }); + } + + try { + switch (step) { + case 1: await executeStep1(state); break; + case 2: await executeStep2(state); break; + case 3: await executeStep3(state); break; + case 4: await executeStep4(state); break; + case 5: await executeStep5(state); break; + case 6: await executeStep6(state); break; + case 7: await executeStep7(state); break; + case 8: await executeStep8(state); break; + case 9: await executeStep9(state); break; + default: + throw new Error(`Unknown step: ${step}`); + } + } catch (err) { + await setStepStatus(step, 'failed'); + await addLog(`Step ${step} failed: ${err.message}`, 'error'); + } +} + +// ============================================================ +// Step 1: Get OAuth Link (via vps-panel.js) +// ============================================================ + +async function executeStep1(state) { + await sendToContentScript('vps-panel', { + type: 'EXECUTE_STEP', + step: 1, + source: 'background', + payload: {}, + }); +} + +// ============================================================ +// Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register) +// ============================================================ + +async function executeStep2(state) { + if (!state.oauthUrl) { + throw new Error('No OAuth URL. Complete step 1 first.'); + } + await addLog(`Step 2: Opening auth URL in new tab: ${state.oauthUrl.slice(0, 80)}...`); + const tab = await chrome.tabs.create({ url: state.oauthUrl, active: true }); + // signup-page.js will auto-inject via manifest content_scripts + // Queue the command — it will flush when script sends READY signal + await sendToContentScript('signup-page', { + type: 'EXECUTE_STEP', + step: 2, + source: 'background', + payload: {}, + }); +} + +// ============================================================ +// Step 3: Fill Email & Password (via signup-page.js) +// ============================================================ + +async function executeStep3(state) { + if (!state.email) { + throw new Error('No email address. Paste email in Side Panel first.'); + } + await addLog(`Step 3: Filling email ${state.email} and password`); + await sendToContentScript('signup-page', { + type: 'EXECUTE_STEP', + step: 3, + source: 'background', + payload: { email: state.email }, + }); +} + +// ============================================================ +// Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js) +// ============================================================ + +async function executeStep4(state) { + // Ensure QQ Mail tab is open + const alive = await isTabAlive('qq-mail'); + if (!alive) { + await addLog('Step 4: Opening QQ Mail...'); + await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true }); + } else { + const tabId = await getTabId('qq-mail'); + if (tabId) await chrome.tabs.update(tabId, { active: true }); + } + + // Send poll command to qq-mail + const result = await sendToContentScript('qq-mail', { + type: 'POLL_EMAIL', + step: 4, + source: 'background', + payload: { + filterAfterTimestamp: state.flowStartTime || 0, + senderFilters: ['openai', 'noreply', 'verify', 'auth'], + subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'], + maxAttempts: 20, + intervalMs: 3000, + }, + }); + + if (result && result.error) { + throw new Error(result.error); + } + + if (result && result.code) { + await setState({ lastEmailTimestamp: result.emailTimestamp }); + await addLog(`Step 4: Got verification code: ${result.code}`); + + // Switch to signup 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: 4, + source: 'background', + payload: { code: result.code }, + }); + } else { + throw new Error('Signup page tab was closed. Cannot fill verification code.'); + } + } +} + +// ============================================================ +// Step 5: Fill Name & Birthday (via signup-page.js) +// ============================================================ + +async function executeStep5(state) { + const { firstName, lastName } = generateRandomName(); + const { year, month, day } = generateRandomBirthday(); + + await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`); + + await sendToContentScript('signup-page', { + type: 'EXECUTE_STEP', + step: 5, + source: 'background', + payload: { firstName, lastName, year, month, day }, + }); +} + +// ============================================================ +// Step 6: Login ChatGPT (Background opens tab, chatgpt.js handles login) +// ============================================================ + +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 }); + } + + await sendToContentScript('chatgpt', { + type: 'EXECUTE_STEP', + step: 6, + source: 'background', + payload: {}, + }); +} + +// ============================================================ +// Step 7: Get Login Verification Code (qq-mail.js polls, then fills in chatgpt.js) +// ============================================================ + +async function executeStep7(state) { + const alive = await isTabAlive('qq-mail'); + if (!alive) { + await addLog('Step 7: Opening QQ Mail...'); + await chrome.tabs.create({ url: 'https://wx.mail.qq.com/', active: true }); + } else { + const tabId = await getTabId('qq-mail'); + if (tabId) await chrome.tabs.update(tabId, { active: true }); + } + + const result = await sendToContentScript('qq-mail', { + type: 'POLL_EMAIL', + step: 7, + source: 'background', + payload: { + filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0, + senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt'], + subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'], + maxAttempts: 20, + intervalMs: 3000, + }, + }); + + if (result && result.error) { + throw new Error(result.error); + } + + 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', { + type: 'FILL_CODE', + step: 7, + source: 'background', + payload: { code: result.code }, + }); + } else { + throw new Error('ChatGPT tab was closed. Cannot fill verification code.'); + } + } +} + +// ============================================================ +// Step 8: Complete OAuth (webNavigation listener + chatgpt.js navigates) +// ============================================================ + +let webNavListener = null; + +async function executeStep8(state) { + if (!state.oauthUrl) { + throw new Error('No OAuth URL. Complete step 1 first.'); + } + + await addLog('Step 8: Setting up localhost redirect listener...'); + + // Register webNavigation listener (scoped to this step) + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (webNavListener) { + chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener); + webNavListener = null; + } + setStepStatus(8, 'failed'); + addLog('Step 8: Localhost redirect not captured after 30s. Check if OAuth authorization completed.', 'error'); + reject(new Error('Localhost redirect not captured after 30s. Check if OAuth authorization completed.')); + }, 30000); + + webNavListener = (details) => { + if (details.url.startsWith('http://localhost')) { + console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`); + chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener); + webNavListener = null; + clearTimeout(timeout); + + setState({ localhostUrl: details.url }).then(() => { + addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok'); + setStepStatus(8, 'completed'); + // Broadcast to side panel + chrome.runtime.sendMessage({ + type: 'DATA_UPDATED', + payload: { localhostUrl: details.url }, + }).catch(() => {}); + resolve(); + }); + } + }; + + 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; + } + reject(err); + }); + }); +} + +// ============================================================ +// Step 9: VPS Verify (via vps-panel.js) +// ============================================================ + +async function executeStep9(state) { + if (!state.localhostUrl) { + throw new Error('No localhost URL. Complete step 8 first.'); + } + + // Switch to VPS panel tab + const alive = await isTabAlive('vps-panel'); + if (!alive) { + await addLog('Step 9: 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 }); + } + + await sendToContentScript('vps-panel', { + type: 'EXECUTE_STEP', + step: 9, + source: 'background', + payload: {}, + }); +} + +// ============================================================ +// Open Side Panel on extension icon click +// ============================================================ + +chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); diff --git a/content/chatgpt.js b/content/chatgpt.js new file mode 100644 index 0000000..75af27e --- /dev/null +++ b/content/chatgpt.js @@ -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 +} diff --git a/content/qq-mail.js b/content/qq-mail.js new file mode 100644 index 0000000..e830b6d --- /dev/null +++ b/content/qq-mail.js @@ -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; +} diff --git a/content/signup-page.js b/content/signup-page.js new file mode 100644 index 0000000..213b03d --- /dev/null +++ b/content/signup-page.js @@ -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); +} diff --git a/content/utils.js b/content/utils.js new file mode 100644 index 0000000..99ee28e --- /dev/null +++ b/content/utils.js @@ -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} + */ +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} + */ +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} + */ +function sleep(ms) { + return new Promise(r => setTimeout(r, ms)); +} + +// Auto-report ready on load +reportReady(); diff --git a/content/vps-panel.js b/content/vps-panel.js new file mode 100644 index 0000000..5617c74 --- /dev/null +++ b/content/vps-panel.js @@ -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); +} diff --git a/data/names.js b/data/names.js new file mode 100644 index 0000000..12e1567 --- /dev/null +++ b/data/names.js @@ -0,0 +1,38 @@ +// data/names.js — English name lists for random generation + +const FIRST_NAMES = [ + 'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Christopher', + 'Mary', 'Patricia', 'Jennifer', 'Linda', 'Barbara', 'Elizabeth', 'Susan', 'Jessica', 'Sarah', 'Karen', + 'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald', 'Steven', 'Andrew', 'Paul', 'Joshua', 'Kenneth', + 'Emma', 'Olivia', 'Ava', 'Isabella', 'Sophia', 'Mia', 'Charlotte', 'Amelia', 'Harper', 'Evelyn', +]; + +const LAST_NAMES = [ + 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez', + 'Hernandez', 'Lopez', 'Gonzalez', 'Wilson', 'Anderson', 'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin', + 'Lee', 'Perez', 'Thompson', 'White', 'Harris', 'Sanchez', 'Clark', 'Ramirez', 'Lewis', 'Robinson', +]; + +/** + * Generate a random full name. + * @returns {{ firstName: string, lastName: string }} + */ +function generateRandomName() { + const firstName = FIRST_NAMES[Math.floor(Math.random() * FIRST_NAMES.length)]; + const lastName = LAST_NAMES[Math.floor(Math.random() * LAST_NAMES.length)]; + return { firstName, lastName }; +} + +/** + * Generate a random birthday (age 19-25). + * @returns {{ year: number, month: number, day: number }} + */ +function generateRandomBirthday() { + const currentYear = new Date().getFullYear(); + const age = 19 + Math.floor(Math.random() * 7); // 19 to 25 + const year = currentYear - age; + const month = 1 + Math.floor(Math.random() * 12); // 1 to 12 + const maxDay = new Date(year, month, 0).getDate(); // days in that month + const day = 1 + Math.floor(Math.random() * maxDay); + return { year, month, day }; +} diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..7e91d25ff07cc13202a32a61ca03d87f9a488fb8 GIT binary patch literal 360 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVAS_?aSW-L^Y)S=BZGp#fdl8= z%%vVHvL&gX^Ut|vH+|ht*#<@f2DSqX5)8}<3_N6WI*c9WFh1fDNN=!UJ|d}bj-ilk c2nu~?xOTM5nCa&0c?>|{>FVdQ&MBb@0Ifk~tpET3 literal 0 HcmV?d00001 diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..fe4eb4119284a39509b21b7fe9aad550c21752c1 GIT binary patch literal 84 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`(w;7kAr*6y3$p%xnlE(5AnU*i g#>H%Zi3eZKWqbc=*55t#c~=>jO%FKm8U!%&Tp@#LAg + + + + + Multi-Page Automation + + + +
+

Multi-Page Automation

+ +
+ +
+
+ + Not obtained +
+
+ + +
+
+ + Not captured +
+
+ + Waiting +
+
+ +
+
+ 1 + + +
+
+ 2 + + +
+
+ 3 + + +
+
+ 4 + + +
+
+ 5 + + +
+
+ 6 + + +
+
+ 7 + + +
+
+ 8 + + +
+
+ 9 + + +
+
+ +
+

Log

+
+
+ + + + diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js new file mode 100644 index 0000000..802e4db --- /dev/null +++ b/sidepanel/sidepanel.js @@ -0,0 +1,234 @@ +// sidepanel/sidepanel.js — Side Panel logic + +const STATUS_ICONS = { + pending: '\u2B1A', // ⬚ + running: '\u23F3', // ⏳ + completed: '\u2705', // ✅ + failed: '\u274C', // ❌ +}; + +const logArea = document.getElementById('log-area'); +const displayOauthUrl = document.getElementById('display-oauth-url'); +const displayLocalhostUrl = document.getElementById('display-localhost-url'); +const displayStatus = document.getElementById('display-status'); +const inputEmail = document.getElementById('input-email'); +const btnReset = document.getElementById('btn-reset'); + +// ============================================================ +// State Restore on load +// ============================================================ + +async function restoreState() { + try { + const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }); + + // Restore data fields + if (state.oauthUrl) { + displayOauthUrl.textContent = state.oauthUrl; + displayOauthUrl.classList.add('has-value'); + } + if (state.localhostUrl) { + displayLocalhostUrl.textContent = state.localhostUrl; + displayLocalhostUrl.classList.add('has-value'); + } + if (state.email) { + inputEmail.value = state.email; + } + + // Restore step statuses + if (state.stepStatuses) { + for (const [step, status] of Object.entries(state.stepStatuses)) { + updateStepUI(Number(step), status); + } + } + + // Restore logs + if (state.logs) { + for (const entry of state.logs) { + appendLog(entry); + } + } + + updateStatusDisplay(state); + } catch (err) { + console.error('Failed to restore state:', err); + } +} + +// ============================================================ +// UI Updates +// ============================================================ + +function updateStepUI(step, status) { + const statusEl = document.querySelector(`.step-status[data-step="${step}"]`); + if (statusEl) statusEl.textContent = STATUS_ICONS[status] || '\u2B1A'; + + // Interlock logic + updateButtonStates(); +} + +function updateButtonStates() { + // Get all current statuses from DOM + const statuses = {}; + document.querySelectorAll('.step-status').forEach(el => { + const step = Number(el.dataset.step); + const icon = el.textContent; + const status = Object.entries(STATUS_ICONS).find(([, v]) => v === icon)?.[0] || 'pending'; + statuses[step] = status; + }); + + // Find if any step is running + const anyRunning = Object.values(statuses).some(s => s === 'running'); + + for (let step = 1; step <= 9; step++) { + const btn = document.querySelector(`.step-btn[data-step="${step}"]`); + if (!btn) continue; + + if (anyRunning) { + // When any step is running, disable all buttons + btn.disabled = true; + } else if (step === 1) { + // Step 1 is always available (unless running) + btn.disabled = false; + } else { + // Steps 2-9: enabled if previous step completed (or current step failed for retry) + const prevStatus = statuses[step - 1]; + const currentStatus = statuses[step]; + btn.disabled = !(prevStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'completed'); + } + } +} + +function updateStatusDisplay(state) { + if (!state || !state.stepStatuses) return; + const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running'); + if (running) { + displayStatus.textContent = `Step ${running[0]} running...`; + displayStatus.classList.add('has-value'); + } else { + const lastCompleted = Object.entries(state.stepStatuses) + .filter(([, s]) => s === 'completed') + .map(([k]) => Number(k)) + .sort((a, b) => b - a)[0]; + if (lastCompleted === 9) { + displayStatus.textContent = 'All steps completed!'; + displayStatus.classList.add('has-value'); + } else if (lastCompleted) { + displayStatus.textContent = `Step ${lastCompleted} done. Ready for step ${lastCompleted + 1}.`; + displayStatus.classList.add('has-value'); + } else { + displayStatus.textContent = 'Waiting'; + displayStatus.classList.remove('has-value'); + } + } +} + +function appendLog(entry) { + const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false }); + const levelLabel = entry.level.toUpperCase().padEnd(5); + const line = document.createElement('div'); + line.className = `log-${entry.level}`; + line.textContent = `${time} [${levelLabel}] ${entry.message}`; + logArea.appendChild(line); + logArea.scrollTop = logArea.scrollHeight; +} + +// ============================================================ +// Button Handlers +// ============================================================ + +document.querySelectorAll('.step-btn').forEach(btn => { + btn.addEventListener('click', async () => { + const step = Number(btn.dataset.step); + + // Save email if step 3 and email input has value + if (step === 3) { + const email = inputEmail.value.trim(); + if (!email) { + appendLog({ message: 'Please paste email address first', level: 'error', timestamp: Date.now() }); + return; + } + await chrome.runtime.sendMessage({ + type: 'EXECUTE_STEP', + source: 'sidepanel', + payload: { step, email }, + }); + } else { + await chrome.runtime.sendMessage({ + type: 'EXECUTE_STEP', + source: 'sidepanel', + payload: { step }, + }); + } + }); +}); + +// Reset button +btnReset.addEventListener('click', async () => { + if (confirm('Reset all steps and data?')) { + await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' }); + // Clear UI + displayOauthUrl.textContent = 'Not obtained'; + displayOauthUrl.classList.remove('has-value'); + displayLocalhostUrl.textContent = 'Not captured'; + displayLocalhostUrl.classList.remove('has-value'); + inputEmail.value = ''; + displayStatus.textContent = 'Waiting'; + displayStatus.classList.remove('has-value'); + logArea.innerHTML = ''; + document.querySelectorAll('.step-status').forEach(el => el.textContent = '\u2B1A'); + updateButtonStates(); + } +}); + +// Save email when user types/pastes +inputEmail.addEventListener('change', async () => { + const email = inputEmail.value.trim(); + if (email) { + await chrome.runtime.sendMessage({ + type: 'SAVE_EMAIL', + source: 'sidepanel', + payload: { email }, + }); + } +}); + +// ============================================================ +// Listen for Background broadcasts +// ============================================================ + +chrome.runtime.onMessage.addListener((message) => { + switch (message.type) { + case 'LOG_ENTRY': + appendLog(message.payload); + break; + + case 'STEP_STATUS_CHANGED': { + const { step, status } = message.payload; + updateStepUI(step, status); + // Update status display + chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay); + break; + } + + case 'DATA_UPDATED': { + if (message.payload.oauthUrl) { + displayOauthUrl.textContent = message.payload.oauthUrl; + displayOauthUrl.classList.add('has-value'); + } + if (message.payload.localhostUrl) { + displayLocalhostUrl.textContent = message.payload.localhostUrl; + displayLocalhostUrl.classList.add('has-value'); + } + break; + } + } +}); + +// ============================================================ +// Init +// ============================================================ + +restoreState().then(() => { + updateButtonStates(); +});