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:
+604
@@ -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 });
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 360 B |
Binary file not shown.
|
After Width: | Height: | Size: 84 B |
Binary file not shown.
|
After Width: | Height: | Size: 160 B |
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Multi-Page Automation",
|
||||
"version": "1.0.0",
|
||||
"description": "Automates multi-step OAuth registration workflow",
|
||||
"permissions": [
|
||||
"sidePanel",
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"storage",
|
||||
"scripting",
|
||||
"activeTab"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://154.26.182.181:8317/*",
|
||||
"https://auth0.openai.com/*",
|
||||
"https://auth.openai.com/*",
|
||||
"https://accounts.openai.com/*",
|
||||
"https://mail.qq.com/*",
|
||||
"https://wx.mail.qq.com/*",
|
||||
"https://chatgpt.com/*",
|
||||
"http://localhost/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"side_panel": {
|
||||
"default_path": "sidepanel/sidepanel.html"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://154.26.182.181:8317/*"],
|
||||
"js": ["content/utils.js", "content/vps-panel.js"],
|
||||
"run_at": "document_idle"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"https://auth0.openai.com/*",
|
||||
"https://auth.openai.com/*",
|
||||
"https://accounts.openai.com/*"
|
||||
],
|
||||
"js": ["content/utils.js", "content/signup-page.js"],
|
||||
"run_at": "document_idle"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"https://mail.qq.com/*",
|
||||
"https://wx.mail.qq.com/*"
|
||||
],
|
||||
"js": ["content/utils.js", "content/qq-mail.js"],
|
||||
"all_frames": true,
|
||||
"run_at": "document_idle"
|
||||
},
|
||||
{
|
||||
"matches": ["https://chatgpt.com/*"],
|
||||
"js": ["content/utils.js", "content/chatgpt.js"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/* sidepanel/sidepanel.css */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#btn-reset {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#btn-reset:hover { background: #c82333; }
|
||||
|
||||
/* Data Section */
|
||||
#data-section {
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.data-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.data-row:last-child { margin-bottom: 0; }
|
||||
|
||||
.data-row label {
|
||||
width: 80px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.data-value.has-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#input-email {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Steps Section */
|
||||
#steps-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.step-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.step-btn {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.step-btn:hover:not(:disabled) { background: #0069d9; }
|
||||
.step-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.step-status {
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Log Section */
|
||||
#log-section h2 {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
#log-area {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
height: 250px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.log-info { color: #d4d4d4; }
|
||||
.log-ok { color: #4ec9b0; }
|
||||
.log-warn { color: #dcdcaa; }
|
||||
.log-error { color: #f44747; }
|
||||
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Multi-Page Automation</title>
|
||||
<link rel="stylesheet" href="sidepanel.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Multi-Page Automation</h1>
|
||||
<button id="btn-reset" title="Reset all steps">Reset</button>
|
||||
</header>
|
||||
|
||||
<section id="data-section">
|
||||
<div class="data-row">
|
||||
<label>OAuth URL:</label>
|
||||
<span id="display-oauth-url" class="data-value">Not obtained</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<label>Email:</label>
|
||||
<input type="text" id="input-email" placeholder="Paste DuckDuckGo email here" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<label>Localhost:</label>
|
||||
<span id="display-localhost-url" class="data-value">Not captured</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<label>Status:</label>
|
||||
<span id="display-status" class="data-value">Waiting</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="steps-section">
|
||||
<div class="step-row" data-step="1">
|
||||
<span class="step-num">1</span>
|
||||
<button class="step-btn" data-step="1">Get OAuth Link</button>
|
||||
<span class="step-status" data-step="1">⬚</span>
|
||||
</div>
|
||||
<div class="step-row" data-step="2">
|
||||
<span class="step-num">2</span>
|
||||
<button class="step-btn" data-step="2">Open Signup</button>
|
||||
<span class="step-status" data-step="2">⬚</span>
|
||||
</div>
|
||||
<div class="step-row" data-step="3">
|
||||
<span class="step-num">3</span>
|
||||
<button class="step-btn" data-step="3">Fill Email/Password</button>
|
||||
<span class="step-status" data-step="3">⬚</span>
|
||||
</div>
|
||||
<div class="step-row" data-step="4">
|
||||
<span class="step-num">4</span>
|
||||
<button class="step-btn" data-step="4">Get Signup Code</button>
|
||||
<span class="step-status" data-step="4">⬚</span>
|
||||
</div>
|
||||
<div class="step-row" data-step="5">
|
||||
<span class="step-num">5</span>
|
||||
<button class="step-btn" data-step="5">Fill Name/Birthday</button>
|
||||
<span class="step-status" data-step="5">⬚</span>
|
||||
</div>
|
||||
<div class="step-row" data-step="6">
|
||||
<span class="step-num">6</span>
|
||||
<button class="step-btn" data-step="6">Login ChatGPT</button>
|
||||
<span class="step-status" data-step="6">⬚</span>
|
||||
</div>
|
||||
<div class="step-row" data-step="7">
|
||||
<span class="step-num">7</span>
|
||||
<button class="step-btn" data-step="7">Get Login Code</button>
|
||||
<span class="step-status" data-step="7">⬚</span>
|
||||
</div>
|
||||
<div class="step-row" data-step="8">
|
||||
<span class="step-num">8</span>
|
||||
<button class="step-btn" data-step="8">Complete OAuth</button>
|
||||
<span class="step-status" data-step="8">⬚</span>
|
||||
</div>
|
||||
<div class="step-row" data-step="9">
|
||||
<span class="step-num">9</span>
|
||||
<button class="step-btn" data-step="9">VPS Verify</button>
|
||||
<span class="step-status" data-step="9">⬚</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="log-section">
|
||||
<h2>Log</h2>
|
||||
<div id="log-area"></div>
|
||||
</section>
|
||||
|
||||
<script src="sidepanel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user