feat: add DuckDuckGo Email Protection autofill settings
- Implemented a new content script for DuckDuckGo Email autofill functionality. - Enhanced email polling scripts (mail-163.js, qq-mail.js, signup-page.js, vps-panel.js) to handle user flow interruptions. - Updated utility functions to manage flow stopping and error handling. - Introduced a stop button in the side panel for user control over ongoing processes. - Improved UI elements and styles for better user experience, including new button states and messages. - Adjusted the side panel to fetch DuckDuckGo email automatically and display it in the input field.
This commit is contained in:
Vendored
@@ -0,0 +1,41 @@
|
|||||||
|
# Multi-Page Automation
|
||||||
|
|
||||||
|
ChatGPT OAuth 批量注册自动化 Chrome 扩展。
|
||||||
|
|
||||||
|
## 前提
|
||||||
|
|
||||||
|
- 注册 [DuckDuckGo Email Protection](https://duckduckgo.com/email/),获取 @duck.com 邮箱地址,转发目标设为 163 或 QQ 邮箱
|
||||||
|
- Chrome 浏览器,开发者模式加载本扩展
|
||||||
|
- 提前登录好 VPS 管理面板和对应的邮箱(163 或 QQ)
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
1. 打开 `chrome://extensions/`,开启「开发者模式」
|
||||||
|
2. 点击「加载已解压的扩展程序」,选择本项目文件夹
|
||||||
|
3. 点击浏览器工具栏的扩展图标,打开侧边面板
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
在侧边面板填入 VPS 管理面板地址,选择邮箱类型,设置运行次数,点击 Auto:
|
||||||
|
|
||||||
|
1. **获取 OAuth 链接** — 自动打开 VPS 面板,点击登录拿到 OpenAI 授权链接
|
||||||
|
2. **打开注册页** — 用授权链接打开 OpenAI 注册页,自动点击 Sign up
|
||||||
|
3. ⏸️ **暂停等待** — 面板弹出橙色提示,**手动粘贴一个新的 @duck.com 邮箱地址**,点击 Continue
|
||||||
|
4. **填写注册信息** — 自动将 @duck.com 地址填入注册表单,自动生成随机密码,提交
|
||||||
|
5. **获取注册验证码** — 自动切到邮箱轮询收件箱,提取 6 位验证码,填回注册页
|
||||||
|
6. **完善资料** — 自动随机生成英文姓名和生日,提交完成注册
|
||||||
|
7. **登录** — 重新打开 OAuth 链接,用刚注册的账号自动登录
|
||||||
|
8. **获取登录验证码** — 再次轮询邮箱拿验证码,自动填入
|
||||||
|
9. **完成 OAuth 授权** — 自动点击授权页「继续」,后台捕获回调地址
|
||||||
|
10. **VPS 回调验证** — 自动将回调地址提交到 VPS 面板,完成认证
|
||||||
|
|
||||||
|
> 全程仅需在第 3 步手动粘贴一次邮箱地址,其余全自动。
|
||||||
|
> 支持设置 N 次连续批量运行,每轮在第 2 步后暂停等待粘贴新邮箱。
|
||||||
|
|
||||||
|
## 安全说明
|
||||||
|
|
||||||
|
- VPS 地址在面板中以密码框显示,不会明文泄露
|
||||||
|
- 每次注册自动生成随机强密码(14位,含大小写+数字+符号)
|
||||||
|
- 所有状态存储在 `chrome.storage.session`,浏览器关闭即清除
|
||||||
|
- 代码中无硬编码的密码、IP 地址等敏感信息
|
||||||
|
|
||||||
+261
-63
@@ -3,6 +3,10 @@
|
|||||||
importScripts('data/names.js');
|
importScripts('data/names.js');
|
||||||
|
|
||||||
const LOG_PREFIX = '[MultiPage:bg]';
|
const LOG_PREFIX = '[MultiPage:bg]';
|
||||||
|
const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill';
|
||||||
|
const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
|
||||||
|
const HUMAN_STEP_DELAY_MIN = 700;
|
||||||
|
const HUMAN_STEP_DELAY_MAX = 2200;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// State Management (chrome.storage.session)
|
// State Management (chrome.storage.session)
|
||||||
@@ -37,6 +41,18 @@ async function setState(updates) {
|
|||||||
await chrome.storage.session.set(updates);
|
await chrome.storage.session.set(updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function broadcastDataUpdate(payload) {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: 'DATA_UPDATED',
|
||||||
|
payload,
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setEmailState(email) {
|
||||||
|
await setState({ email });
|
||||||
|
broadcastDataUpdate({ email });
|
||||||
|
}
|
||||||
|
|
||||||
async function resetState() {
|
async function resetState() {
|
||||||
console.log(LOG_PREFIX, 'Resetting all state');
|
console.log(LOG_PREFIX, 'Resetting all state');
|
||||||
// Preserve settings and persistent data across resets
|
// Preserve settings and persistent data across resets
|
||||||
@@ -143,6 +159,15 @@ function flushCommand(source, tabId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) {
|
||||||
|
for (const [source, pending] of pendingCommands.entries()) {
|
||||||
|
clearTimeout(pending.timer);
|
||||||
|
pending.reject(new Error(reason));
|
||||||
|
pendingCommands.delete(source);
|
||||||
|
console.log(LOG_PREFIX, `Cancelled queued command for ${source}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Reuse or create tab
|
// Reuse or create tab
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -151,9 +176,30 @@ async function reuseOrCreateTab(source, url, options = {}) {
|
|||||||
const alive = await isTabAlive(source);
|
const alive = await isTabAlive(source);
|
||||||
if (alive) {
|
if (alive) {
|
||||||
const tabId = await getTabId(source);
|
const tabId = await getTabId(source);
|
||||||
|
const currentTab = await chrome.tabs.get(tabId);
|
||||||
|
const sameUrl = currentTab.url === url;
|
||||||
|
|
||||||
|
const registry = await getTabRegistry();
|
||||||
|
if (sameUrl) {
|
||||||
|
await chrome.tabs.update(tabId, { active: true });
|
||||||
|
console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}) on same URL`);
|
||||||
|
|
||||||
|
// For dynamically injected pages like the VPS panel, re-inject immediately.
|
||||||
|
// Waiting for a navigation event here will hang when the URL hasn't changed.
|
||||||
|
if (options.inject) {
|
||||||
|
if (registry[source]) registry[source].ready = false;
|
||||||
|
await setState({ tabRegistry: registry });
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
files: options.inject,
|
||||||
|
});
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabId;
|
||||||
|
}
|
||||||
|
|
||||||
// Mark as not ready BEFORE navigating — so READY signal from new page is captured correctly
|
// Mark as not ready BEFORE navigating — so READY signal from new page is captured correctly
|
||||||
const registry = await getTabRegistry();
|
|
||||||
if (registry[source]) registry[source].ready = false;
|
if (registry[source]) registry[source].ready = false;
|
||||||
await setState({ tabRegistry: registry });
|
await setState({ tabRegistry: registry });
|
||||||
|
|
||||||
@@ -271,6 +317,50 @@ async function setStepStatus(step, status) {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStopError(error) {
|
||||||
|
const message = typeof error === 'string' ? error : error?.message;
|
||||||
|
return message === STOP_ERROR_MESSAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStopRequest() {
|
||||||
|
stopRequested = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwIfStopped() {
|
||||||
|
if (stopRequested) {
|
||||||
|
throw new Error(STOP_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sleepWithStop(ms) {
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < ms) {
|
||||||
|
throwIfStopped();
|
||||||
|
await new Promise(r => setTimeout(r, Math.min(100, ms - (Date.now() - start))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY_MAX) {
|
||||||
|
const duration = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
await sleepWithStop(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function broadcastStopToContentScripts() {
|
||||||
|
const registry = await getTabRegistry();
|
||||||
|
for (const entry of Object.values(registry)) {
|
||||||
|
if (!entry?.tabId) continue;
|
||||||
|
try {
|
||||||
|
await chrome.tabs.sendMessage(entry.tabId, {
|
||||||
|
type: 'STOP_FLOW',
|
||||||
|
source: 'background',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stopRequested = false;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Message Handler (central router)
|
// Message Handler (central router)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -307,6 +397,11 @@ async function handleMessage(message, sender) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'STEP_COMPLETE': {
|
case 'STEP_COMPLETE': {
|
||||||
|
if (stopRequested) {
|
||||||
|
await setStepStatus(message.step, 'stopped');
|
||||||
|
notifyStepError(message.step, STOP_ERROR_MESSAGE);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
await setStepStatus(message.step, 'completed');
|
await setStepStatus(message.step, 'completed');
|
||||||
await addLog(`Step ${message.step} completed`, 'ok');
|
await addLog(`Step ${message.step} completed`, 'ok');
|
||||||
await handleStepData(message.step, message.payload);
|
await handleStepData(message.step, message.payload);
|
||||||
@@ -315,9 +410,15 @@ async function handleMessage(message, sender) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'STEP_ERROR': {
|
case 'STEP_ERROR': {
|
||||||
await setStepStatus(message.step, 'failed');
|
if (isStopError(message.error)) {
|
||||||
await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
|
await setStepStatus(message.step, 'stopped');
|
||||||
notifyStepError(message.step, message.error);
|
await addLog(`Step ${message.step} stopped by user`, 'warn');
|
||||||
|
notifyStepError(message.step, message.error);
|
||||||
|
} else {
|
||||||
|
await setStepStatus(message.step, 'failed');
|
||||||
|
await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
|
||||||
|
notifyStepError(message.step, message.error);
|
||||||
|
}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,30 +427,34 @@ async function handleMessage(message, sender) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'RESET': {
|
case 'RESET': {
|
||||||
|
clearStopRequest();
|
||||||
await resetState();
|
await resetState();
|
||||||
await addLog('Flow reset', 'info');
|
await addLog('Flow reset', 'info');
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'EXECUTE_STEP': {
|
case 'EXECUTE_STEP': {
|
||||||
|
clearStopRequest();
|
||||||
const step = message.payload.step;
|
const step = message.payload.step;
|
||||||
// Save email if provided (from side panel step 3)
|
// Save email if provided (from side panel step 3)
|
||||||
if (message.payload.email) {
|
if (message.payload.email) {
|
||||||
await setState({ email: message.payload.email });
|
await setEmailState(message.payload.email);
|
||||||
}
|
}
|
||||||
await executeStep(step);
|
await executeStep(step);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'AUTO_RUN': {
|
case 'AUTO_RUN': {
|
||||||
|
clearStopRequest();
|
||||||
const totalRuns = message.payload?.totalRuns || 1;
|
const totalRuns = message.payload?.totalRuns || 1;
|
||||||
autoRunLoop(totalRuns); // fire-and-forget
|
autoRunLoop(totalRuns); // fire-and-forget
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'RESUME_AUTO_RUN': {
|
case 'RESUME_AUTO_RUN': {
|
||||||
|
clearStopRequest();
|
||||||
if (message.payload.email) {
|
if (message.payload.email) {
|
||||||
await setState({ email: message.payload.email });
|
await setEmailState(message.payload.email);
|
||||||
}
|
}
|
||||||
resumeAutoRun(); // fire-and-forget
|
resumeAutoRun(); // fire-and-forget
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
@@ -365,7 +470,18 @@ async function handleMessage(message, sender) {
|
|||||||
|
|
||||||
// Side panel data updates
|
// Side panel data updates
|
||||||
case 'SAVE_EMAIL': {
|
case 'SAVE_EMAIL': {
|
||||||
await setState({ email: message.payload.email });
|
await setEmailState(message.payload.email);
|
||||||
|
return { ok: true, email: message.payload.email };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'FETCH_DUCK_EMAIL': {
|
||||||
|
clearStopRequest();
|
||||||
|
const email = await fetchDuckEmail(message.payload || {});
|
||||||
|
return { ok: true, email };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'STOP_FLOW': {
|
||||||
|
await requestStop();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,15 +500,11 @@ async function handleStepData(step, payload) {
|
|||||||
case 1:
|
case 1:
|
||||||
if (payload.oauthUrl) {
|
if (payload.oauthUrl) {
|
||||||
await setState({ oauthUrl: payload.oauthUrl });
|
await setState({ oauthUrl: payload.oauthUrl });
|
||||||
// Broadcast OAuth URL to side panel
|
broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
|
||||||
chrome.runtime.sendMessage({
|
|
||||||
type: 'DATA_UPDATED',
|
|
||||||
payload: { oauthUrl: payload.oauthUrl },
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
if (payload.email) await setState({ email: payload.email });
|
if (payload.email) await setEmailState(payload.email);
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp });
|
if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp });
|
||||||
@@ -400,10 +512,7 @@ async function handleStepData(step, payload) {
|
|||||||
case 8:
|
case 8:
|
||||||
if (payload.localhostUrl) {
|
if (payload.localhostUrl) {
|
||||||
await setState({ localhostUrl: payload.localhostUrl });
|
await setState({ localhostUrl: payload.localhostUrl });
|
||||||
chrome.runtime.sendMessage({
|
broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
|
||||||
type: 'DATA_UPDATED',
|
|
||||||
payload: { localhostUrl: payload.localhostUrl },
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -415,9 +524,11 @@ async function handleStepData(step, payload) {
|
|||||||
|
|
||||||
// Map of step -> { resolve, reject } for waiting on step completion
|
// Map of step -> { resolve, reject } for waiting on step completion
|
||||||
const stepWaiters = new Map();
|
const stepWaiters = new Map();
|
||||||
|
let resumeWaiter = null;
|
||||||
|
|
||||||
function waitForStepComplete(step, timeoutMs = 120000) {
|
function waitForStepComplete(step, timeoutMs = 120000) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
throwIfStopped();
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
stepWaiters.delete(step);
|
stepWaiters.delete(step);
|
||||||
reject(new Error(`Step ${step} timed out after ${timeoutMs / 1000}s`));
|
reject(new Error(`Step ${step} timed out after ${timeoutMs / 1000}s`));
|
||||||
@@ -440,14 +551,59 @@ function notifyStepError(step, error) {
|
|||||||
if (waiter) waiter.reject(new Error(error));
|
if (waiter) waiter.reject(new Error(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function markRunningStepsStopped() {
|
||||||
|
const state = await getState();
|
||||||
|
const runningSteps = Object.entries(state.stepStatuses || {})
|
||||||
|
.filter(([, status]) => status === 'running')
|
||||||
|
.map(([step]) => Number(step));
|
||||||
|
|
||||||
|
for (const step of runningSteps) {
|
||||||
|
await setStepStatus(step, 'stopped');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestStop() {
|
||||||
|
if (stopRequested) return;
|
||||||
|
|
||||||
|
stopRequested = true;
|
||||||
|
cancelPendingCommands();
|
||||||
|
if (webNavListener) {
|
||||||
|
chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
|
||||||
|
webNavListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog('Stop requested. Cancelling current operations...', 'warn');
|
||||||
|
await broadcastStopToContentScripts();
|
||||||
|
|
||||||
|
for (const waiter of stepWaiters.values()) {
|
||||||
|
waiter.reject(new Error(STOP_ERROR_MESSAGE));
|
||||||
|
}
|
||||||
|
stepWaiters.clear();
|
||||||
|
|
||||||
|
if (resumeWaiter) {
|
||||||
|
resumeWaiter.reject(new Error(STOP_ERROR_MESSAGE));
|
||||||
|
resumeWaiter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await markRunningStepsStopped();
|
||||||
|
autoRunActive = false;
|
||||||
|
await setState({ autoRunning: false });
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: 'AUTO_RUN_STATUS',
|
||||||
|
payload: { phase: 'stopped', currentRun: autoRunCurrentRun, totalRuns: autoRunTotalRuns },
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Step Execution
|
// Step Execution
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function executeStep(step) {
|
async function executeStep(step) {
|
||||||
console.log(LOG_PREFIX, `Executing step ${step}`);
|
console.log(LOG_PREFIX, `Executing step ${step}`);
|
||||||
|
throwIfStopped();
|
||||||
await setStepStatus(step, 'running');
|
await setStepStatus(step, 'running');
|
||||||
await addLog(`Step ${step} started`);
|
await addLog(`Step ${step} started`);
|
||||||
|
await humanStepDelay();
|
||||||
|
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
|
|
||||||
@@ -471,8 +627,14 @@ async function executeStep(step) {
|
|||||||
throw new Error(`Unknown step: ${step}`);
|
throw new Error(`Unknown step: ${step}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (isStopError(err)) {
|
||||||
|
await setStepStatus(step, 'stopped');
|
||||||
|
await addLog(`Step ${step} stopped by user`, 'warn');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
await setStepStatus(step, 'failed');
|
await setStepStatus(step, 'failed');
|
||||||
await addLog(`Step ${step} failed: ${err.message}`, 'error');
|
await addLog(`Step ${step} failed: ${err.message}`, 'error');
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,15 +644,41 @@ async function executeStep(step) {
|
|||||||
* @param {number} delayAfter - ms to wait after completion (for page transitions)
|
* @param {number} delayAfter - ms to wait after completion (for page transitions)
|
||||||
*/
|
*/
|
||||||
async function executeStepAndWait(step, delayAfter = 2000) {
|
async function executeStepAndWait(step, delayAfter = 2000) {
|
||||||
|
throwIfStopped();
|
||||||
const promise = waitForStepComplete(step, 120000);
|
const promise = waitForStepComplete(step, 120000);
|
||||||
await executeStep(step);
|
await executeStep(step);
|
||||||
await promise;
|
await promise;
|
||||||
// Extra delay for page transitions / DOM updates
|
// Extra delay for page transitions / DOM updates
|
||||||
if (delayAfter > 0) {
|
if (delayAfter > 0) {
|
||||||
await new Promise(r => setTimeout(r, delayAfter));
|
await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchDuckEmail(options = {}) {
|
||||||
|
throwIfStopped();
|
||||||
|
const { generateNew = true } = options;
|
||||||
|
|
||||||
|
await addLog(`Duck Mail: Opening autofill settings (${generateNew ? 'generate new' : 'reuse current'})...`);
|
||||||
|
await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL);
|
||||||
|
|
||||||
|
const result = await sendToContentScript('duck-mail', {
|
||||||
|
type: 'FETCH_DUCK_EMAIL',
|
||||||
|
source: 'background',
|
||||||
|
payload: { generateNew },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
if (!result?.email) {
|
||||||
|
throw new Error('Duck email not returned.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await setEmailState(result.email);
|
||||||
|
await addLog(`Duck Mail: ${result.generated ? 'Generated' : 'Loaded'} ${result.email}`, 'ok');
|
||||||
|
return result.email;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Auto Run Flow
|
// Auto Run Flow
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -506,6 +694,7 @@ async function autoRunLoop(totalRuns) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearStopRequest();
|
||||||
autoRunActive = true;
|
autoRunActive = true;
|
||||||
autoRunTotalRuns = totalRuns;
|
autoRunTotalRuns = totalRuns;
|
||||||
await setState({ autoRunning: true });
|
await setState({ autoRunning: true });
|
||||||
@@ -524,33 +713,49 @@ async function autoRunLoop(totalRuns) {
|
|||||||
await setState(keepSettings);
|
await setState(keepSettings);
|
||||||
// Tell side panel to reset all UI
|
// Tell side panel to reset all UI
|
||||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {});
|
chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {});
|
||||||
await new Promise(r => setTimeout(r, 500));
|
await sleepWithStop(500);
|
||||||
|
|
||||||
await addLog(`=== Auto Run ${run}/${totalRuns} — Phase 1: Get OAuth link & open signup ===`, 'info');
|
await addLog(`=== Auto Run ${run}/${totalRuns} — Phase 1: Get OAuth link & open signup ===`, 'info');
|
||||||
const status = (phase) => ({ type: 'AUTO_RUN_STATUS', payload: { phase, currentRun: run, totalRuns } });
|
const status = (phase) => ({ type: 'AUTO_RUN_STATUS', payload: { phase, currentRun: run, totalRuns } });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
throwIfStopped();
|
||||||
chrome.runtime.sendMessage(status('running')).catch(() => {});
|
chrome.runtime.sendMessage(status('running')).catch(() => {});
|
||||||
|
|
||||||
await executeStepAndWait(1, 2000);
|
await executeStepAndWait(1, 2000);
|
||||||
await executeStepAndWait(2, 2000);
|
await executeStepAndWait(2, 2000);
|
||||||
|
|
||||||
// Pause for email
|
let emailReady = false;
|
||||||
await addLog(`=== Run ${run}/${totalRuns} PAUSED: Paste DuckDuckGo email, click Continue ===`, 'warn');
|
try {
|
||||||
chrome.runtime.sendMessage(status('waiting_email')).catch(() => {});
|
const duckEmail = await fetchDuckEmail({ generateNew: true });
|
||||||
|
await addLog(`=== Run ${run}/${totalRuns} — Duck email ready: ${duckEmail} ===`, 'ok');
|
||||||
|
emailReady = true;
|
||||||
|
} catch (err) {
|
||||||
|
await addLog(`Duck Mail auto-fetch failed: ${err.message}`, 'warn');
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for RESUME_AUTO_RUN — sets a promise that resumeAutoRun resolves
|
if (!emailReady) {
|
||||||
await waitForResume();
|
await addLog(`=== Run ${run}/${totalRuns} PAUSED: Fetch Duck email or paste manually, then continue ===`, 'warn');
|
||||||
|
chrome.runtime.sendMessage(status('waiting_email')).catch(() => {});
|
||||||
|
|
||||||
const state = await getState();
|
// Wait for RESUME_AUTO_RUN — sets a promise that resumeAutoRun resolves
|
||||||
if (!state.email) {
|
await waitForResume();
|
||||||
await addLog('Cannot resume: no email address.', 'error');
|
|
||||||
break;
|
const resumedState = await getState();
|
||||||
|
if (!resumedState.email) {
|
||||||
|
await addLog('Cannot resume: no email address.', 'error');
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info');
|
await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info');
|
||||||
chrome.runtime.sendMessage(status('running')).catch(() => {});
|
chrome.runtime.sendMessage(status('running')).catch(() => {});
|
||||||
|
|
||||||
|
const signupTabId = await getTabId('signup-page');
|
||||||
|
if (signupTabId) {
|
||||||
|
await chrome.tabs.update(signupTabId, { active: true });
|
||||||
|
}
|
||||||
|
|
||||||
await executeStepAndWait(3, 3000);
|
await executeStepAndWait(3, 3000);
|
||||||
await executeStepAndWait(4, 2000);
|
await executeStepAndWait(4, 2000);
|
||||||
await executeStepAndWait(5, 3000);
|
await executeStepAndWait(5, 3000);
|
||||||
@@ -562,41 +767,49 @@ async function autoRunLoop(totalRuns) {
|
|||||||
await addLog(`=== Run ${run}/${totalRuns} COMPLETE! ===`, 'ok');
|
await addLog(`=== Run ${run}/${totalRuns} COMPLETE! ===`, 'ok');
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await addLog(`Run ${run}/${totalRuns} failed: ${err.message}`, 'error');
|
if (isStopError(err)) {
|
||||||
|
await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn');
|
||||||
|
} else {
|
||||||
|
await addLog(`Run ${run}/${totalRuns} failed: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
chrome.runtime.sendMessage(status('stopped')).catch(() => {});
|
chrome.runtime.sendMessage(status('stopped')).catch(() => {});
|
||||||
break; // Stop on error
|
break; // Stop on error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const completedRuns = autoRunCurrentRun;
|
const completedRuns = autoRunCurrentRun;
|
||||||
if (completedRuns >= autoRunTotalRuns) {
|
if (stopRequested) {
|
||||||
|
await addLog(`=== Stopped after ${Math.max(0, completedRuns - 1)}/${autoRunTotalRuns} runs ===`, 'warn');
|
||||||
|
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
|
||||||
|
} else if (completedRuns >= autoRunTotalRuns) {
|
||||||
await addLog(`=== All ${autoRunTotalRuns} runs completed successfully ===`, 'ok');
|
await addLog(`=== All ${autoRunTotalRuns} runs completed successfully ===`, 'ok');
|
||||||
|
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
await addLog(`=== Stopped after ${completedRuns}/${autoRunTotalRuns} runs ===`, 'warn');
|
await addLog(`=== Stopped after ${completedRuns}/${autoRunTotalRuns} runs ===`, 'warn');
|
||||||
|
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
|
||||||
}
|
}
|
||||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
|
|
||||||
autoRunActive = false;
|
autoRunActive = false;
|
||||||
await setState({ autoRunning: false });
|
await setState({ autoRunning: false });
|
||||||
|
clearStopRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Promise-based pause/resume mechanism
|
|
||||||
let resumeResolver = null;
|
|
||||||
|
|
||||||
function waitForResume() {
|
function waitForResume() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
resumeResolver = resolve;
|
throwIfStopped();
|
||||||
|
resumeWaiter = { resolve, reject };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resumeAutoRun() {
|
async function resumeAutoRun() {
|
||||||
|
throwIfStopped();
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
if (!state.email) {
|
if (!state.email) {
|
||||||
await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error');
|
await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (resumeResolver) {
|
if (resumeWaiter) {
|
||||||
resumeResolver();
|
resumeWaiter.resolve();
|
||||||
resumeResolver = null;
|
resumeWaiter = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,7 +1036,7 @@ async function executeStep7(state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Step 8: Complete OAuth (webNavigation listener + chatgpt.js navigates)
|
// Step 8: Complete OAuth (manual click + localhost listener)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
let webNavListener = null;
|
let webNavListener = null;
|
||||||
@@ -833,7 +1046,7 @@ async function executeStep8(state) {
|
|||||||
throw new Error('No OAuth URL. Complete step 1 first.');
|
throw new Error('No OAuth URL. Complete step 1 first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog('Step 8: Setting up localhost redirect listener...');
|
await addLog('Step 8: Setting up localhost redirect listener for manual confirmation...');
|
||||||
|
|
||||||
// Register webNavigation listener (scoped to this step)
|
// Register webNavigation listener (scoped to this step)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -843,9 +1056,9 @@ async function executeStep8(state) {
|
|||||||
webNavListener = null;
|
webNavListener = null;
|
||||||
}
|
}
|
||||||
setStepStatus(8, 'failed');
|
setStepStatus(8, 'failed');
|
||||||
addLog('Step 8: Localhost redirect not captured after 30s. Check if OAuth authorization completed.', 'error');
|
addLog('Step 8: Localhost redirect not captured after 120s. Please confirm you clicked "继续" on the OAuth page.', 'error');
|
||||||
reject(new Error('Localhost redirect not captured after 30s. Check if OAuth authorization completed.'));
|
reject(new Error('Localhost redirect not captured after 120s. Please click "继续" on the OAuth page.'));
|
||||||
}, 30000);
|
}, 120000);
|
||||||
|
|
||||||
webNavListener = (details) => {
|
webNavListener = (details) => {
|
||||||
if (details.url.startsWith('http://localhost')) {
|
if (details.url.startsWith('http://localhost')) {
|
||||||
@@ -858,10 +1071,7 @@ async function executeStep8(state) {
|
|||||||
addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok');
|
addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok');
|
||||||
setStepStatus(8, 'completed');
|
setStepStatus(8, 'completed');
|
||||||
notifyStepComplete(8, { localhostUrl: details.url });
|
notifyStepComplete(8, { localhostUrl: details.url });
|
||||||
chrome.runtime.sendMessage({
|
broadcastDataUpdate({ localhostUrl: details.url });
|
||||||
type: 'DATA_UPDATED',
|
|
||||||
payload: { localhostUrl: details.url },
|
|
||||||
}).catch(() => {});
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -870,28 +1080,16 @@ async function executeStep8(state) {
|
|||||||
chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
|
chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
|
||||||
|
|
||||||
// After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
|
// After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
|
||||||
// with a "继续" button. We need to click it, which triggers the localhost redirect.
|
// with a "继续" button. The user must click it manually.
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const signupTabId = await getTabId('signup-page');
|
const signupTabId = await getTabId('signup-page');
|
||||||
if (signupTabId) {
|
if (signupTabId) {
|
||||||
await chrome.tabs.update(signupTabId, { active: true });
|
await chrome.tabs.update(signupTabId, { active: true });
|
||||||
await addLog('Step 8: Switching to auth page, clicking "继续" to complete OAuth...');
|
await addLog('Step 8: Switched to auth page. Please click "继续" manually to complete OAuth.', 'warn');
|
||||||
await sendToContentScript('signup-page', {
|
|
||||||
type: 'EXECUTE_STEP',
|
|
||||||
step: 8,
|
|
||||||
source: 'background',
|
|
||||||
payload: {},
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||||
await addLog('Step 8: Auth tab reopened...');
|
await addLog('Step 8: Auth tab reopened. Please click "继续" manually to complete OAuth.', 'warn');
|
||||||
await sendToContentScript('signup-page', {
|
|
||||||
type: 'EXECUTE_STEP',
|
|
||||||
step: 8,
|
|
||||||
source: 'background',
|
|
||||||
payload: {},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// content/duck-mail.js — Content script for DuckDuckGo Email Protection autofill settings
|
||||||
|
|
||||||
|
console.log('[MultiPage:duck-mail] Content script loaded on', location.href);
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
if (message.type !== 'FETCH_DUCK_EMAIL') return;
|
||||||
|
|
||||||
|
resetStopState();
|
||||||
|
fetchDuckEmail(message.payload).then(result => {
|
||||||
|
sendResponse(result);
|
||||||
|
}).catch(err => {
|
||||||
|
if (isStopError(err)) {
|
||||||
|
log('Duck Mail: Stopped by user.', 'warn');
|
||||||
|
sendResponse({ stopped: true, error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendResponse({ error: err.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchDuckEmail(payload = {}) {
|
||||||
|
const { generateNew = true } = payload;
|
||||||
|
|
||||||
|
log(`Duck Mail: ${generateNew ? 'Generating' : 'Reading'} private address...`);
|
||||||
|
|
||||||
|
await waitForElement(
|
||||||
|
'input.AutofillSettingsPanel__PrivateDuckAddressValue, button.AutofillSettingsPanel__GeneratorButton',
|
||||||
|
15000
|
||||||
|
);
|
||||||
|
|
||||||
|
const getAddressInput = () => document.querySelector('input.AutofillSettingsPanel__PrivateDuckAddressValue');
|
||||||
|
const getGeneratorButton = () => document.querySelector('button.AutofillSettingsPanel__GeneratorButton')
|
||||||
|
|| Array.from(document.querySelectorAll('button')).find(btn => /generate private duck address/i.test(btn.textContent || ''));
|
||||||
|
const readEmail = () => {
|
||||||
|
const value = getAddressInput()?.value?.trim() || '';
|
||||||
|
return value.includes('@duck.com') ? value : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const waitForEmailValue = async (previousValue = '') => {
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const nextValue = readEmail();
|
||||||
|
if (nextValue && nextValue !== previousValue) {
|
||||||
|
return nextValue;
|
||||||
|
}
|
||||||
|
await sleep(150);
|
||||||
|
}
|
||||||
|
throw new Error('Timed out waiting for Duck address to appear.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentEmail = readEmail();
|
||||||
|
if (currentEmail && !generateNew) {
|
||||||
|
log(`Duck Mail: Found existing address ${currentEmail}`);
|
||||||
|
return { email: currentEmail, generated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await humanPause(500, 1300);
|
||||||
|
const generatorButton = getGeneratorButton();
|
||||||
|
if (!generatorButton) {
|
||||||
|
if (currentEmail) {
|
||||||
|
log(`Duck Mail: Reusing existing address ${currentEmail}`, 'warn');
|
||||||
|
return { email: currentEmail, generated: false };
|
||||||
|
}
|
||||||
|
throw new Error('Could not find "Generate Private Duck Address" button.');
|
||||||
|
}
|
||||||
|
|
||||||
|
generatorButton.click();
|
||||||
|
log('Duck Mail: Clicked "Generate Private Duck Address"');
|
||||||
|
|
||||||
|
const nextEmail = await waitForEmailValue(currentEmail);
|
||||||
|
log(`Duck Mail: Ready address ${nextEmail}`, 'ok');
|
||||||
|
return { email: nextEmail, generated: true };
|
||||||
|
}
|
||||||
@@ -41,9 +41,15 @@ async function persistSeenCodes() {
|
|||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (message.type === 'POLL_EMAIL') {
|
if (message.type === 'POLL_EMAIL') {
|
||||||
|
resetStopState();
|
||||||
handlePollEmail(message.step, message.payload).then(result => {
|
handlePollEmail(message.step, message.payload).then(result => {
|
||||||
sendResponse(result);
|
sendResponse(result);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
if (isStopError(err)) {
|
||||||
|
log(`Step ${message.step}: Stopped by user.`, 'warn');
|
||||||
|
sendResponse({ stopped: true, error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
reportError(message.step, err.message);
|
reportError(message.step, err.message);
|
||||||
sendResponse({ error: err.message });
|
sendResponse({ error: err.message });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,9 +22,15 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
sendResponse({ ok: false, reason: 'wrong-frame' });
|
sendResponse({ ok: false, reason: 'wrong-frame' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
resetStopState();
|
||||||
handlePollEmail(message.step, message.payload).then(result => {
|
handlePollEmail(message.step, message.payload).then(result => {
|
||||||
sendResponse(result);
|
sendResponse(result);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
if (isStopError(err)) {
|
||||||
|
log(`Step ${message.step}: Stopped by user.`, 'warn');
|
||||||
|
sendResponse({ stopped: true, error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
reportError(message.step, err.message);
|
reportError(message.step, err.message);
|
||||||
sendResponse({ error: err.message });
|
sendResponse({ error: err.message });
|
||||||
});
|
});
|
||||||
|
|||||||
+96
-75
@@ -6,9 +6,15 @@ console.log('[MultiPage:signup-page] Content script loaded on', location.href);
|
|||||||
// Listen for commands from Background
|
// Listen for commands from Background
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE') {
|
if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE') {
|
||||||
|
resetStopState();
|
||||||
handleCommand(message).then(() => {
|
handleCommand(message).then(() => {
|
||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
if (isStopError(err)) {
|
||||||
|
log(`Step ${message.step}: Stopped by user.`, 'warn');
|
||||||
|
sendResponse({ stopped: true, error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
reportError(message.step, err.message);
|
reportError(message.step, err.message);
|
||||||
sendResponse({ error: err.message });
|
sendResponse({ error: err.message });
|
||||||
});
|
});
|
||||||
@@ -59,6 +65,7 @@ async function step2_clickRegister() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await humanPause(450, 1200);
|
||||||
reportComplete(2);
|
reportComplete(2);
|
||||||
simulateClick(registerBtn);
|
simulateClick(registerBtn);
|
||||||
log('Step 2: Clicked Register button');
|
log('Step 2: Clicked Register button');
|
||||||
@@ -85,6 +92,7 @@ async function step3_fillEmailPassword(payload) {
|
|||||||
throw new Error('Could not find email input field on signup page. URL: ' + location.href);
|
throw new Error('Could not find email input field on signup page. URL: ' + location.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await humanPause(500, 1400);
|
||||||
fillInput(emailInput, email);
|
fillInput(emailInput, email);
|
||||||
log('Step 3: Email filled');
|
log('Step 3: Email filled');
|
||||||
|
|
||||||
@@ -98,6 +106,7 @@ async function step3_fillEmailPassword(payload) {
|
|||||||
|| await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
|
|| await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
|
||||||
|
|
||||||
if (submitBtn) {
|
if (submitBtn) {
|
||||||
|
await humanPause(400, 1100);
|
||||||
simulateClick(submitBtn);
|
simulateClick(submitBtn);
|
||||||
log('Step 3: Submitted email, waiting for password field...');
|
log('Step 3: Submitted email, waiting for password field...');
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
@@ -111,6 +120,7 @@ async function step3_fillEmailPassword(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
|
if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
|
||||||
|
await humanPause(600, 1500);
|
||||||
fillInput(passwordInput, payload.password);
|
fillInput(passwordInput, payload.password);
|
||||||
log('Step 3: Password filled');
|
log('Step 3: Password filled');
|
||||||
|
|
||||||
@@ -124,6 +134,7 @@ async function step3_fillEmailPassword(payload) {
|
|||||||
|| await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
|
|| await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null);
|
||||||
|
|
||||||
if (submitBtn) {
|
if (submitBtn) {
|
||||||
|
await humanPause(500, 1300);
|
||||||
simulateClick(submitBtn);
|
simulateClick(submitBtn);
|
||||||
log('Step 3: Form submitted');
|
log('Step 3: Form submitted');
|
||||||
}
|
}
|
||||||
@@ -174,6 +185,7 @@ async function fillVerificationCode(step, payload) {
|
|||||||
|| await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证/i, 5000).catch(() => null);
|
|| await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证/i, 5000).catch(() => null);
|
||||||
|
|
||||||
if (submitBtn) {
|
if (submitBtn) {
|
||||||
|
await humanPause(450, 1200);
|
||||||
simulateClick(submitBtn);
|
simulateClick(submitBtn);
|
||||||
log(`Step ${step}: Verification submitted`);
|
log(`Step ${step}: Verification submitted`);
|
||||||
}
|
}
|
||||||
@@ -200,6 +212,7 @@ async function step6_login(payload) {
|
|||||||
throw new Error('Could not find email input on login page. URL: ' + location.href);
|
throw new Error('Could not find email input on login page. URL: ' + location.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await humanPause(500, 1400);
|
||||||
fillInput(emailInput, email);
|
fillInput(emailInput, email);
|
||||||
log('Step 6: Email filled');
|
log('Step 6: Email filled');
|
||||||
|
|
||||||
@@ -208,6 +221,7 @@ async function step6_login(payload) {
|
|||||||
const submitBtn1 = document.querySelector('button[type="submit"]')
|
const submitBtn1 = document.querySelector('button[type="submit"]')
|
||||||
|| await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
|
|| await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
|
||||||
if (submitBtn1) {
|
if (submitBtn1) {
|
||||||
|
await humanPause(400, 1100);
|
||||||
simulateClick(submitBtn1);
|
simulateClick(submitBtn1);
|
||||||
log('Step 6: Submitted email');
|
log('Step 6: Submitted email');
|
||||||
}
|
}
|
||||||
@@ -218,6 +232,7 @@ async function step6_login(payload) {
|
|||||||
const passwordInput = document.querySelector('input[type="password"]');
|
const passwordInput = document.querySelector('input[type="password"]');
|
||||||
if (passwordInput) {
|
if (passwordInput) {
|
||||||
log('Step 6: Password field found, filling password...');
|
log('Step 6: Password field found, filling password...');
|
||||||
|
await humanPause(550, 1450);
|
||||||
fillInput(passwordInput, password);
|
fillInput(passwordInput, password);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
@@ -227,6 +242,7 @@ async function step6_login(payload) {
|
|||||||
reportComplete(6, { needsOTP: true });
|
reportComplete(6, { needsOTP: true });
|
||||||
|
|
||||||
if (submitBtn2) {
|
if (submitBtn2) {
|
||||||
|
await humanPause(450, 1200);
|
||||||
simulateClick(submitBtn2);
|
simulateClick(submitBtn2);
|
||||||
log('Step 6: Submitted password, may need verification code (step 7)');
|
log('Step 6: Submitted password, may need verification code (step 7)');
|
||||||
}
|
}
|
||||||
@@ -239,14 +255,14 @@ async function step6_login(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Step 8: Click "继续" on OAuth consent page
|
// Step 8: Focus "继续" on OAuth consent page for manual click
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// After login + verification, page shows:
|
// After login + verification, page shows:
|
||||||
// "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
|
// "使用 ChatGPT 登录到 Codex" with a "继续" submit button.
|
||||||
// Clicking it triggers redirect to localhost URL.
|
// We only locate and focus it so the user can click manually.
|
||||||
|
|
||||||
async function step8_clickContinue() {
|
async function step8_clickContinue() {
|
||||||
log('Step 8: Looking for OAuth consent "继续" button...');
|
log('Step 8: Looking for OAuth consent "继续" button for manual click...');
|
||||||
|
|
||||||
// Wait for the consent page to be ready
|
// Wait for the consent page to be ready
|
||||||
// Look for the submit button with text "继续" or data-dd-action-name="Continue"
|
// Look for the submit button with text "继续" or data-dd-action-name="Continue"
|
||||||
@@ -264,40 +280,33 @@ async function step8_clickContinue() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log('Step 8: Found "继续" button, clicking...');
|
await humanPause(350, 900);
|
||||||
|
continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
// Use native .click() — simulateClick (dispatchEvent) may not trigger form submit
|
continueBtn.focus();
|
||||||
continueBtn.click();
|
log('Step 8: Found "继续" button and focused it. Please click it manually.');
|
||||||
log('Step 8: Clicked via .click()');
|
|
||||||
|
|
||||||
// Also try submitting the form directly as a fallback
|
|
||||||
await sleep(500);
|
|
||||||
const form = continueBtn.closest('form');
|
|
||||||
if (form) {
|
|
||||||
form.requestSubmit(continueBtn);
|
|
||||||
log('Step 8: Also triggered form.requestSubmit()');
|
|
||||||
}
|
|
||||||
|
|
||||||
log('Step 8: Redirecting to localhost... (background will capture URL)');
|
|
||||||
|
|
||||||
// Don't reportComplete — background handles it via webNavigation listener
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Step 5: Fill Name & Birthday
|
// Step 5: Fill Name & Birthday / Age
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function step5_fillNameBirthday(payload) {
|
async function step5_fillNameBirthday(payload) {
|
||||||
const { firstName, lastName, year, month, day } = payload;
|
const { firstName, lastName, age, year, month, day } = payload;
|
||||||
if (!firstName || !lastName) throw new Error('No name data provided.');
|
if (!firstName || !lastName) throw new Error('No name data provided.');
|
||||||
|
|
||||||
|
const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null);
|
||||||
|
const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value)));
|
||||||
|
if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) {
|
||||||
|
throw new Error('No birthday or age data provided.');
|
||||||
|
}
|
||||||
|
|
||||||
const fullName = `${firstName} ${lastName}`;
|
const fullName = `${firstName} ${lastName}`;
|
||||||
log(`Step 5: Filling name: ${fullName}, Birthday: ${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}`);
|
log(`Step 5: Filling name: ${fullName}`);
|
||||||
|
|
||||||
// Actual DOM structure:
|
// Actual DOM structure:
|
||||||
// - Full name: <input name="name" placeholder="全名" type="text">
|
// - Full name: <input name="name" placeholder="全名" type="text">
|
||||||
// - Birthday: React Aria DateField with 3 spinbutton divs (year/month/day)
|
// - Birthday: React Aria DateField or hidden input[name="birthday"]
|
||||||
// + <input type="hidden" name="birthday" value="2026-04-05">
|
// - Age: <input name="age" type="text|number">
|
||||||
|
|
||||||
// --- Full Name (single field, not first+last) ---
|
// --- Full Name (single field, not first+last) ---
|
||||||
let nameInput = null;
|
let nameInput = null;
|
||||||
@@ -309,74 +318,85 @@ async function step5_fillNameBirthday(payload) {
|
|||||||
} catch {
|
} catch {
|
||||||
throw new Error('Could not find name input. URL: ' + location.href);
|
throw new Error('Could not find name input. URL: ' + location.href);
|
||||||
}
|
}
|
||||||
|
await humanPause(500, 1300);
|
||||||
fillInput(nameInput, fullName);
|
fillInput(nameInput, fullName);
|
||||||
log(`Step 5: Name filled: ${fullName}`);
|
log(`Step 5: Name filled: ${fullName}`);
|
||||||
|
|
||||||
// --- Birthday (React Aria DateField with spinbutton segments) ---
|
let birthdayMode = false;
|
||||||
// The date field has three contenteditable divs with role="spinbutton"
|
let ageInput = null;
|
||||||
// and data-type="year", data-type="month", data-type="day"
|
|
||||||
// There's also a hidden input[name="birthday"] that stores the actual value
|
|
||||||
|
|
||||||
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
|
for (let i = 0; i < 100; i++) {
|
||||||
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
|
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
|
||||||
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
|
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
|
||||||
|
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
|
||||||
|
const hiddenBirthday = document.querySelector('input[name="birthday"]');
|
||||||
|
ageInput = document.querySelector('input[name="age"]');
|
||||||
|
|
||||||
if (yearSpinner && monthSpinner && daySpinner) {
|
if ((yearSpinner && monthSpinner && daySpinner) || hiddenBirthday) {
|
||||||
log('Step 5: Found React Aria DateField spinbuttons');
|
birthdayMode = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ageInput) break;
|
||||||
|
await sleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to set a spinbutton value via focus + keyboard input
|
if (birthdayMode) {
|
||||||
async function setSpinButton(el, value) {
|
if (!hasBirthdayData) {
|
||||||
el.focus();
|
throw new Error('Birthday field detected, but no birthday data provided.');
|
||||||
await sleep(100);
|
|
||||||
|
|
||||||
// Select all existing text
|
|
||||||
document.execCommand('selectAll', false, null);
|
|
||||||
await sleep(50);
|
|
||||||
|
|
||||||
// Type the new value digit by digit
|
|
||||||
const valueStr = String(value);
|
|
||||||
for (const char of valueStr) {
|
|
||||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true }));
|
|
||||||
el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true }));
|
|
||||||
// Also use InputEvent for React Aria
|
|
||||||
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true }));
|
|
||||||
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true }));
|
|
||||||
await sleep(50);
|
|
||||||
}
|
|
||||||
|
|
||||||
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true }));
|
|
||||||
el.blur();
|
|
||||||
await sleep(100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await setSpinButton(yearSpinner, year);
|
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
|
||||||
log(`Step 5: Year set: ${year}`);
|
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
|
||||||
|
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
|
||||||
|
|
||||||
await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
|
if (yearSpinner && monthSpinner && daySpinner) {
|
||||||
log(`Step 5: Month set: ${month}`);
|
log('Step 5: Birthday fields detected, filling birthday...');
|
||||||
|
|
||||||
await setSpinButton(daySpinner, String(day).padStart(2, '0'));
|
async function setSpinButton(el, value) {
|
||||||
log(`Step 5: Day set: ${day}`);
|
el.focus();
|
||||||
|
await sleep(100);
|
||||||
|
document.execCommand('selectAll', false, null);
|
||||||
|
await sleep(50);
|
||||||
|
|
||||||
// Also update the hidden input directly as a safety measure
|
const valueStr = String(value);
|
||||||
const hiddenBirthday = document.querySelector('input[type="hidden"][name="birthday"]');
|
for (const char of valueStr) {
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true }));
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: `Digit${char}`, bubbles: true }));
|
||||||
|
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: char, bubbles: true }));
|
||||||
|
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true }));
|
||||||
|
await sleep(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Tab', code: 'Tab', bubbles: true }));
|
||||||
|
el.blur();
|
||||||
|
await sleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
await humanPause(450, 1100);
|
||||||
|
await setSpinButton(yearSpinner, year);
|
||||||
|
await humanPause(250, 650);
|
||||||
|
await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
|
||||||
|
await humanPause(250, 650);
|
||||||
|
await setSpinButton(daySpinner, String(day).padStart(2, '0'));
|
||||||
|
log(`Step 5: Birthday filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenBirthday = document.querySelector('input[name="birthday"]');
|
||||||
if (hiddenBirthday) {
|
if (hiddenBirthday) {
|
||||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
hiddenBirthday.value = dateStr;
|
hiddenBirthday.value = dateStr;
|
||||||
hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
|
hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
log(`Step 5: Hidden birthday input set: ${dateStr}`);
|
log(`Step 5: Hidden birthday input set: ${dateStr}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (ageInput) {
|
||||||
// Fallback: try setting hidden input directly
|
if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) {
|
||||||
const hiddenBirthday = document.querySelector('input[name="birthday"]');
|
throw new Error('Age field detected, but no age data provided.');
|
||||||
if (hiddenBirthday) {
|
|
||||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
||||||
hiddenBirthday.value = dateStr;
|
|
||||||
hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
log(`Step 5: Birthday set via hidden input: ${dateStr}`);
|
|
||||||
} else {
|
|
||||||
log('Step 5: WARNING - Could not find birthday fields. May need to adjust selectors.', 'warn');
|
|
||||||
}
|
}
|
||||||
|
await humanPause(500, 1300);
|
||||||
|
fillInput(ageInput, String(resolvedAge));
|
||||||
|
log(`Step 5: Age filled: ${resolvedAge}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('Could not find birthday or age input. URL: ' + location.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click "完成帐户创建" button
|
// Click "完成帐户创建" button
|
||||||
@@ -388,6 +408,7 @@ async function step5_fillNameBirthday(payload) {
|
|||||||
reportComplete(5);
|
reportComplete(5);
|
||||||
|
|
||||||
if (completeBtn) {
|
if (completeBtn) {
|
||||||
|
await humanPause(500, 1300);
|
||||||
simulateClick(completeBtn);
|
simulateClick(completeBtn);
|
||||||
log('Step 5: Clicked "完成帐户创建"');
|
log('Step 5: Clicked "完成帐户创建"');
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-7
@@ -5,12 +5,37 @@ const SCRIPT_SOURCE = (() => {
|
|||||||
if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
|
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('mail.qq.com')) return 'qq-mail';
|
||||||
if (url.includes('mail.163.com')) return 'mail-163';
|
if (url.includes('mail.163.com')) return 'mail-163';
|
||||||
|
if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
|
||||||
if (url.includes('chatgpt.com')) return 'chatgpt';
|
if (url.includes('chatgpt.com')) return 'chatgpt';
|
||||||
// VPS panel — detected dynamically since URL is configurable
|
// VPS panel — detected dynamically since URL is configurable
|
||||||
return 'vps-panel';
|
return 'vps-panel';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
|
const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
|
||||||
|
const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
|
||||||
|
let flowStopped = false;
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((message) => {
|
||||||
|
if (message.type === 'STOP_FLOW') {
|
||||||
|
flowStopped = true;
|
||||||
|
console.warn(LOG_PREFIX, STOP_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetStopState() {
|
||||||
|
flowStopped = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStopError(error) {
|
||||||
|
const message = typeof error === 'string' ? error : error?.message;
|
||||||
|
return message === STOP_ERROR_MESSAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwIfStopped() {
|
||||||
|
if (flowStopped) {
|
||||||
|
throw new Error(STOP_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for a DOM element to appear.
|
* Wait for a DOM element to appear.
|
||||||
@@ -20,6 +45,8 @@ const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
|
|||||||
*/
|
*/
|
||||||
function waitForElement(selector, timeout = 10000) {
|
function waitForElement(selector, timeout = 10000) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
throwIfStopped();
|
||||||
|
|
||||||
const existing = document.querySelector(selector);
|
const existing = document.querySelector(selector);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
console.log(LOG_PREFIX, `Found immediately: ${selector}`);
|
console.log(LOG_PREFIX, `Found immediately: ${selector}`);
|
||||||
@@ -31,11 +58,25 @@ function waitForElement(selector, timeout = 10000) {
|
|||||||
console.log(LOG_PREFIX, `Waiting for: ${selector} (timeout: ${timeout}ms)`);
|
console.log(LOG_PREFIX, `Waiting for: ${selector} (timeout: ${timeout}ms)`);
|
||||||
log(`Waiting for selector: ${selector}...`);
|
log(`Waiting for selector: ${selector}...`);
|
||||||
|
|
||||||
|
let settled = false;
|
||||||
|
let stopTimer = null;
|
||||||
|
const cleanup = () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
observer.disconnect();
|
||||||
|
clearTimeout(timer);
|
||||||
|
clearTimeout(stopTimer);
|
||||||
|
};
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
|
if (flowStopped) {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error(STOP_ERROR_MESSAGE));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const el = document.querySelector(selector);
|
const el = document.querySelector(selector);
|
||||||
if (el) {
|
if (el) {
|
||||||
observer.disconnect();
|
cleanup();
|
||||||
clearTimeout(timer);
|
|
||||||
console.log(LOG_PREFIX, `Found after wait: ${selector}`);
|
console.log(LOG_PREFIX, `Found after wait: ${selector}`);
|
||||||
log(`Found element: ${selector}`);
|
log(`Found element: ${selector}`);
|
||||||
resolve(el);
|
resolve(el);
|
||||||
@@ -48,11 +89,22 @@ function waitForElement(selector, timeout = 10000) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
observer.disconnect();
|
cleanup();
|
||||||
const msg = `Timeout waiting for ${selector} after ${timeout}ms on ${location.href}`;
|
const msg = `Timeout waiting for ${selector} after ${timeout}ms on ${location.href}`;
|
||||||
console.error(LOG_PREFIX, msg);
|
console.error(LOG_PREFIX, msg);
|
||||||
reject(new Error(msg));
|
reject(new Error(msg));
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
|
const pollStop = () => {
|
||||||
|
if (settled) return;
|
||||||
|
if (flowStopped) {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error(STOP_ERROR_MESSAGE));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopTimer = setTimeout(pollStop, 100);
|
||||||
|
};
|
||||||
|
pollStop();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +117,8 @@ function waitForElement(selector, timeout = 10000) {
|
|||||||
*/
|
*/
|
||||||
function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
|
function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
throwIfStopped();
|
||||||
|
|
||||||
function search() {
|
function search() {
|
||||||
const candidates = document.querySelectorAll(containerSelector);
|
const candidates = document.querySelectorAll(containerSelector);
|
||||||
for (const el of candidates) {
|
for (const el of candidates) {
|
||||||
@@ -86,11 +140,25 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
|
|||||||
console.log(LOG_PREFIX, `Waiting for text match: ${containerSelector} / ${textPattern}`);
|
console.log(LOG_PREFIX, `Waiting for text match: ${containerSelector} / ${textPattern}`);
|
||||||
log(`Waiting for element with text: ${textPattern}...`);
|
log(`Waiting for element with text: ${textPattern}...`);
|
||||||
|
|
||||||
|
let settled = false;
|
||||||
|
let stopTimer = null;
|
||||||
|
const cleanup = () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
observer.disconnect();
|
||||||
|
clearTimeout(timer);
|
||||||
|
clearTimeout(stopTimer);
|
||||||
|
};
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
|
if (flowStopped) {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error(STOP_ERROR_MESSAGE));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const el = search();
|
const el = search();
|
||||||
if (el) {
|
if (el) {
|
||||||
observer.disconnect();
|
cleanup();
|
||||||
clearTimeout(timer);
|
|
||||||
console.log(LOG_PREFIX, `Found by text after wait: ${textPattern}`);
|
console.log(LOG_PREFIX, `Found by text after wait: ${textPattern}`);
|
||||||
log(`Found element by text: ${textPattern}`);
|
log(`Found element by text: ${textPattern}`);
|
||||||
resolve(el);
|
resolve(el);
|
||||||
@@ -103,11 +171,22 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
observer.disconnect();
|
cleanup();
|
||||||
const msg = `Timeout waiting for text "${textPattern}" in "${containerSelector}" after ${timeout}ms on ${location.href}`;
|
const msg = `Timeout waiting for text "${textPattern}" in "${containerSelector}" after ${timeout}ms on ${location.href}`;
|
||||||
console.error(LOG_PREFIX, msg);
|
console.error(LOG_PREFIX, msg);
|
||||||
reject(new Error(msg));
|
reject(new Error(msg));
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
|
const pollStop = () => {
|
||||||
|
if (settled) return;
|
||||||
|
if (flowStopped) {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error(STOP_ERROR_MESSAGE));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopTimer = setTimeout(pollStop, 100);
|
||||||
|
};
|
||||||
|
pollStop();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +197,7 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
|
|||||||
* @param {string} value
|
* @param {string} value
|
||||||
*/
|
*/
|
||||||
function fillInput(el, value) {
|
function fillInput(el, value) {
|
||||||
|
throwIfStopped();
|
||||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||||
window.HTMLInputElement.prototype,
|
window.HTMLInputElement.prototype,
|
||||||
'value'
|
'value'
|
||||||
@@ -135,6 +215,7 @@ function fillInput(el, value) {
|
|||||||
* @param {string} value
|
* @param {string} value
|
||||||
*/
|
*/
|
||||||
function fillSelect(el, value) {
|
function fillSelect(el, value) {
|
||||||
|
throwIfStopped();
|
||||||
el.value = value;
|
el.value = value;
|
||||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
console.log(LOG_PREFIX, `Selected value ${value} in ${el.name || el.id}`);
|
console.log(LOG_PREFIX, `Selected value ${value} in ${el.name || el.id}`);
|
||||||
@@ -209,6 +290,7 @@ function reportError(step, errorMessage) {
|
|||||||
* @param {Element} el
|
* @param {Element} el
|
||||||
*/
|
*/
|
||||||
function simulateClick(el) {
|
function simulateClick(el) {
|
||||||
|
throwIfStopped();
|
||||||
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
||||||
console.log(LOG_PREFIX, `Clicked: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
|
console.log(LOG_PREFIX, `Clicked: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
|
||||||
log(`Clicked [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`);
|
log(`Clicked [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`);
|
||||||
@@ -220,7 +302,28 @@ function simulateClick(el) {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise(r => setTimeout(r, ms));
|
return new Promise((resolve, reject) => {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
if (flowStopped) {
|
||||||
|
reject(new Error(STOP_ERROR_MESSAGE));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Date.now() - start >= ms) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(tick, Math.min(100, Math.max(25, ms - (Date.now() - start))));
|
||||||
|
}
|
||||||
|
|
||||||
|
tick();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function humanPause(min = 250, max = 850) {
|
||||||
|
const duration = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
await sleep(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-report ready on load
|
// Auto-report ready on load
|
||||||
|
|||||||
@@ -28,9 +28,15 @@ console.log('[MultiPage:vps-panel] Content script loaded on', location.href);
|
|||||||
// Listen for commands from Background
|
// Listen for commands from Background
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (message.type === 'EXECUTE_STEP') {
|
if (message.type === 'EXECUTE_STEP') {
|
||||||
|
resetStopState();
|
||||||
handleStep(message.step, message.payload).then(() => {
|
handleStep(message.step, message.payload).then(() => {
|
||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
if (isStopError(err)) {
|
||||||
|
log(`Step ${message.step}: Stopped by user.`, 'warn');
|
||||||
|
sendResponse({ stopped: true, error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
reportError(message.step, err.message);
|
reportError(message.step, err.message);
|
||||||
sendResponse({ error: err.message });
|
sendResponse({ error: err.message });
|
||||||
});
|
});
|
||||||
@@ -77,6 +83,7 @@ async function step1_getOAuthLink() {
|
|||||||
if (loginBtn.disabled) {
|
if (loginBtn.disabled) {
|
||||||
log('Step 1: Login button is disabled (already loading), waiting for auth URL...');
|
log('Step 1: Login button is disabled (already loading), waiting for auth URL...');
|
||||||
} else {
|
} else {
|
||||||
|
await humanPause(500, 1400);
|
||||||
simulateClick(loginBtn);
|
simulateClick(loginBtn);
|
||||||
log('Step 1: Clicked login button, waiting for auth URL...');
|
log('Step 1: Clicked login button, waiting for auth URL...');
|
||||||
}
|
}
|
||||||
@@ -133,6 +140,7 @@ async function step9_vpsVerify(payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await humanPause(600, 1500);
|
||||||
fillInput(urlInput, localhostUrl);
|
fillInput(urlInput, localhostUrl);
|
||||||
log(`Step 9: Filled callback URL: ${localhostUrl.slice(0, 80)}...`);
|
log(`Step 9: Filled callback URL: ${localhostUrl.slice(0, 80)}...`);
|
||||||
|
|
||||||
@@ -152,6 +160,7 @@ async function step9_vpsVerify(payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await humanPause(450, 1200);
|
||||||
simulateClick(submitBtn);
|
simulateClick(submitBtn);
|
||||||
log('Step 9: Clicked "提交回调 URL", waiting for authentication result...');
|
log('Step 9: Clicked "提交回调 URL", waiting for authentication result...');
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,13 @@
|
|||||||
"js": ["content/utils.js", "content/mail-163.js"],
|
"js": ["content/utils.js", "content/mail-163.js"],
|
||||||
"all_frames": true,
|
"all_frames": true,
|
||||||
"run_at": "document_idle"
|
"run_at": "document_idle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"https://duckduckgo.com/email/settings/autofill*"
|
||||||
|
],
|
||||||
|
"js": ["content/utils.js", "content/duck-mail.js"],
|
||||||
|
"run_at": "document_idle"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"action": {
|
"action": {
|
||||||
|
|||||||
+27
-1
@@ -173,6 +173,12 @@ header {
|
|||||||
}
|
}
|
||||||
.btn-success:hover { opacity: 0.9; box-shadow: 0 2px 8px var(--green-soft); }
|
.btn-success:hover { opacity: 0.9; box-shadow: 0 2px 8px var(--green-soft); }
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--red);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-danger:hover { opacity: 0.9; box-shadow: 0 2px 8px var(--red-soft); }
|
||||||
|
|
||||||
.run-group {
|
.run-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -194,7 +200,7 @@ header {
|
|||||||
}
|
}
|
||||||
.run-count-input:focus { border-color: var(--blue); }
|
.run-count-input:focus { border-color: var(--blue); }
|
||||||
.run-count-input::-webkit-inner-spin-button { opacity: 0.5; }
|
.run-count-input::-webkit-inner-spin-button { opacity: 0.5; }
|
||||||
.btn-success:disabled, .btn-primary:disabled { background: var(--bg-elevated); color: var(--text-muted); cursor: not-allowed; box-shadow: none; }
|
.btn-success:disabled, .btn-primary:disabled, .btn-danger:disabled { background: var(--bg-elevated); color: var(--text-muted); cursor: not-allowed; box-shadow: none; }
|
||||||
.run-count-input:disabled { opacity: 0.5; cursor: not-allowed; }
|
.run-count-input:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
@@ -238,6 +244,14 @@ header {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.data-label {
|
.data-label {
|
||||||
width: 56px;
|
width: 56px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -283,6 +297,10 @@ header {
|
|||||||
.data-input::placeholder { color: var(--text-muted); }
|
.data-input::placeholder { color: var(--text-muted); }
|
||||||
.data-input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
|
.data-input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
|
||||||
|
|
||||||
|
#btn-fetch-email {
|
||||||
|
padding-inline: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.data-select {
|
.data-select {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
@@ -337,6 +355,9 @@ header {
|
|||||||
.status-bar.failed .status-dot { background: var(--red); }
|
.status-bar.failed .status-dot { background: var(--red); }
|
||||||
.status-bar.failed { color: var(--red); }
|
.status-bar.failed { color: var(--red); }
|
||||||
|
|
||||||
|
.status-bar.stopped .status-dot { background: var(--cyan); }
|
||||||
|
.status-bar.stopped { color: var(--cyan); }
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
50% { opacity: 0.4; transform: scale(0.85); }
|
50% { opacity: 0.4; transform: scale(0.85); }
|
||||||
@@ -433,6 +454,9 @@ header {
|
|||||||
.step-row.failed .step-indicator { border-color: var(--red); background: var(--red-soft); }
|
.step-row.failed .step-indicator { border-color: var(--red); background: var(--red-soft); }
|
||||||
.step-row.failed .step-num { color: var(--red); }
|
.step-row.failed .step-num { color: var(--red); }
|
||||||
|
|
||||||
|
.step-row.stopped .step-indicator { border-color: var(--cyan); background: rgba(8, 145, 178, 0.08); }
|
||||||
|
.step-row.stopped .step-num { color: var(--cyan); }
|
||||||
|
|
||||||
/* Step Button */
|
/* Step Button */
|
||||||
.step-btn {
|
.step-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -455,6 +479,7 @@ header {
|
|||||||
.step-row.running .step-btn { border-color: var(--orange); color: var(--orange); }
|
.step-row.running .step-btn { border-color: var(--orange); color: var(--orange); }
|
||||||
.step-row.completed .step-btn { border-color: var(--border-subtle); color: var(--text-secondary); opacity: 0.7; }
|
.step-row.completed .step-btn { border-color: var(--border-subtle); color: var(--text-secondary); opacity: 0.7; }
|
||||||
.step-row.failed .step-btn { border-color: var(--red); color: var(--red); }
|
.step-row.failed .step-btn { border-color: var(--red); color: var(--red); }
|
||||||
|
.step-row.stopped .step-btn { border-color: var(--cyan); color: var(--cyan); }
|
||||||
|
|
||||||
.step-status {
|
.step-status {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
@@ -465,6 +490,7 @@ header {
|
|||||||
}
|
}
|
||||||
.step-row.completed .step-status { color: var(--green); }
|
.step-row.completed .step-status { color: var(--green); }
|
||||||
.step-row.failed .step-status { color: var(--red); }
|
.step-row.failed .step-status { color: var(--red); }
|
||||||
|
.step-row.stopped .step-status { color: var(--cyan); }
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Log / Console Section
|
Log / Console Section
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||||
Auto
|
Auto
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btn-stop" class="btn btn-danger" title="Stop current flow" disabled>Stop</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-reset" class="btn btn-ghost" title="Reset all steps">
|
<button id="btn-reset" class="btn btn-ghost" title="Reset all steps">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -56,7 +57,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">Email</span>
|
<span class="data-label">Email</span>
|
||||||
<input type="text" id="input-email" class="data-input" placeholder="Paste DuckDuckGo email" />
|
<div class="data-inline">
|
||||||
|
<input type="text" id="input-email" class="data-input" placeholder="Paste DuckDuckGo email" />
|
||||||
|
<button id="btn-fetch-email" class="btn btn-outline btn-sm" type="button">Auto</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">OAuth</span>
|
<span class="data-label">OAuth</span>
|
||||||
@@ -73,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="auto-continue-bar" class="auto-continue-bar" style="display:none;">
|
<div id="auto-continue-bar" class="auto-continue-bar" style="display:none;">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||||
<span class="auto-hint">Paste email above, then continue</span>
|
<span class="auto-hint">Use Auto to fetch Duck email, or paste manually, then continue</span>
|
||||||
<button id="btn-auto-continue" class="btn btn-primary btn-sm">Continue</button>
|
<button id="btn-auto-continue" class="btn btn-primary btn-sm">Continue</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -121,7 +125,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="step-row" data-step="8">
|
<div class="step-row" data-step="8">
|
||||||
<div class="step-indicator" data-step="8"><span class="step-num">8</span></div>
|
<div class="step-indicator" data-step="8"><span class="step-num">8</span></div>
|
||||||
<button class="step-btn" data-step="8">Complete OAuth</button>
|
<button class="step-btn" data-step="8">Manual OAuth Confirm</button>
|
||||||
<span class="step-status" data-step="8"></span>
|
<span class="step-status" data-step="8"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="step-row" data-step="9">
|
<div class="step-row" data-step="9">
|
||||||
|
|||||||
+71
-3
@@ -5,6 +5,7 @@ const STATUS_ICONS = {
|
|||||||
running: '',
|
running: '',
|
||||||
completed: '\u2713', // ✓
|
completed: '\u2713', // ✓
|
||||||
failed: '\u2717', // ✗
|
failed: '\u2717', // ✗
|
||||||
|
stopped: '\u25A0', // ■
|
||||||
};
|
};
|
||||||
|
|
||||||
const logArea = document.getElementById('log-area');
|
const logArea = document.getElementById('log-area');
|
||||||
@@ -13,6 +14,8 @@ const displayLocalhostUrl = document.getElementById('display-localhost-url');
|
|||||||
const displayStatus = document.getElementById('display-status');
|
const displayStatus = document.getElementById('display-status');
|
||||||
const statusBar = document.getElementById('status-bar');
|
const statusBar = document.getElementById('status-bar');
|
||||||
const inputEmail = document.getElementById('input-email');
|
const inputEmail = document.getElementById('input-email');
|
||||||
|
const btnFetchEmail = document.getElementById('btn-fetch-email');
|
||||||
|
const btnStop = document.getElementById('btn-stop');
|
||||||
const btnReset = document.getElementById('btn-reset');
|
const btnReset = document.getElementById('btn-reset');
|
||||||
const stepsProgress = document.getElementById('steps-progress');
|
const stepsProgress = document.getElementById('steps-progress');
|
||||||
const btnAutoRun = document.getElementById('btn-auto-run');
|
const btnAutoRun = document.getElementById('btn-auto-run');
|
||||||
@@ -132,6 +135,7 @@ function updateButtonStates() {
|
|||||||
if (row.classList.contains('completed')) statuses[step] = 'completed';
|
if (row.classList.contains('completed')) statuses[step] = 'completed';
|
||||||
else if (row.classList.contains('running')) statuses[step] = 'running';
|
else if (row.classList.contains('running')) statuses[step] = 'running';
|
||||||
else if (row.classList.contains('failed')) statuses[step] = 'failed';
|
else if (row.classList.contains('failed')) statuses[step] = 'failed';
|
||||||
|
else if (row.classList.contains('stopped')) statuses[step] = 'stopped';
|
||||||
else statuses[step] = 'pending';
|
else statuses[step] = 'pending';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,9 +152,15 @@ function updateButtonStates() {
|
|||||||
} else {
|
} else {
|
||||||
const prevStatus = statuses[step - 1];
|
const prevStatus = statuses[step - 1];
|
||||||
const currentStatus = statuses[step];
|
const currentStatus = statuses[step];
|
||||||
btn.disabled = !(prevStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'completed');
|
btn.disabled = !(prevStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'completed' || currentStatus === 'stopped');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateStopButtonState(anyRunning || autoContinueBar.style.display !== 'none');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStopButtonState(active) {
|
||||||
|
btnStop.disabled = !active;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatusDisplay(state) {
|
function updateStatusDisplay(state) {
|
||||||
@@ -172,6 +182,13 @@ function updateStatusDisplay(state) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stopped = Object.entries(state.stepStatuses).find(([, s]) => s === 'stopped');
|
||||||
|
if (stopped) {
|
||||||
|
displayStatus.textContent = `Step ${stopped[0]} stopped`;
|
||||||
|
statusBar.classList.add('stopped');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const lastCompleted = Object.entries(state.stepStatuses)
|
const lastCompleted = Object.entries(state.stepStatuses)
|
||||||
.filter(([, s]) => s === 'completed')
|
.filter(([, s]) => s === 'completed')
|
||||||
.map(([k]) => Number(k))
|
.map(([k]) => Number(k))
|
||||||
@@ -214,6 +231,37 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchDuckEmail() {
|
||||||
|
const defaultLabel = 'Auto';
|
||||||
|
btnFetchEmail.disabled = true;
|
||||||
|
btnFetchEmail.textContent = '...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'FETCH_DUCK_EMAIL',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { generateNew: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
if (!response?.email) {
|
||||||
|
throw new Error('Duck email was not returned.');
|
||||||
|
}
|
||||||
|
|
||||||
|
inputEmail.value = response.email;
|
||||||
|
showToast(`Fetched ${response.email}`, 'success', 2500);
|
||||||
|
return response.email;
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`Auto fetch failed: ${err.message}`, 'error');
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
btnFetchEmail.disabled = false;
|
||||||
|
btnFetchEmail.textContent = defaultLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Button Handlers
|
// Button Handlers
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -224,7 +272,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
|
|||||||
if (step === 3) {
|
if (step === 3) {
|
||||||
const email = inputEmail.value.trim();
|
const email = inputEmail.value.trim();
|
||||||
if (!email) {
|
if (!email) {
|
||||||
showToast('Please paste email address first', 'warn');
|
showToast('Please paste email address or use Auto first', 'warn');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
|
await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
|
||||||
@@ -234,6 +282,16 @@ document.querySelectorAll('.step-btn').forEach(btn => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
btnFetchEmail.addEventListener('click', async () => {
|
||||||
|
await fetchDuckEmail().catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
btnStop.addEventListener('click', async () => {
|
||||||
|
btnStop.disabled = true;
|
||||||
|
await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} });
|
||||||
|
showToast('Stopping current flow...', 'warn', 2000);
|
||||||
|
});
|
||||||
|
|
||||||
// Auto Run
|
// Auto Run
|
||||||
btnAutoRun.addEventListener('click', async () => {
|
btnAutoRun.addEventListener('click', async () => {
|
||||||
const totalRuns = parseInt(inputRunCount.value) || 1;
|
const totalRuns = parseInt(inputRunCount.value) || 1;
|
||||||
@@ -246,7 +304,7 @@ btnAutoRun.addEventListener('click', async () => {
|
|||||||
btnAutoContinue.addEventListener('click', async () => {
|
btnAutoContinue.addEventListener('click', async () => {
|
||||||
const email = inputEmail.value.trim();
|
const email = inputEmail.value.trim();
|
||||||
if (!email) {
|
if (!email) {
|
||||||
showToast('Please paste DuckDuckGo email first!', 'warn');
|
showToast('Please fetch or paste DuckDuckGo email first!', 'warn');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
autoContinueBar.style.display = 'none';
|
autoContinueBar.style.display = 'none';
|
||||||
@@ -268,8 +326,10 @@ btnReset.addEventListener('click', async () => {
|
|||||||
document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
|
document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
|
||||||
document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
|
document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
|
||||||
btnAutoRun.disabled = false;
|
btnAutoRun.disabled = false;
|
||||||
|
inputRunCount.disabled = false;
|
||||||
btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
|
btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
|
||||||
autoContinueBar.style.display = 'none';
|
autoContinueBar.style.display = 'none';
|
||||||
|
updateStopButtonState(false);
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
updateProgressCounter();
|
updateProgressCounter();
|
||||||
}
|
}
|
||||||
@@ -346,11 +406,15 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
logArea.innerHTML = '';
|
logArea.innerHTML = '';
|
||||||
document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
|
document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
|
||||||
document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
|
document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
|
||||||
|
updateStopButtonState(false);
|
||||||
updateProgressCounter();
|
updateProgressCounter();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'DATA_UPDATED': {
|
case 'DATA_UPDATED': {
|
||||||
|
if (message.payload.email) {
|
||||||
|
inputEmail.value = message.payload.email;
|
||||||
|
}
|
||||||
if (message.payload.oauthUrl) {
|
if (message.payload.oauthUrl) {
|
||||||
displayOauthUrl.textContent = message.payload.oauthUrl;
|
displayOauthUrl.textContent = message.payload.oauthUrl;
|
||||||
displayOauthUrl.classList.add('has-value');
|
displayOauthUrl.classList.add('has-value');
|
||||||
@@ -369,21 +433,25 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
case 'waiting_email':
|
case 'waiting_email':
|
||||||
autoContinueBar.style.display = 'flex';
|
autoContinueBar.style.display = 'flex';
|
||||||
btnAutoRun.innerHTML = `Paused${runLabel}`;
|
btnAutoRun.innerHTML = `Paused${runLabel}`;
|
||||||
|
updateStopButtonState(true);
|
||||||
break;
|
break;
|
||||||
case 'running':
|
case 'running':
|
||||||
btnAutoRun.innerHTML = `Running${runLabel}`;
|
btnAutoRun.innerHTML = `Running${runLabel}`;
|
||||||
|
updateStopButtonState(true);
|
||||||
break;
|
break;
|
||||||
case 'complete':
|
case 'complete':
|
||||||
btnAutoRun.disabled = false;
|
btnAutoRun.disabled = false;
|
||||||
inputRunCount.disabled = false;
|
inputRunCount.disabled = false;
|
||||||
btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
|
btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
|
||||||
autoContinueBar.style.display = 'none';
|
autoContinueBar.style.display = 'none';
|
||||||
|
updateStopButtonState(false);
|
||||||
break;
|
break;
|
||||||
case 'stopped':
|
case 'stopped':
|
||||||
btnAutoRun.disabled = false;
|
btnAutoRun.disabled = false;
|
||||||
inputRunCount.disabled = false;
|
inputRunCount.disabled = false;
|
||||||
btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
|
btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
|
||||||
autoContinueBar.style.display = 'none';
|
autoContinueBar.style.display = 'none';
|
||||||
|
updateStopButtonState(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user