翻译界面和日志信息为中文,更新 OAuth 流程中的步骤描述,添加管理密钥输入框和相关功能,增强用户体验,优化 CSS 样式,修复部分逻辑错误,确保自动化流程的稳定性和可用性。
This commit is contained in:
+373
-155
@@ -4,7 +4,7 @@ importScripts('data/names.js');
|
||||
|
||||
const LOG_PREFIX = '[MultiPage:bg]';
|
||||
const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill';
|
||||
const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
const HUMAN_STEP_DELAY_MIN = 700;
|
||||
const HUMAN_STEP_DELAY_MAX = 2200;
|
||||
|
||||
@@ -25,12 +25,16 @@ const DEFAULT_STATE = {
|
||||
password: null,
|
||||
accounts: [], // { email, password, createdAt }
|
||||
lastEmailTimestamp: null,
|
||||
lastSignupCode: null,
|
||||
lastLoginCode: null,
|
||||
localhostUrl: null,
|
||||
flowStartTime: null,
|
||||
tabRegistry: {},
|
||||
logs: [],
|
||||
vpsUrl: '',
|
||||
vpsPassword: '',
|
||||
customPassword: '',
|
||||
autoRunSkipFailures: false,
|
||||
mailProvider: '163', // 'qq' or '163'
|
||||
inbucketHost: '',
|
||||
inbucketMailbox: '',
|
||||
@@ -85,7 +89,9 @@ async function resetState() {
|
||||
'accounts',
|
||||
'tabRegistry',
|
||||
'vpsUrl',
|
||||
'vpsPassword',
|
||||
'customPassword',
|
||||
'autoRunSkipFailures',
|
||||
'mailProvider',
|
||||
'inbucketHost',
|
||||
'inbucketMailbox',
|
||||
@@ -98,7 +104,9 @@ async function resetState() {
|
||||
accounts: prev.accounts || [],
|
||||
tabRegistry: prev.tabRegistry || {},
|
||||
vpsUrl: prev.vpsUrl || '',
|
||||
vpsPassword: prev.vpsPassword || '',
|
||||
customPassword: prev.customPassword || '',
|
||||
autoRunSkipFailures: Boolean(prev.autoRunSkipFailures),
|
||||
mailProvider: prev.mailProvider || '163',
|
||||
inbucketHost: prev.inbucketHost || '',
|
||||
inbucketMailbox: prev.inbucketMailbox || '',
|
||||
@@ -383,6 +391,19 @@ async function addLog(message, level = 'info') {
|
||||
chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {});
|
||||
}
|
||||
|
||||
function getSourceLabel(source) {
|
||||
const labels = {
|
||||
'sidepanel': '侧边栏',
|
||||
'signup-page': '认证页',
|
||||
'vps-panel': 'VPS 面板',
|
||||
'qq-mail': 'QQ 邮箱',
|
||||
'mail-163': '163 邮箱',
|
||||
'inbucket-mail': 'Inbucket 邮箱',
|
||||
'duck-mail': 'Duck 邮箱',
|
||||
};
|
||||
return labels[source] || source || '未知来源';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step Status Management
|
||||
// ============================================================
|
||||
@@ -429,10 +450,10 @@ async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY
|
||||
|
||||
async function clickWithDebugger(tabId, rect) {
|
||||
if (!tabId) {
|
||||
throw new Error('No auth tab found for debugger click.');
|
||||
throw new Error('未找到用于调试点击的认证页面标签页。');
|
||||
}
|
||||
if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) {
|
||||
throw new Error('Step 8 debugger fallback needs a valid button position.');
|
||||
throw new Error('步骤 8 的调试器兜底点击需要有效的按钮坐标。');
|
||||
}
|
||||
|
||||
const target = { tabId };
|
||||
@@ -440,8 +461,8 @@ async function clickWithDebugger(tabId, rect) {
|
||||
await chrome.debugger.attach(target, '1.3');
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Debugger attach failed during step 8 fallback: ${err.message}. ` +
|
||||
'If DevTools is open on the auth tab, close it and retry.'
|
||||
`步骤 8 的调试器兜底点击附加失败:${err.message}。` +
|
||||
'如果认证页标签已打开 DevTools,请先关闭后重试。'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -519,14 +540,14 @@ async function handleMessage(message, sender) {
|
||||
if (tabId && message.source) {
|
||||
await registerTab(message.source, tabId);
|
||||
flushCommand(message.source, tabId);
|
||||
await addLog(`Content script ready: ${message.source} (tab ${tabId})`);
|
||||
await addLog(`内容脚本已就绪:${getSourceLabel(message.source)}(标签页 ${tabId})`);
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'LOG': {
|
||||
const { message: msg, level } = message.payload;
|
||||
await addLog(`[${message.source}] ${msg}`, level);
|
||||
await addLog(`[${getSourceLabel(message.source)}] ${msg}`, level);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -537,7 +558,7 @@ async function handleMessage(message, sender) {
|
||||
return { ok: true };
|
||||
}
|
||||
await setStepStatus(message.step, 'completed');
|
||||
await addLog(`Step ${message.step} completed`, 'ok');
|
||||
await addLog(`步骤 ${message.step} 已完成`, 'ok');
|
||||
await handleStepData(message.step, message.payload);
|
||||
notifyStepComplete(message.step, message.payload);
|
||||
return { ok: true };
|
||||
@@ -546,11 +567,11 @@ async function handleMessage(message, sender) {
|
||||
case 'STEP_ERROR': {
|
||||
if (isStopError(message.error)) {
|
||||
await setStepStatus(message.step, 'stopped');
|
||||
await addLog(`Step ${message.step} stopped by user`, 'warn');
|
||||
await addLog(`步骤 ${message.step} 已被用户停止`, 'warn');
|
||||
notifyStepError(message.step, message.error);
|
||||
} else {
|
||||
await setStepStatus(message.step, 'failed');
|
||||
await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
|
||||
await addLog(`步骤 ${message.step} 失败:${message.error}`, 'error');
|
||||
notifyStepError(message.step, message.error);
|
||||
}
|
||||
return { ok: true };
|
||||
@@ -563,7 +584,7 @@ async function handleMessage(message, sender) {
|
||||
case 'RESET': {
|
||||
clearStopRequest();
|
||||
await resetState();
|
||||
await addLog('Flow reset', 'info');
|
||||
await addLog('流程已重置', 'info');
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -581,7 +602,9 @@ async function handleMessage(message, sender) {
|
||||
case 'AUTO_RUN': {
|
||||
clearStopRequest();
|
||||
const totalRuns = message.payload?.totalRuns || 1;
|
||||
autoRunLoop(totalRuns); // fire-and-forget
|
||||
const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures);
|
||||
await setState({ autoRunSkipFailures });
|
||||
autoRunLoop(totalRuns, { autoRunSkipFailures }); // fire-and-forget
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -597,7 +620,9 @@ async function handleMessage(message, sender) {
|
||||
case 'SAVE_SETTING': {
|
||||
const updates = {};
|
||||
if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
|
||||
if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword;
|
||||
if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
|
||||
if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures);
|
||||
if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
|
||||
if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
|
||||
if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
|
||||
@@ -688,6 +713,19 @@ function notifyStepError(step, error) {
|
||||
if (waiter) waiter.reject(new Error(error));
|
||||
}
|
||||
|
||||
async function completeStepFromBackground(step, payload = {}) {
|
||||
if (stopRequested) {
|
||||
await setStepStatus(step, 'stopped');
|
||||
notifyStepError(step, STOP_ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
await setStepStatus(step, 'completed');
|
||||
await addLog(`步骤 ${step} 已完成`, 'ok');
|
||||
await handleStepData(step, payload);
|
||||
notifyStepComplete(step, payload);
|
||||
}
|
||||
|
||||
async function markRunningStepsStopped() {
|
||||
const state = await getState();
|
||||
const runningSteps = Object.entries(state.stepStatuses || {})
|
||||
@@ -709,7 +747,7 @@ async function requestStop() {
|
||||
webNavListener = null;
|
||||
}
|
||||
|
||||
await addLog('Stop requested. Cancelling current operations...', 'warn');
|
||||
await addLog('已收到停止请求,正在取消当前操作...', 'warn');
|
||||
await broadcastStopToContentScripts();
|
||||
|
||||
for (const waiter of stepWaiters.values()) {
|
||||
@@ -739,7 +777,7 @@ async function executeStep(step) {
|
||||
console.log(LOG_PREFIX, `Executing step ${step}`);
|
||||
throwIfStopped();
|
||||
await setStepStatus(step, 'running');
|
||||
await addLog(`Step ${step} started`);
|
||||
await addLog(`步骤 ${step} 开始执行`);
|
||||
await humanStepDelay();
|
||||
|
||||
const state = await getState();
|
||||
@@ -761,16 +799,16 @@ async function executeStep(step) {
|
||||
case 8: await executeStep8(state); break;
|
||||
case 9: await executeStep9(state); break;
|
||||
default:
|
||||
throw new Error(`Unknown step: ${step}`);
|
||||
throw new Error(`未知步骤:${step}`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isStopError(err)) {
|
||||
await setStepStatus(step, 'stopped');
|
||||
await addLog(`Step ${step} stopped by user`, 'warn');
|
||||
await addLog(`步骤 ${step} 已被用户停止`, 'warn');
|
||||
throw err;
|
||||
}
|
||||
await setStepStatus(step, 'failed');
|
||||
await addLog(`Step ${step} failed: ${err.message}`, 'error');
|
||||
await addLog(`步骤 ${step} 失败:${err.message}`, 'error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -795,7 +833,7 @@ async function fetchDuckEmail(options = {}) {
|
||||
throwIfStopped();
|
||||
const { generateNew = true } = options;
|
||||
|
||||
await addLog(`Duck Mail: Opening autofill settings (${generateNew ? 'generate new' : 'reuse current'})...`);
|
||||
await addLog(`Duck 邮箱:正在打开自动填充设置(${generateNew ? '生成新地址' : '复用当前地址'})...`);
|
||||
await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL);
|
||||
|
||||
const result = await sendToContentScript('duck-mail', {
|
||||
@@ -808,11 +846,11 @@ async function fetchDuckEmail(options = {}) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
if (!result?.email) {
|
||||
throw new Error('Duck email not returned.');
|
||||
throw new Error('未返回 Duck 邮箱地址。');
|
||||
}
|
||||
|
||||
await setEmailState(result.email);
|
||||
await addLog(`Duck Mail: ${result.generated ? 'Generated' : 'Loaded'} ${result.email}`, 'ok');
|
||||
await addLog(`Duck 邮箱:${result.generated ? '已生成' : '已读取'} ${result.email}`, 'ok');
|
||||
return result.email;
|
||||
}
|
||||
|
||||
@@ -824,38 +862,58 @@ let autoRunActive = false;
|
||||
let autoRunCurrentRun = 0;
|
||||
let autoRunTotalRuns = 1;
|
||||
|
||||
// Outer loop: runs the full flow N times
|
||||
async function autoRunLoop(totalRuns) {
|
||||
// Outer loop: keep retrying until the target number of successful runs is reached.
|
||||
async function autoRunLoop(totalRuns, options = {}) {
|
||||
if (autoRunActive) {
|
||||
await addLog('Auto run already in progress', 'warn');
|
||||
await addLog('自动运行已在进行中', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
clearStopRequest();
|
||||
autoRunActive = true;
|
||||
autoRunTotalRuns = totalRuns;
|
||||
await setState({ autoRunning: true });
|
||||
const autoRunSkipFailures = Boolean(options.autoRunSkipFailures);
|
||||
const maxAttempts = autoRunSkipFailures ? Math.max(totalRuns * 10, totalRuns + 20) : totalRuns;
|
||||
let successfulRuns = 0;
|
||||
let attemptRuns = 0;
|
||||
let forceFreshTabsNextRun = false;
|
||||
|
||||
for (let run = 1; run <= totalRuns; run++) {
|
||||
autoRunCurrentRun = run;
|
||||
await setState({ autoRunning: true, autoRunSkipFailures });
|
||||
|
||||
// Reset everything at the start of each run (keep VPS/mail settings)
|
||||
while (successfulRuns < totalRuns && attemptRuns < maxAttempts) {
|
||||
attemptRuns += 1;
|
||||
const targetRun = successfulRuns + 1;
|
||||
autoRunCurrentRun = targetRun;
|
||||
|
||||
// Reset everything at the start of each attempt (keep user settings).
|
||||
const prevState = await getState();
|
||||
const keepSettings = {
|
||||
vpsUrl: prevState.vpsUrl,
|
||||
vpsPassword: prevState.vpsPassword,
|
||||
customPassword: prevState.customPassword,
|
||||
autoRunSkipFailures: prevState.autoRunSkipFailures,
|
||||
mailProvider: prevState.mailProvider,
|
||||
inbucketHost: prevState.inbucketHost,
|
||||
inbucketMailbox: prevState.inbucketMailbox,
|
||||
autoRunning: true,
|
||||
...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}),
|
||||
};
|
||||
await resetState();
|
||||
await setState(keepSettings);
|
||||
// Tell side panel to reset all UI
|
||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {});
|
||||
await sleepWithStop(500);
|
||||
|
||||
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 } });
|
||||
if (forceFreshTabsNextRun) {
|
||||
await addLog(`兜底模式:上一轮已放弃,当前开始第 ${attemptRuns} 次尝试,将使用新线程继续补足第 ${targetRun}/${totalRuns} 轮。`, 'warn');
|
||||
forceFreshTabsNextRun = false;
|
||||
}
|
||||
|
||||
const status = (phase) => ({
|
||||
type: 'AUTO_RUN_STATUS',
|
||||
payload: { phase, currentRun: targetRun, totalRuns, attemptRun: attemptRuns },
|
||||
});
|
||||
|
||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info');
|
||||
|
||||
try {
|
||||
throwIfStopped();
|
||||
@@ -867,27 +925,25 @@ async function autoRunLoop(totalRuns) {
|
||||
let emailReady = false;
|
||||
try {
|
||||
const duckEmail = await fetchDuckEmail({ generateNew: true });
|
||||
await addLog(`=== Run ${run}/${totalRuns} — Duck email ready: ${duckEmail} ===`, 'ok');
|
||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:Duck 邮箱已就绪:${duckEmail}(第 ${attemptRuns} 次尝试)===`, 'ok');
|
||||
emailReady = true;
|
||||
} catch (err) {
|
||||
await addLog(`Duck Mail auto-fetch failed: ${err.message}`, 'warn');
|
||||
await addLog(`Duck 邮箱自动获取失败:${err.message}`, 'warn');
|
||||
}
|
||||
|
||||
if (!emailReady) {
|
||||
await addLog(`=== Run ${run}/${totalRuns} PAUSED: Fetch Duck email or paste manually, then continue ===`, 'warn');
|
||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先获取 Duck 邮箱或手动粘贴邮箱,然后继续 ===`, 'warn');
|
||||
chrome.runtime.sendMessage(status('waiting_email')).catch(() => {});
|
||||
|
||||
// Wait for RESUME_AUTO_RUN — sets a promise that resumeAutoRun resolves
|
||||
await waitForResume();
|
||||
|
||||
const resumedState = await getState();
|
||||
if (!resumedState.email) {
|
||||
await addLog('Cannot resume: no email address.', 'error');
|
||||
break;
|
||||
throw new Error('无法继续:当前没有邮箱地址。');
|
||||
}
|
||||
}
|
||||
|
||||
await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info');
|
||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,注册、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info');
|
||||
chrome.runtime.sendMessage(status('running')).catch(() => {});
|
||||
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
@@ -903,29 +959,56 @@ async function autoRunLoop(totalRuns) {
|
||||
await executeStepAndWait(8, 2000);
|
||||
await executeStepAndWait(9, 1000);
|
||||
|
||||
await addLog(`=== Run ${run}/${totalRuns} COMPLETE! ===`, 'ok');
|
||||
|
||||
successfulRuns += 1;
|
||||
autoRunCurrentRun = successfulRuns;
|
||||
await addLog(`=== 目标 ${successfulRuns}/${totalRuns} 轮已完成(第 ${attemptRuns} 次尝试成功)===`, 'ok');
|
||||
continue;
|
||||
} catch (err) {
|
||||
if (isStopError(err)) {
|
||||
await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn');
|
||||
} else {
|
||||
await addLog(`Run ${run}/${totalRuns} failed: ${err.message}`, 'error');
|
||||
await addLog(`目标 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
||||
chrome.runtime.sendMessage(status('stopped')).catch(() => {});
|
||||
break;
|
||||
}
|
||||
chrome.runtime.sendMessage(status('stopped')).catch(() => {});
|
||||
break; // Stop on error
|
||||
|
||||
if (!autoRunSkipFailures) {
|
||||
await addLog(`目标 ${targetRun}/${totalRuns} 轮失败:${err.message}`, 'error');
|
||||
chrome.runtime.sendMessage(status('stopped')).catch(() => {});
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`目标 ${targetRun}/${totalRuns} 轮的第 ${attemptRuns} 次尝试失败:${err.message}`, 'error');
|
||||
await addLog('兜底开关已开启:将放弃当前线程,重新开一轮继续补足目标次数。', 'warn');
|
||||
cancelPendingCommands('当前尝试已放弃。');
|
||||
await broadcastStopToContentScripts();
|
||||
chrome.runtime.sendMessage(status('retrying')).catch(() => {});
|
||||
forceFreshTabsNextRun = true;
|
||||
}
|
||||
}
|
||||
|
||||
const completedRuns = autoRunCurrentRun;
|
||||
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');
|
||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
|
||||
if (!stopRequested && autoRunSkipFailures && successfulRuns < totalRuns && attemptRuns >= maxAttempts) {
|
||||
await addLog(`已达到安全重试上限(${attemptRuns} 次尝试),当前仅完成 ${successfulRuns}/${totalRuns} 轮。`, 'error');
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'AUTO_RUN_STATUS',
|
||||
payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
|
||||
}).catch(() => {});
|
||||
} else if (stopRequested) {
|
||||
await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'AUTO_RUN_STATUS',
|
||||
payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
|
||||
}).catch(() => {});
|
||||
} else if (successfulRuns >= autoRunTotalRuns) {
|
||||
await addLog(`=== 全部 ${autoRunTotalRuns} 轮均已成功完成,共尝试 ${attemptRuns} 次 ===`, 'ok');
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'AUTO_RUN_STATUS',
|
||||
payload: { phase: 'complete', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
await addLog(`=== Stopped after ${completedRuns}/${autoRunTotalRuns} runs ===`, 'warn');
|
||||
chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
|
||||
await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'AUTO_RUN_STATUS',
|
||||
payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
|
||||
}).catch(() => {});
|
||||
}
|
||||
autoRunActive = false;
|
||||
await setState({ autoRunning: false });
|
||||
@@ -943,7 +1026,7 @@ async function resumeAutoRun() {
|
||||
throwIfStopped();
|
||||
const state = await getState();
|
||||
if (!state.email) {
|
||||
await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error');
|
||||
await addLog('无法继续:当前没有邮箱地址,请先在侧边栏填写邮箱。', 'error');
|
||||
return;
|
||||
}
|
||||
if (resumeWaiter) {
|
||||
@@ -958,9 +1041,9 @@ async function resumeAutoRun() {
|
||||
|
||||
async function executeStep1(state) {
|
||||
if (!state.vpsUrl) {
|
||||
throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.');
|
||||
throw new Error('尚未配置 VPS 地址,请先在侧边栏填写。');
|
||||
}
|
||||
await addLog(`Step 1: Opening VPS panel...`);
|
||||
await addLog('步骤 1:正在打开 VPS 面板...');
|
||||
await reuseOrCreateTab('vps-panel', state.vpsUrl, {
|
||||
inject: ['content/utils.js', 'content/vps-panel.js'],
|
||||
reloadIfSameUrl: true,
|
||||
@@ -970,7 +1053,7 @@ async function executeStep1(state) {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 1,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
payload: { vpsPassword: state.vpsPassword },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -980,9 +1063,9 @@ async function executeStep1(state) {
|
||||
|
||||
async function executeStep2(state) {
|
||||
if (!state.oauthUrl) {
|
||||
throw new Error('No OAuth URL. Complete step 1 first.');
|
||||
throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
|
||||
}
|
||||
await addLog(`Step 2: Opening auth URL...`);
|
||||
await addLog('步骤 2:正在打开认证链接...');
|
||||
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||
|
||||
await sendToContentScript('signup-page', {
|
||||
@@ -999,7 +1082,7 @@ async function executeStep2(state) {
|
||||
|
||||
async function executeStep3(state) {
|
||||
if (!state.email) {
|
||||
throw new Error('No email address. Paste email in Side Panel first.');
|
||||
throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。');
|
||||
}
|
||||
|
||||
const password = state.customPassword || generatePassword();
|
||||
@@ -1011,7 +1094,7 @@ async function executeStep3(state) {
|
||||
await setState({ accounts });
|
||||
|
||||
await addLog(
|
||||
`Step 3: Filling email ${state.email}, password ${state.customPassword ? 'customized' : 'generated'} (${password.length} chars)`
|
||||
`步骤 3:正在填写邮箱 ${state.email},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
|
||||
);
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
@@ -1028,27 +1111,27 @@ async function executeStep3(state) {
|
||||
function getMailConfig(state) {
|
||||
const provider = state.mailProvider || 'qq';
|
||||
if (provider === '163') {
|
||||
return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 Mail' };
|
||||
return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' };
|
||||
}
|
||||
if (provider === 'inbucket') {
|
||||
const host = normalizeInbucketOrigin(state.inbucketHost);
|
||||
const mailbox = (state.inbucketMailbox || '').trim();
|
||||
if (!host) {
|
||||
return { error: 'Inbucket host is empty or invalid.' };
|
||||
return { error: 'Inbucket 主机地址为空或无效。' };
|
||||
}
|
||||
if (!mailbox) {
|
||||
return { error: 'Inbucket mailbox name is empty.' };
|
||||
return { error: 'Inbucket 邮箱名称为空。' };
|
||||
}
|
||||
return {
|
||||
source: 'inbucket-mail',
|
||||
url: `${host}/m/${encodeURIComponent(mailbox)}/`,
|
||||
label: `Inbucket Mailbox (${mailbox})`,
|
||||
label: `Inbucket 邮箱(${mailbox})`,
|
||||
navigateOnReuse: true,
|
||||
inject: ['content/utils.js', 'content/inbucket-mail.js'],
|
||||
injectSource: 'inbucket-mail',
|
||||
};
|
||||
}
|
||||
return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
|
||||
return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ 邮箱' };
|
||||
}
|
||||
|
||||
function normalizeInbucketOrigin(rawValue) {
|
||||
@@ -1065,10 +1148,189 @@ function normalizeInbucketOrigin(rawValue) {
|
||||
}
|
||||
}
|
||||
|
||||
function getVerificationCodeStateKey(step) {
|
||||
return step === 4 ? 'lastSignupCode' : 'lastLoginCode';
|
||||
}
|
||||
|
||||
function getVerificationCodeLabel(step) {
|
||||
return step === 4 ? '注册' : '登录';
|
||||
}
|
||||
|
||||
function getVerificationPollPayload(step, state, overrides = {}) {
|
||||
if (step === 4) {
|
||||
return {
|
||||
filterAfterTimestamp: state.flowStartTime || 0,
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm'],
|
||||
targetEmail: state.email,
|
||||
maxAttempts: 5,
|
||||
intervalMs: 3000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm', 'login'],
|
||||
targetEmail: state.email,
|
||||
maxAttempts: 5,
|
||||
intervalMs: 3000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function requestVerificationCodeResend(step) {
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
if (!signupTabId) {
|
||||
throw new Error('认证页面标签页已关闭,无法重新请求验证码。');
|
||||
}
|
||||
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await addLog(`步骤 ${step}:正在请求新的${getVerificationCodeLabel(step)}验证码...`, 'warn');
|
||||
|
||||
const result = await sendToContentScript('signup-page', {
|
||||
type: 'RESEND_VERIFICATION_CODE',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
if (result && result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
|
||||
const stateKey = getVerificationCodeStateKey(step);
|
||||
const rejectedCodes = new Set();
|
||||
if (state[stateKey]) {
|
||||
rejectedCodes.add(state[stateKey]);
|
||||
}
|
||||
for (const code of (pollOverrides.excludeCodes || [])) {
|
||||
if (code) rejectedCodes.add(code);
|
||||
}
|
||||
|
||||
let lastError = null;
|
||||
let filterAfterTimestamp = pollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
|
||||
const maxRounds = pollOverrides.maxRounds || 3;
|
||||
|
||||
for (let round = 1; round <= maxRounds; round++) {
|
||||
if (round > 1) {
|
||||
filterAfterTimestamp = await requestVerificationCodeResend(step);
|
||||
}
|
||||
|
||||
const payload = getVerificationPollPayload(step, state, {
|
||||
...pollOverrides,
|
||||
filterAfterTimestamp,
|
||||
excludeCodes: [...rejectedCodes],
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await sendToContentScript(mail.source, {
|
||||
type: 'POLL_EMAIL',
|
||||
step,
|
||||
source: 'background',
|
||||
payload,
|
||||
});
|
||||
|
||||
if (result && result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
if (!result || !result.code) {
|
||||
throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
|
||||
}
|
||||
|
||||
if (rejectedCodes.has(result.code)) {
|
||||
throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
await addLog(`步骤 ${step}:${err.message}`, 'warn');
|
||||
if (round < maxRounds) {
|
||||
await addLog(`步骤 ${step}:将重新发送验证码后重试(${round + 1}/${maxRounds})...`, 'warn');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
|
||||
}
|
||||
|
||||
async function submitVerificationCode(step, code) {
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
if (!signupTabId) {
|
||||
throw new Error('认证页面标签页已关闭,无法填写验证码。');
|
||||
}
|
||||
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
const result = await sendToContentScript('signup-page', {
|
||||
type: 'FILL_CODE',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: { code },
|
||||
});
|
||||
|
||||
if (result && result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function resolveVerificationStep(step, state, mail) {
|
||||
const stateKey = getVerificationCodeStateKey(step);
|
||||
const rejectedCodes = new Set();
|
||||
if (state[stateKey]) {
|
||||
rejectedCodes.add(state[stateKey]);
|
||||
}
|
||||
|
||||
let nextFilterAfterTimestamp = null;
|
||||
const maxSubmitAttempts = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
|
||||
const result = await pollFreshVerificationCode(step, state, mail, {
|
||||
excludeCodes: [...rejectedCodes],
|
||||
filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined,
|
||||
});
|
||||
|
||||
await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`);
|
||||
const submitResult = await submitVerificationCode(step, result.code);
|
||||
|
||||
if (submitResult.invalidCode) {
|
||||
rejectedCodes.add(result.code);
|
||||
await addLog(`步骤 ${step}:验证码被页面拒绝:${submitResult.errorText || result.code}`, 'warn');
|
||||
|
||||
if (attempt >= maxSubmitAttempts) {
|
||||
throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`);
|
||||
}
|
||||
|
||||
nextFilterAfterTimestamp = await requestVerificationCodeResend(step);
|
||||
await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn');
|
||||
continue;
|
||||
}
|
||||
|
||||
await setState({
|
||||
lastEmailTimestamp: result.emailTimestamp,
|
||||
[stateKey]: result.code,
|
||||
});
|
||||
|
||||
await completeStepFromBackground(step, {
|
||||
emailTimestamp: result.emailTimestamp,
|
||||
code: result.code,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeStep4(state) {
|
||||
const mail = getMailConfig(state);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
await addLog(`Step 4: Opening ${mail.label}...`);
|
||||
await addLog(`步骤 4:正在打开${mail.label}...`);
|
||||
|
||||
// For mail tabs, only create if not alive — don't navigate (preserves login session)
|
||||
const alive = await isTabAlive(mail.source);
|
||||
@@ -1089,42 +1351,8 @@ async function executeStep4(state) {
|
||||
});
|
||||
}
|
||||
|
||||
const result = await sendToContentScript(mail.source, {
|
||||
type: 'POLL_EMAIL',
|
||||
step: 4,
|
||||
source: 'background',
|
||||
payload: {
|
||||
filterAfterTimestamp: state.flowStartTime || 0,
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
|
||||
targetEmail: state.email,
|
||||
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.');
|
||||
}
|
||||
}
|
||||
await resolveVerificationStep(4, state, mail);
|
||||
return;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -1135,7 +1363,7 @@ 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 addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
|
||||
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
@@ -1151,13 +1379,13 @@ async function executeStep5(state) {
|
||||
|
||||
async function executeStep6(state) {
|
||||
if (!state.oauthUrl) {
|
||||
throw new Error('No OAuth URL. Complete step 1 first.');
|
||||
throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
|
||||
}
|
||||
if (!state.email) {
|
||||
throw new Error('No email. Complete step 3 first.');
|
||||
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
||||
}
|
||||
|
||||
await addLog(`Step 6: Opening OAuth URL for login...`);
|
||||
await addLog('步骤 6:正在打开用于登录的 OAuth 链接...');
|
||||
// Reuse the signup-page tab — navigate it to the OAuth URL
|
||||
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||
|
||||
@@ -1177,7 +1405,30 @@ async function executeStep6(state) {
|
||||
async function executeStep7(state) {
|
||||
const mail = getMailConfig(state);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
await addLog(`Step 7: Opening ${mail.label}...`);
|
||||
const authTabId = await getTabId('signup-page');
|
||||
|
||||
if (authTabId) {
|
||||
await chrome.tabs.update(authTabId, { active: true });
|
||||
} else {
|
||||
if (!state.oauthUrl) {
|
||||
throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
|
||||
}
|
||||
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||
}
|
||||
|
||||
await addLog('步骤 7:正在准备认证页,必要时切换到一次性验证码登录...');
|
||||
const prepareResult = await sendToContentScript('signup-page', {
|
||||
type: 'PREPARE_LOGIN_CODE',
|
||||
step: 7,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
if (prepareResult && prepareResult.error) {
|
||||
throw new Error(prepareResult.error);
|
||||
}
|
||||
|
||||
await addLog(`步骤 7:正在打开${mail.label}...`);
|
||||
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
@@ -1197,41 +1448,8 @@ async function executeStep7(state) {
|
||||
});
|
||||
}
|
||||
|
||||
const result = await sendToContentScript(mail.source, {
|
||||
type: 'POLL_EMAIL',
|
||||
step: 7,
|
||||
source: 'background',
|
||||
payload: {
|
||||
filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
|
||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||
subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
|
||||
targetEmail: state.email,
|
||||
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 signup/auth tab and fill code
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
if (signupTabId) {
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'FILL_CODE',
|
||||
step: 7,
|
||||
source: 'background',
|
||||
payload: { code: result.code },
|
||||
});
|
||||
} else {
|
||||
throw new Error('Auth page tab was closed. Cannot fill verification code.');
|
||||
}
|
||||
}
|
||||
await resolveVerificationStep(7, state, mail);
|
||||
return;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -1242,10 +1460,10 @@ let webNavListener = null;
|
||||
|
||||
async function executeStep8(state) {
|
||||
if (!state.oauthUrl) {
|
||||
throw new Error('No OAuth URL. Complete step 1 first.');
|
||||
throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
|
||||
}
|
||||
|
||||
await addLog('Step 8: Setting up localhost redirect listener...');
|
||||
await addLog('步骤 8:正在监听 localhost 回调地址...');
|
||||
|
||||
// Register webNavigation listener (scoped to this step)
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -1264,7 +1482,7 @@ async function executeStep8(state) {
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
cleanupListener();
|
||||
reject(new Error('Localhost redirect not captured after 120s. Step 8 click may have been blocked.'));
|
||||
reject(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 的点击可能被拦截了。'));
|
||||
}, 120000);
|
||||
|
||||
webNavListener = (details) => {
|
||||
@@ -1276,7 +1494,7 @@ async function executeStep8(state) {
|
||||
if (resolveCaptureWait) resolveCaptureWait(details.url);
|
||||
|
||||
setState({ localhostUrl: details.url }).then(() => {
|
||||
addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok');
|
||||
addLog(`步骤 8:已捕获 localhost 地址:${details.url}`, 'ok');
|
||||
setStepStatus(8, 'completed');
|
||||
notifyStepComplete(8, { localhostUrl: details.url });
|
||||
broadcastDataUpdate({ localhostUrl: details.url });
|
||||
@@ -1295,10 +1513,10 @@ async function executeStep8(state) {
|
||||
let signupTabId = await getTabId('signup-page');
|
||||
if (signupTabId) {
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await addLog('Step 8: Switched to auth page. Preparing debugger click...');
|
||||
await addLog('步骤 8:已切回认证页,正在准备调试器点击...');
|
||||
} else {
|
||||
signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||
await addLog('Step 8: Auth tab reopened. Preparing debugger click...');
|
||||
await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...');
|
||||
}
|
||||
|
||||
const clickResult = await sendToContentScript('signup-page', {
|
||||
@@ -1313,7 +1531,7 @@ async function executeStep8(state) {
|
||||
|
||||
if (!resolved) {
|
||||
await clickWithDebugger(signupTabId, clickResult?.rect);
|
||||
await addLog('Step 8: Debugger click dispatched, waiting for redirect...');
|
||||
await addLog('步骤 8:已发送调试器点击,正在等待跳转...');
|
||||
}
|
||||
} catch (err) {
|
||||
clearTimeout(timeout);
|
||||
@@ -1330,13 +1548,13 @@ async function executeStep8(state) {
|
||||
|
||||
async function executeStep9(state) {
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error('No localhost URL. Complete step 8 first.');
|
||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 8。');
|
||||
}
|
||||
if (!state.vpsUrl) {
|
||||
throw new Error('VPS URL not set. Please enter VPS URL in the side panel.');
|
||||
throw new Error('尚未填写 VPS 地址,请先在侧边栏输入。');
|
||||
}
|
||||
|
||||
await addLog('Step 9: Opening VPS panel...');
|
||||
await addLog('步骤 9:正在打开 VPS 面板...');
|
||||
|
||||
let tabId = await getTabId('vps-panel');
|
||||
const alive = tabId && await isTabAlive('vps-panel');
|
||||
@@ -1366,12 +1584,12 @@ async function executeStep9(state) {
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Send command directly — bypass queue/ready mechanism
|
||||
await addLog(`Step 9: Filling callback URL...`);
|
||||
await addLog('步骤 9:正在填写回调地址...');
|
||||
await chrome.tabs.sendMessage(tabId, {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 9,
|
||||
source: 'background',
|
||||
payload: { localhostUrl: state.localhostUrl },
|
||||
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
sendResponse(result);
|
||||
}).catch(err => {
|
||||
if (isStopError(err)) {
|
||||
log('Duck Mail: Stopped by user.', 'warn');
|
||||
log('Duck 邮箱:已被用户停止。', 'warn');
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
async function fetchDuckEmail(payload = {}) {
|
||||
const { generateNew = true } = payload;
|
||||
|
||||
log(`Duck Mail: ${generateNew ? 'Generating' : 'Reading'} private address...`);
|
||||
log(`Duck 邮箱:正在${generateNew ? '生成' : '读取'}私有地址...`);
|
||||
|
||||
await waitForElement(
|
||||
'input.AutofillSettingsPanel__PrivateDuckAddressValue, button.AutofillSettingsPanel__GeneratorButton',
|
||||
@@ -46,12 +46,12 @@ async function fetchDuckEmail(payload = {}) {
|
||||
}
|
||||
await sleep(150);
|
||||
}
|
||||
throw new Error('Timed out waiting for Duck address to appear.');
|
||||
throw new Error('等待 Duck 地址出现超时。');
|
||||
};
|
||||
|
||||
const currentEmail = readEmail();
|
||||
if (currentEmail && !generateNew) {
|
||||
log(`Duck Mail: Found existing address ${currentEmail}`);
|
||||
log(`Duck 邮箱:已发现现有地址 ${currentEmail}`);
|
||||
return { email: currentEmail, generated: false };
|
||||
}
|
||||
|
||||
@@ -59,16 +59,16 @@ async function fetchDuckEmail(payload = {}) {
|
||||
const generatorButton = getGeneratorButton();
|
||||
if (!generatorButton) {
|
||||
if (currentEmail) {
|
||||
log(`Duck Mail: Reusing existing address ${currentEmail}`, 'warn');
|
||||
log(`Duck 邮箱:正在复用现有地址 ${currentEmail}`, 'warn');
|
||||
return { email: currentEmail, generated: false };
|
||||
}
|
||||
throw new Error('Could not find "Generate Private Duck Address" button.');
|
||||
throw new Error('未找到“生成 Duck 私有地址”按钮。');
|
||||
}
|
||||
|
||||
generatorButton.click();
|
||||
log('Duck Mail: Clicked "Generate Private Duck Address"');
|
||||
log('Duck 邮箱:已点击“生成 Duck 私有地址”按钮');
|
||||
|
||||
const nextEmail = await waitForEmailValue(currentEmail);
|
||||
log(`Duck Mail: Ready address ${nextEmail}`, 'ok');
|
||||
log(`Duck 邮箱:地址已就绪 ${nextEmail}`, 'ok');
|
||||
return { email: nextEmail, generated: true };
|
||||
}
|
||||
|
||||
+21
-15
@@ -45,11 +45,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
sendResponse(result);
|
||||
}).catch(err => {
|
||||
if (isStopError(err)) {
|
||||
log(`Step ${message.step}: Stopped by user.`, 'warn');
|
||||
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
reportError(message.step, err.message);
|
||||
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true;
|
||||
@@ -158,10 +158,10 @@ async function deleteCurrentMailboxMessage(step) {
|
||||
try {
|
||||
const deleteButton = await waitForElement('.button-bar button.danger', 5000);
|
||||
simulateClick(deleteButton);
|
||||
log(`Step ${step}: Deleted mailbox message`, 'ok');
|
||||
log(`步骤 ${step}:已删除邮箱消息`, 'ok');
|
||||
await sleep(1200);
|
||||
} catch (err) {
|
||||
log(`Step ${step}: Failed to delete mailbox message: ${err.message}`, 'warn');
|
||||
log(`步骤 ${step}:删除邮箱消息失败:${err.message}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,24 +171,26 @@ async function handleMailboxPollEmail(step, payload) {
|
||||
subjectFilters = [],
|
||||
maxAttempts = 20,
|
||||
intervalMs = 3000,
|
||||
excludeCodes = [],
|
||||
} = payload || {};
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
|
||||
log(`Step ${step}: Starting email poll on Inbucket mailbox page (max ${maxAttempts} attempts)`);
|
||||
log(`步骤 ${step}:开始轮询 Inbucket 邮箱页面(最多 ${maxAttempts} 次)`);
|
||||
|
||||
try {
|
||||
await waitForElement('.message-list, .message-list-entry', 15000);
|
||||
log(`Step ${step}: Mailbox page loaded`);
|
||||
log(`步骤 ${step}:邮箱页面已加载`);
|
||||
} catch {
|
||||
throw new Error('Inbucket mailbox page did not load. Make sure /m/<mailbox>/ is open.');
|
||||
throw new Error('Inbucket 邮箱页面未加载完成,请确认已打开 /m/<mailbox>/ 页面。');
|
||||
}
|
||||
|
||||
const existingMailIds = getCurrentMailboxIds();
|
||||
log(`Step ${step}: Snapshotted ${existingMailIds.size} existing mailbox messages`);
|
||||
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧消息快照`);
|
||||
|
||||
const FALLBACK_AFTER = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
log(`Polling Inbucket mailbox... attempt ${attempt}/${maxAttempts}`);
|
||||
log(`步骤 ${step}:正在轮询 Inbucket 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
||||
|
||||
if (attempt > 1) {
|
||||
await refreshMailbox();
|
||||
@@ -212,6 +214,10 @@ async function handleMailboxPollEmail(step, payload) {
|
||||
for (const mail of candidates) {
|
||||
const code = mail.code || extractVerificationCode(mail.combinedText);
|
||||
if (!code) continue;
|
||||
if (excludedCodeSet.has(code)) {
|
||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
||||
continue;
|
||||
}
|
||||
|
||||
await openMailboxEntry(mail.entry);
|
||||
await deleteCurrentMailboxMessage(step);
|
||||
@@ -219,9 +225,9 @@ async function handleMailboxPollEmail(step, payload) {
|
||||
seenMailIds.add(mail.mailId);
|
||||
await persistSeenMailIds();
|
||||
|
||||
const source = existingMailIds.has(mail.mailId) ? 'fallback' : 'new';
|
||||
const source = existingMailIds.has(mail.mailId) ? '回退匹配邮件' : '新邮件';
|
||||
log(
|
||||
`Step ${step}: Code found: ${code} (${source}, sender: ${mail.sender || 'unknown'}, subject: ${(mail.subject || '').slice(0, 60)})`,
|
||||
`步骤 ${step}:已找到验证码:${code}(来源:${source},发件人:${mail.sender || '未知'},主题:${(mail.subject || '').slice(0, 60)})`,
|
||||
'ok'
|
||||
);
|
||||
|
||||
@@ -234,7 +240,7 @@ async function handleMailboxPollEmail(step, payload) {
|
||||
}
|
||||
|
||||
if (attempt === FALLBACK_AFTER + 1) {
|
||||
log(`Step ${step}: No new mailbox messages yet, falling back to older matching messages`, 'warn');
|
||||
log(`步骤 ${step}:暂未发现新消息,开始回退到较早的匹配邮件`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
@@ -243,14 +249,14 @@ async function handleMailboxPollEmail(step, payload) {
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No matching verification email found in Inbucket mailbox after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
|
||||
'Check the mailbox page manually.'
|
||||
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Inbucket 邮箱中找到匹配的验证码邮件。` +
|
||||
'请手动检查邮箱页面。'
|
||||
);
|
||||
}
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
if (!location.pathname.startsWith('/m/')) {
|
||||
throw new Error('Inbucket now only supports mailbox pages like /m/<mailbox>/.');
|
||||
throw new Error('当前 Inbucket 仅支持 /m/<mailbox>/ 这种邮箱页面。');
|
||||
}
|
||||
return handleMailboxPollEmail(step, payload);
|
||||
}
|
||||
|
||||
+30
-27
@@ -54,11 +54,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
sendResponse(result);
|
||||
}).catch(err => {
|
||||
if (isStopError(err)) {
|
||||
log(`Step ${message.step}: Stopped by user.`, 'warn');
|
||||
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
reportError(message.step, err.message);
|
||||
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true;
|
||||
@@ -87,22 +87,23 @@ function getCurrentMailIds() {
|
||||
// ============================================================
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload;
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
|
||||
log(`Step ${step}: Starting email poll on 163 Mail (max ${maxAttempts} attempts)`);
|
||||
log(`步骤 ${step}:开始轮询 163 邮箱(最多 ${maxAttempts} 次)`);
|
||||
|
||||
// Click inbox in sidebar to ensure we're in inbox view
|
||||
log(`Step ${step}: Waiting for sidebar...`);
|
||||
log(`步骤 ${step}:正在等待侧边栏加载...`);
|
||||
try {
|
||||
const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000);
|
||||
inboxLink.click();
|
||||
log(`Step ${step}: Clicked inbox`);
|
||||
log(`步骤 ${step}:已点击收件箱`);
|
||||
} catch {
|
||||
log(`Step ${step}: Inbox link not found, proceeding...`, 'warn');
|
||||
log(`步骤 ${step}:未找到收件箱入口,继续尝试后续流程...`, 'warn');
|
||||
}
|
||||
|
||||
// Wait for mail list to appear
|
||||
log(`Step ${step}: Waiting for mail list...`);
|
||||
log(`步骤 ${step}:正在等待邮件列表加载...`);
|
||||
let items = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
items = findMailItems();
|
||||
@@ -117,19 +118,19 @@ async function handlePollEmail(step, payload) {
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
throw new Error('163 Mail list did not load. Make sure inbox is open.');
|
||||
throw new Error('163 邮箱列表未加载完成,请确认当前已打开收件箱。');
|
||||
}
|
||||
|
||||
log(`Step ${step}: Mail list loaded, ${items.length} items`);
|
||||
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
|
||||
|
||||
// Snapshot existing mail IDs
|
||||
const existingMailIds = getCurrentMailIds();
|
||||
log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails`);
|
||||
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
|
||||
|
||||
const FALLBACK_AFTER = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
log(`Polling 163 Mail... attempt ${attempt}/${maxAttempts}`);
|
||||
log(`步骤 ${step}:正在轮询 163 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
||||
|
||||
if (attempt > 1) {
|
||||
await refreshInbox();
|
||||
@@ -157,11 +158,13 @@ async function handlePollEmail(step, payload) {
|
||||
|
||||
if (senderMatch || subjectMatch) {
|
||||
const code = extractVerificationCode(subject + ' ' + ariaLabel);
|
||||
if (code && !seenCodes.has(code)) {
|
||||
if (code && excludedCodeSet.has(code)) {
|
||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
||||
} else if (code && !seenCodes.has(code)) {
|
||||
seenCodes.add(code);
|
||||
persistSeenCodes();
|
||||
const source = useFallback && existingMailIds.has(id) ? 'fallback' : 'new';
|
||||
log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok');
|
||||
const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件' : '新邮件';
|
||||
log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)})`, 'ok');
|
||||
|
||||
// Delete this email via right-click menu, WAIT for it to finish before returning
|
||||
await deleteEmail(item, step);
|
||||
@@ -170,13 +173,13 @@ async function handlePollEmail(step, payload) {
|
||||
|
||||
return { ok: true, code, emailTimestamp: Date.now(), mailId: id };
|
||||
} else if (code && seenCodes.has(code)) {
|
||||
log(`Step ${step}: Skipping already-seen code: ${code}`, 'info');
|
||||
log(`步骤 ${step}:跳过已处理过的验证码:${code}`, 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt === FALLBACK_AFTER + 1) {
|
||||
log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first match`, 'warn');
|
||||
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
@@ -185,8 +188,8 @@ async function handlePollEmail(step, payload) {
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No new matching email found on 163 Mail after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
|
||||
'Check inbox manually.'
|
||||
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 163 邮箱中找到新的匹配邮件。` +
|
||||
'请手动检查收件箱。'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,7 +199,7 @@ async function handlePollEmail(step, payload) {
|
||||
|
||||
async function deleteEmail(item, step) {
|
||||
try {
|
||||
log(`Step ${step}: Deleting email...`);
|
||||
log(`步骤 ${step}:正在删除邮件...`);
|
||||
|
||||
// Strategy 1: Click the trash icon inside the mail item
|
||||
// Each mail item has: <b class="nui-ico nui-ico-delete" title="删除邮件" sign="trash">
|
||||
@@ -208,21 +211,21 @@ async function deleteEmail(item, step) {
|
||||
const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]');
|
||||
if (trashIcon) {
|
||||
trashIcon.click();
|
||||
log(`Step ${step}: Clicked trash icon`, 'ok');
|
||||
log(`步骤 ${step}:已点击删除图标`, 'ok');
|
||||
await sleep(1500);
|
||||
|
||||
// Check if item disappeared (confirm deletion)
|
||||
const stillExists = document.getElementById(item.id);
|
||||
if (!stillExists || stillExists.style.display === 'none') {
|
||||
log(`Step ${step}: Email deleted successfully`);
|
||||
log(`步骤 ${step}:邮件已成功删除`);
|
||||
} else {
|
||||
log(`Step ${step}: Email may not have been deleted, item still visible`, 'warn');
|
||||
log(`步骤 ${step}:邮件可能尚未删除,列表中仍可见`, 'warn');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy 2: Select checkbox then click toolbar delete button
|
||||
log(`Step ${step}: Trash icon not found, trying checkbox + toolbar delete...`);
|
||||
log(`步骤 ${step}:未找到删除图标,尝试使用复选框加工具栏删除...`);
|
||||
const checkbox = item.querySelector('[sign="checkbox"], .nui-chk');
|
||||
if (checkbox) {
|
||||
checkbox.click();
|
||||
@@ -233,16 +236,16 @@ async function deleteEmail(item, step) {
|
||||
for (const btn of toolbarBtns) {
|
||||
if (btn.textContent.replace(/\s/g, '').includes('删除')) {
|
||||
btn.closest('.nui-btn').click();
|
||||
log(`Step ${step}: Clicked toolbar delete`, 'ok');
|
||||
log(`步骤 ${step}:已点击工具栏删除`, 'ok');
|
||||
await sleep(1500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log(`Step ${step}: Could not delete email (no delete button found)`, 'warn');
|
||||
log(`步骤 ${step}:无法删除邮件(未找到删除按钮)`, 'warn');
|
||||
} catch (err) {
|
||||
log(`Step ${step}: Failed to delete email: ${err.message}`, 'warn');
|
||||
log(`步骤 ${step}:删除邮件失败:${err.message}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-13
@@ -27,11 +27,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
sendResponse(result);
|
||||
}).catch(err => {
|
||||
if (isStopError(err)) {
|
||||
log(`Step ${message.step}: Stopped by user.`, 'warn');
|
||||
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
reportError(message.step, err.message);
|
||||
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true; // async response
|
||||
@@ -55,28 +55,29 @@ function getCurrentMailIds() {
|
||||
// ============================================================
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload;
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
|
||||
log(`Step ${step}: Starting email poll (max ${maxAttempts} attempts, every ${intervalMs / 1000}s)`);
|
||||
log(`步骤 ${step}:开始轮询邮箱(最多 ${maxAttempts} 次,每 ${intervalMs / 1000} 秒一次)`);
|
||||
|
||||
// Wait for mail list to load
|
||||
try {
|
||||
await waitForElement('.mail-list-page-item', 10000);
|
||||
log(`Step ${step}: Mail list loaded`);
|
||||
log(`步骤 ${step}:邮件列表已加载`);
|
||||
} catch {
|
||||
throw new Error('Mail list did not load. Make sure QQ Mail inbox is open.');
|
||||
throw new Error('邮件列表未加载完成,请确认 QQ 邮箱已打开收件箱。');
|
||||
}
|
||||
|
||||
// Step 1: Snapshot existing mail IDs BEFORE we start waiting for new email
|
||||
const existingMailIds = getCurrentMailIds();
|
||||
log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails as "old"`);
|
||||
log(`步骤 ${step}:已将当前 ${existingMailIds.size} 封邮件标记为旧邮件快照`);
|
||||
|
||||
// Fallback after just 3 attempts (~10s). In practice, the email is usually
|
||||
// already in the list but has the same mailid (page was already open).
|
||||
const FALLBACK_AFTER = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
log(`Polling QQ Mail... attempt ${attempt}/${maxAttempts}`);
|
||||
log(`步骤 ${step}:正在轮询 QQ 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
||||
|
||||
// Refresh inbox (skip on first attempt, list is fresh)
|
||||
if (attempt > 1) {
|
||||
@@ -104,15 +105,19 @@ async function handlePollEmail(step, payload) {
|
||||
if (senderMatch || subjectMatch) {
|
||||
const code = extractVerificationCode(subject + ' ' + digest);
|
||||
if (code) {
|
||||
const source = useFallback && existingMailIds.has(mailId) ? 'fallback-first-match' : 'new';
|
||||
log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok');
|
||||
if (excludedCodeSet.has(code)) {
|
||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
||||
continue;
|
||||
}
|
||||
const source = useFallback && existingMailIds.has(mailId) ? '回退首封匹配邮件' : '新邮件';
|
||||
log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)})`, 'ok');
|
||||
return { ok: true, code, emailTimestamp: Date.now(), mailId };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt === FALLBACK_AFTER + 1) {
|
||||
log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first matching email`, 'warn');
|
||||
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
@@ -121,8 +126,8 @@ async function handlePollEmail(step, payload) {
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No new matching email found after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
|
||||
'Check QQ Mail manually. Email may be delayed or in spam folder.'
|
||||
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未找到新的匹配邮件。` +
|
||||
'请手动检查 QQ 邮箱,邮件可能延迟到达或进入垃圾箱。'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+413
-67
@@ -5,19 +5,25 @@ 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' || message.type === 'STEP8_FIND_AND_CLICK') {
|
||||
if (
|
||||
message.type === 'EXECUTE_STEP'
|
||||
|| message.type === 'FILL_CODE'
|
||||
|| message.type === 'STEP8_FIND_AND_CLICK'
|
||||
|| message.type === 'PREPARE_LOGIN_CODE'
|
||||
|| message.type === 'RESEND_VERIFICATION_CODE'
|
||||
) {
|
||||
resetStopState();
|
||||
handleCommand(message).then((result) => {
|
||||
sendResponse({ ok: true, ...(result || {}) });
|
||||
}).catch(err => {
|
||||
if (isStopError(err)) {
|
||||
log(`Step ${message.step || 8}: Stopped by user.`, 'warn');
|
||||
log(`步骤 ${message.step || 8}:已被用户停止。`, 'warn');
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'STEP8_FIND_AND_CLICK') {
|
||||
log(`Step 8: ${err.message}`, 'error');
|
||||
log(`步骤 8:${err.message}`, 'error');
|
||||
sendResponse({ error: err.message });
|
||||
return;
|
||||
}
|
||||
@@ -38,22 +44,212 @@ async function handleCommand(message) {
|
||||
case 5: return await step5_fillNameBirthday(message.payload);
|
||||
case 6: return await step6_login(message.payload);
|
||||
case 8: return await step8_findAndClick();
|
||||
default: throw new Error(`signup-page.js does not handle step ${message.step}`);
|
||||
default: throw new Error(`signup-page.js 不处理步骤 ${message.step}`);
|
||||
}
|
||||
case 'FILL_CODE':
|
||||
// Step 4 = signup code, Step 7 = login code (same handler)
|
||||
return await fillVerificationCode(message.step, message.payload);
|
||||
case 'PREPARE_LOGIN_CODE':
|
||||
return await prepareLoginCodeFlow();
|
||||
case 'RESEND_VERIFICATION_CODE':
|
||||
return await resendVerificationCode(message.step);
|
||||
case 'STEP8_FIND_AND_CLICK':
|
||||
return await step8_findAndClick();
|
||||
}
|
||||
}
|
||||
|
||||
const VERIFICATION_CODE_INPUT_SELECTOR = [
|
||||
'input[name="code"]',
|
||||
'input[name="otp"]',
|
||||
'input[autocomplete="one-time-code"]',
|
||||
'input[type="text"][maxlength="6"]',
|
||||
'input[type="tel"][maxlength="6"]',
|
||||
'input[aria-label*="code" i]',
|
||||
'input[placeholder*="code" i]',
|
||||
'input[inputmode="numeric"]',
|
||||
].join(', ');
|
||||
|
||||
const ONE_TIME_CODE_LOGIN_PATTERN = /使用一次性验证码登录|改用(?:一次性)?验证码(?:登录)?|使用验证码登录|一次性验证码|验证码登录|one[-\s]*time\s*(?:passcode|password|code)|use\s+(?:a\s+)?one[-\s]*time\s*(?:passcode|password|code)(?:\s+instead)?|use\s+(?:a\s+)?code(?:\s+instead)?|sign\s+in\s+with\s+(?:email|code)|email\s+(?:me\s+)?(?:a\s+)?code/i;
|
||||
|
||||
const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i;
|
||||
|
||||
function isVisibleElement(el) {
|
||||
if (!el) return false;
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return style.display !== 'none'
|
||||
&& style.visibility !== 'hidden'
|
||||
&& rect.width > 0
|
||||
&& rect.height > 0;
|
||||
}
|
||||
|
||||
function getVerificationCodeTarget() {
|
||||
const codeInput = document.querySelector(VERIFICATION_CODE_INPUT_SELECTOR);
|
||||
if (codeInput && isVisibleElement(codeInput)) {
|
||||
return { type: 'single', element: codeInput };
|
||||
}
|
||||
|
||||
const singleInputs = Array.from(document.querySelectorAll('input[maxlength="1"]'))
|
||||
.filter(isVisibleElement);
|
||||
if (singleInputs.length >= 6) {
|
||||
return { type: 'split', elements: singleInputs };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getActionText(el) {
|
||||
return [
|
||||
el?.textContent,
|
||||
el?.value,
|
||||
el?.getAttribute?.('aria-label'),
|
||||
el?.getAttribute?.('title'),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isActionEnabled(el) {
|
||||
return Boolean(el)
|
||||
&& !el.disabled
|
||||
&& el.getAttribute('aria-disabled') !== 'true';
|
||||
}
|
||||
|
||||
function findOneTimeCodeLoginTrigger() {
|
||||
const candidates = document.querySelectorAll(
|
||||
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
|
||||
);
|
||||
|
||||
for (const el of candidates) {
|
||||
if (!isVisibleElement(el)) continue;
|
||||
if (el.disabled || el.getAttribute('aria-disabled') === 'true') continue;
|
||||
|
||||
const text = [
|
||||
el.textContent,
|
||||
el.value,
|
||||
el.getAttribute('aria-label'),
|
||||
el.getAttribute('title'),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
if (text && ONE_TIME_CODE_LOGIN_PATTERN.test(text)) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findResendVerificationCodeTrigger({ allowDisabled = false } = {}) {
|
||||
const candidates = document.querySelectorAll(
|
||||
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
|
||||
);
|
||||
|
||||
for (const el of candidates) {
|
||||
if (!isVisibleElement(el)) continue;
|
||||
if (!allowDisabled && !isActionEnabled(el)) continue;
|
||||
|
||||
const text = getActionText(el);
|
||||
if (text && RESEND_VERIFICATION_CODE_PATTERN.test(text)) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function prepareLoginCodeFlow(timeout = 15000) {
|
||||
const readyTarget = getVerificationCodeTarget();
|
||||
if (readyTarget) {
|
||||
log('步骤 7:验证码输入框已就绪。');
|
||||
return { ready: true, mode: readyTarget.type };
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
let switchClickCount = 0;
|
||||
let lastSwitchAttemptAt = 0;
|
||||
let loggedPasswordPage = false;
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
|
||||
const target = getVerificationCodeTarget();
|
||||
if (target) {
|
||||
log('步骤 7:验证码页面已就绪。');
|
||||
return { ready: true, mode: target.type };
|
||||
}
|
||||
|
||||
const passwordInput = document.querySelector('input[type="password"]');
|
||||
const switchTrigger = findOneTimeCodeLoginTrigger();
|
||||
|
||||
if (switchTrigger && (switchClickCount === 0 || Date.now() - lastSwitchAttemptAt > 1500)) {
|
||||
switchClickCount += 1;
|
||||
lastSwitchAttemptAt = Date.now();
|
||||
loggedPasswordPage = false;
|
||||
log('步骤 7:检测到密码页,正在切换到一次性验证码登录...');
|
||||
await humanPause(350, 900);
|
||||
simulateClick(switchTrigger);
|
||||
await sleep(1200);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (passwordInput && !loggedPasswordPage) {
|
||||
loggedPasswordPage = true;
|
||||
log('步骤 7:正在等待密码页上的一次性验证码登录入口...');
|
||||
}
|
||||
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
throw new Error('无法切换到一次性验证码验证页面。URL: ' + location.href);
|
||||
}
|
||||
|
||||
async function resendVerificationCode(step, timeout = 45000) {
|
||||
if (step === 7) {
|
||||
await prepareLoginCodeFlow();
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
let action = null;
|
||||
let loggedWaiting = false;
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
action = findResendVerificationCodeTrigger({ allowDisabled: true });
|
||||
|
||||
if (action && isActionEnabled(action)) {
|
||||
log(`步骤 ${step}:重新发送验证码按钮已可用。`);
|
||||
await humanPause(350, 900);
|
||||
simulateClick(action);
|
||||
await sleep(1200);
|
||||
return {
|
||||
resent: true,
|
||||
buttonText: getActionText(action),
|
||||
};
|
||||
}
|
||||
|
||||
if (action && !loggedWaiting) {
|
||||
loggedWaiting = true;
|
||||
log(`步骤 ${step}:正在等待重新发送验证码按钮变为可点击...`);
|
||||
}
|
||||
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
throw new Error('无法点击重新发送验证码按钮。URL: ' + location.href);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 2: Click Register
|
||||
// ============================================================
|
||||
|
||||
async function step2_clickRegister() {
|
||||
log('Step 2: Looking for Register/Sign up button...');
|
||||
log('步骤 2:正在查找注册按钮...');
|
||||
|
||||
let registerBtn = null;
|
||||
try {
|
||||
@@ -68,8 +264,8 @@ async function step2_clickRegister() {
|
||||
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
|
||||
'未找到注册按钮。' +
|
||||
'请在 DevTools 中检查认证页面 DOM。URL: ' + location.href
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +273,7 @@ async function step2_clickRegister() {
|
||||
await humanPause(450, 1200);
|
||||
reportComplete(2);
|
||||
simulateClick(registerBtn);
|
||||
log('Step 2: Clicked Register button');
|
||||
log('步骤 2:已点击注册按钮');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -86,9 +282,9 @@ async function step2_clickRegister() {
|
||||
|
||||
async function step3_fillEmailPassword(payload) {
|
||||
const { email } = payload;
|
||||
if (!email) throw new Error('No email provided. Paste email in Side Panel first.');
|
||||
if (!email) throw new Error('未提供邮箱地址,请先在侧边栏粘贴邮箱。');
|
||||
|
||||
log(`Step 3: Filling email: ${email}`);
|
||||
log(`步骤 3:正在填写邮箱:${email}`);
|
||||
|
||||
// Find email input
|
||||
let emailInput = null;
|
||||
@@ -98,40 +294,40 @@ async function step3_fillEmailPassword(payload) {
|
||||
10000
|
||||
);
|
||||
} catch {
|
||||
throw new Error('Could not find email input field on signup page. URL: ' + location.href);
|
||||
throw new Error('在注册页未找到邮箱输入框。URL: ' + location.href);
|
||||
}
|
||||
|
||||
await humanPause(500, 1400);
|
||||
fillInput(emailInput, email);
|
||||
log('Step 3: Email filled');
|
||||
log('步骤 3:邮箱已填写');
|
||||
|
||||
// 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...');
|
||||
log('步骤 3:暂未发现密码输入框,先提交邮箱...');
|
||||
const submitBtn = document.querySelector('button[type="submit"]')
|
||||
|| await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
|
||||
|
||||
if (submitBtn) {
|
||||
await humanPause(400, 1100);
|
||||
simulateClick(submitBtn);
|
||||
log('Step 3: Submitted email, waiting for password field...');
|
||||
log('步骤 3:邮箱已提交,正在等待密码输入框...');
|
||||
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);
|
||||
throw new Error('提交邮箱后仍未找到密码输入框。URL: ' + location.href);
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
|
||||
if (!payload.password) throw new Error('未提供密码,步骤 3 需要可用密码。');
|
||||
await humanPause(600, 1500);
|
||||
fillInput(passwordInput, payload.password);
|
||||
log('Step 3: Password filled');
|
||||
log('步骤 3:密码已填写');
|
||||
|
||||
// Report complete BEFORE submit, because submit causes page navigation
|
||||
// which kills the content script connection
|
||||
@@ -145,7 +341,7 @@ async function step3_fillEmailPassword(payload) {
|
||||
if (submitBtn) {
|
||||
await humanPause(500, 1300);
|
||||
simulateClick(submitBtn);
|
||||
log('Step 3: Form submitted');
|
||||
log('步骤 3:表单已提交');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,40 +349,177 @@ async function step3_fillEmailPassword(payload) {
|
||||
// Fill Verification Code (used by step 4 and step 7)
|
||||
// ============================================================
|
||||
|
||||
const INVALID_VERIFICATION_CODE_PATTERN = /代码不正确|验证码不正确|验证码错误|code\s+(?:is\s+)?incorrect|invalid\s+code|incorrect\s+code|try\s+again/i;
|
||||
const VERIFICATION_PAGE_PATTERN = /检查您的收件箱|输入我们刚刚向|重新发送电子邮件|重新发送验证码|验证码|代码不正确|email\s+verification/i;
|
||||
const OAUTH_CONSENT_PAGE_PATTERN = /使用\s*ChatGPT\s*登录到\s*Codex|login\s+to\s+codex|log\s+in\s+to\s+codex|authorize|授权/i;
|
||||
const ADD_PHONE_PAGE_PATTERN = /add[\s-]*phone|添加手机号|手机号码|手机号|phone\s+number|telephone/i;
|
||||
|
||||
function getVerificationErrorText() {
|
||||
const messages = [];
|
||||
const selectors = [
|
||||
'.react-aria-FieldError',
|
||||
'[slot="errorMessage"]',
|
||||
'[id$="-error"]',
|
||||
'[data-invalid="true"] + *',
|
||||
'[aria-invalid="true"] + *',
|
||||
'[class*="error"]',
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
document.querySelectorAll(selector).forEach((el) => {
|
||||
const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
if (text) {
|
||||
messages.push(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const invalidInput = document.querySelector(`${VERIFICATION_CODE_INPUT_SELECTOR}[aria-invalid="true"], ${VERIFICATION_CODE_INPUT_SELECTOR}[data-invalid="true"]`);
|
||||
if (invalidInput) {
|
||||
const wrapper = invalidInput.closest('form, [data-rac], ._root_18qcl_51, div');
|
||||
if (wrapper) {
|
||||
const text = (wrapper.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
if (text) {
|
||||
messages.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages.find((text) => INVALID_VERIFICATION_CODE_PATTERN.test(text)) || '';
|
||||
}
|
||||
|
||||
function isStep5Ready() {
|
||||
return Boolean(
|
||||
document.querySelector('input[name="name"], input[autocomplete="name"], input[name="birthday"], input[name="age"], [role="spinbutton"][data-type="year"]')
|
||||
);
|
||||
}
|
||||
|
||||
function getPageTextSnapshot() {
|
||||
return (document.body?.innerText || document.body?.textContent || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getPrimaryContinueButton() {
|
||||
const continueBtn = document.querySelector(
|
||||
'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107'
|
||||
);
|
||||
if (continueBtn && isVisibleElement(continueBtn)) {
|
||||
return continueBtn;
|
||||
}
|
||||
|
||||
const buttons = document.querySelectorAll('button, [role="button"]');
|
||||
return Array.from(buttons).find((el) => isVisibleElement(el) && /继续|Continue/i.test(el.textContent || '')) || null;
|
||||
}
|
||||
|
||||
function isVerificationPageStillVisible() {
|
||||
if (getVerificationCodeTarget()) return true;
|
||||
if (findResendVerificationCodeTrigger({ allowDisabled: true })) return true;
|
||||
if (document.querySelector('form[action*="email-verification" i]')) return true;
|
||||
|
||||
return VERIFICATION_PAGE_PATTERN.test(getPageTextSnapshot());
|
||||
}
|
||||
|
||||
function isAddPhonePageReady() {
|
||||
const path = `${location.pathname || ''} ${location.href || ''}`;
|
||||
if (/\/add-phone(?:[/?#]|$)/i.test(path)) return true;
|
||||
|
||||
const phoneInput = document.querySelector(
|
||||
'input[type="tel"]:not([maxlength="6"]), input[name*="phone" i], input[id*="phone" i], input[autocomplete="tel"]'
|
||||
);
|
||||
if (phoneInput && isVisibleElement(phoneInput)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ADD_PHONE_PAGE_PATTERN.test(getPageTextSnapshot());
|
||||
}
|
||||
|
||||
function isStep8Ready() {
|
||||
const continueBtn = getPrimaryContinueButton();
|
||||
if (!continueBtn) return false;
|
||||
if (isVerificationPageStillVisible()) return false;
|
||||
if (isAddPhonePageReady()) return false;
|
||||
|
||||
return OAUTH_CONSENT_PAGE_PATTERN.test(getPageTextSnapshot());
|
||||
}
|
||||
|
||||
async function waitForVerificationSubmitOutcome(step, timeout) {
|
||||
const resolvedTimeout = timeout ?? (step === 7 ? 30000 : 12000);
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start < resolvedTimeout) {
|
||||
throwIfStopped();
|
||||
|
||||
const errorText = getVerificationErrorText();
|
||||
if (errorText) {
|
||||
return { invalidCode: true, errorText };
|
||||
}
|
||||
|
||||
if (step === 4 && isStep5Ready()) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
if (step === 7 && isStep8Ready()) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
if (step === 7 && isAddPhonePageReady()) {
|
||||
return { success: true, addPhonePage: true };
|
||||
}
|
||||
|
||||
await sleep(150);
|
||||
}
|
||||
|
||||
if (isVerificationPageStillVisible()) {
|
||||
return {
|
||||
invalidCode: true,
|
||||
errorText: getVerificationErrorText() || '提交后仍停留在验证码页面,准备重新发送验证码。',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, assumed: true };
|
||||
}
|
||||
|
||||
async function fillVerificationCode(step, payload) {
|
||||
const { code } = payload;
|
||||
if (!code) throw new Error('No verification code provided.');
|
||||
if (!code) throw new Error('未提供验证码。');
|
||||
|
||||
log(`Step ${step}: Filling verification code: ${code}`);
|
||||
log(`步骤 ${step}:正在填写验证码:${code}`);
|
||||
|
||||
if (step === 7) {
|
||||
await prepareLoginCodeFlow();
|
||||
}
|
||||
|
||||
// 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
|
||||
);
|
||||
codeInput = await waitForElement(VERIFICATION_CODE_INPUT_SELECTOR, 10000);
|
||||
} catch {
|
||||
// Check for multiple single-digit inputs (common pattern)
|
||||
const singleInputs = document.querySelectorAll('input[maxlength="1"]');
|
||||
if (singleInputs.length >= 6) {
|
||||
log(`Step ${step}: Found single-digit code inputs, filling individually...`);
|
||||
log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`);
|
||||
for (let i = 0; i < 6 && i < singleInputs.length; i++) {
|
||||
fillInput(singleInputs[i], code[i]);
|
||||
await sleep(100);
|
||||
}
|
||||
await sleep(1000);
|
||||
reportComplete(step);
|
||||
return;
|
||||
const outcome = await waitForVerificationSubmitOutcome(step);
|
||||
if (outcome.invalidCode) {
|
||||
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
|
||||
} else if (outcome.addPhonePage) {
|
||||
log(`步骤 ${step}:验证码已通过,并已跳转到手机号页面。`, 'ok');
|
||||
} else {
|
||||
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
|
||||
}
|
||||
return outcome;
|
||||
}
|
||||
throw new Error('Could not find verification code input. URL: ' + location.href);
|
||||
throw new Error('未找到验证码输入框。URL: ' + location.href);
|
||||
}
|
||||
|
||||
fillInput(codeInput, code);
|
||||
log(`Step ${step}: Code filled`);
|
||||
log(`步骤 ${step}:验证码已填写`);
|
||||
|
||||
// Report complete BEFORE submit (page may navigate away)
|
||||
reportComplete(step);
|
||||
|
||||
// Submit
|
||||
await sleep(500);
|
||||
@@ -196,8 +529,19 @@ async function fillVerificationCode(step, payload) {
|
||||
if (submitBtn) {
|
||||
await humanPause(450, 1200);
|
||||
simulateClick(submitBtn);
|
||||
log(`Step ${step}: Verification submitted`);
|
||||
log(`步骤 ${step}:验证码已提交`);
|
||||
}
|
||||
|
||||
const outcome = await waitForVerificationSubmitOutcome(step);
|
||||
if (outcome.invalidCode) {
|
||||
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
|
||||
} else if (outcome.addPhonePage) {
|
||||
log(`步骤 ${step}:验证码已通过,并已跳转到手机号页面。`, 'ok');
|
||||
} else {
|
||||
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
|
||||
}
|
||||
|
||||
return outcome;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -206,9 +550,9 @@ async function fillVerificationCode(step, payload) {
|
||||
|
||||
async function step6_login(payload) {
|
||||
const { email, password } = payload;
|
||||
if (!email) throw new Error('No email provided for login.');
|
||||
if (!email) throw new Error('登录时缺少邮箱地址。');
|
||||
|
||||
log(`Step 6: Logging in with ${email}...`);
|
||||
log(`步骤 6:正在使用 ${email} 登录...`);
|
||||
|
||||
// Wait for email input on the auth page
|
||||
let emailInput = null;
|
||||
@@ -218,12 +562,12 @@ async function step6_login(payload) {
|
||||
15000
|
||||
);
|
||||
} catch {
|
||||
throw new Error('Could not find email input on login page. URL: ' + location.href);
|
||||
throw new Error('在登录页未找到邮箱输入框。URL: ' + location.href);
|
||||
}
|
||||
|
||||
await humanPause(500, 1400);
|
||||
fillInput(emailInput, email);
|
||||
log('Step 6: Email filled');
|
||||
log('步骤 6:邮箱已填写');
|
||||
|
||||
// Submit email
|
||||
await sleep(500);
|
||||
@@ -232,7 +576,7 @@ async function step6_login(payload) {
|
||||
if (submitBtn1) {
|
||||
await humanPause(400, 1100);
|
||||
simulateClick(submitBtn1);
|
||||
log('Step 6: Submitted email');
|
||||
log('步骤 6:邮箱已提交');
|
||||
}
|
||||
|
||||
await sleep(2000);
|
||||
@@ -240,7 +584,7 @@ async function step6_login(payload) {
|
||||
// Check for password field
|
||||
const passwordInput = document.querySelector('input[type="password"]');
|
||||
if (passwordInput) {
|
||||
log('Step 6: Password field found, filling password...');
|
||||
log('步骤 6:已找到密码输入框,正在填写密码...');
|
||||
await humanPause(550, 1450);
|
||||
fillInput(passwordInput, password);
|
||||
|
||||
@@ -253,13 +597,13 @@ async function step6_login(payload) {
|
||||
if (submitBtn2) {
|
||||
await humanPause(450, 1200);
|
||||
simulateClick(submitBtn2);
|
||||
log('Step 6: Submitted password, may need verification code (step 7)');
|
||||
log('步骤 6:密码已提交,可能还需要验证码(步骤 7)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No password field — OTP flow
|
||||
log('Step 6: No password field. OTP flow or auto-redirect.');
|
||||
log('步骤 6:未发现密码输入框,可能进入验证码流程或自动跳转。');
|
||||
reportComplete(6, { needsOTP: true });
|
||||
}
|
||||
|
||||
@@ -271,7 +615,7 @@ async function step6_login(payload) {
|
||||
// Background performs the actual click through the debugger Input API.
|
||||
|
||||
async function step8_findAndClick() {
|
||||
log('Step 8: Looking for OAuth consent "继续" button...');
|
||||
log('步骤 8:正在查找 OAuth 同意页的“继续”按钮...');
|
||||
|
||||
const continueBtn = await findContinueButton();
|
||||
await waitForButtonEnabled(continueBtn);
|
||||
@@ -282,7 +626,7 @@ async function step8_findAndClick() {
|
||||
await sleep(250);
|
||||
|
||||
const rect = getSerializableRect(continueBtn);
|
||||
log('Step 8: Found "继续" button and prepared debugger click coordinates.');
|
||||
log('步骤 8:已找到“继续”按钮并准备好调试器点击坐标。');
|
||||
return {
|
||||
rect,
|
||||
buttonText: (continueBtn.textContent || '').trim(),
|
||||
@@ -291,18 +635,20 @@ async function step8_findAndClick() {
|
||||
}
|
||||
|
||||
async function findContinueButton() {
|
||||
try {
|
||||
return await waitForElement(
|
||||
'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
|
||||
10000
|
||||
);
|
||||
} catch {
|
||||
try {
|
||||
return await waitForElementByText('button', /继续|Continue/, 5000);
|
||||
} catch {
|
||||
throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href);
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < 10000) {
|
||||
throwIfStopped();
|
||||
if (isAddPhonePageReady()) {
|
||||
throw new Error('当前页面已进入手机号页面,不是 OAuth 授权同意页。URL: ' + location.href);
|
||||
}
|
||||
const button = getPrimaryContinueButton();
|
||||
if (button && isStep8Ready()) {
|
||||
return button;
|
||||
}
|
||||
await sleep(150);
|
||||
}
|
||||
|
||||
throw new Error('在 OAuth 同意页未找到“继续”按钮,或页面尚未进入授权同意状态。URL: ' + location.href);
|
||||
}
|
||||
|
||||
async function waitForButtonEnabled(button, timeout = 8000) {
|
||||
@@ -312,7 +658,7 @@ async function waitForButtonEnabled(button, timeout = 8000) {
|
||||
if (isButtonEnabled(button)) return;
|
||||
await sleep(150);
|
||||
}
|
||||
throw new Error('"继续" button stayed disabled for too long. URL: ' + location.href);
|
||||
throw new Error('“继续”按钮长时间不可点击。URL: ' + location.href);
|
||||
}
|
||||
|
||||
function isButtonEnabled(button) {
|
||||
@@ -324,7 +670,7 @@ function isButtonEnabled(button) {
|
||||
function getSerializableRect(el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (!rect.width || !rect.height) {
|
||||
throw new Error('"继续" button has no clickable size after scrolling. URL: ' + location.href);
|
||||
throw new Error('滚动后“继续”按钮没有可点击尺寸。URL: ' + location.href);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -343,16 +689,16 @@ function getSerializableRect(el) {
|
||||
|
||||
async function step5_fillNameBirthday(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('未提供姓名数据。');
|
||||
|
||||
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.');
|
||||
throw new Error('未提供生日或年龄数据。');
|
||||
}
|
||||
|
||||
const fullName = `${firstName} ${lastName}`;
|
||||
log(`Step 5: Filling name: ${fullName}`);
|
||||
log(`步骤 5:正在填写姓名:${fullName}`);
|
||||
|
||||
// Actual DOM structure:
|
||||
// - Full name: <input name="name" placeholder="全名" type="text">
|
||||
@@ -367,11 +713,11 @@ async function step5_fillNameBirthday(payload) {
|
||||
10000
|
||||
);
|
||||
} catch {
|
||||
throw new Error('Could not find name input. URL: ' + location.href);
|
||||
throw new Error('未找到姓名输入框。URL: ' + location.href);
|
||||
}
|
||||
await humanPause(500, 1300);
|
||||
fillInput(nameInput, fullName);
|
||||
log(`Step 5: Name filled: ${fullName}`);
|
||||
log(`步骤 5:姓名已填写:${fullName}`);
|
||||
|
||||
let birthdayMode = false;
|
||||
let ageInput = null;
|
||||
@@ -393,7 +739,7 @@ async function step5_fillNameBirthday(payload) {
|
||||
|
||||
if (birthdayMode) {
|
||||
if (!hasBirthdayData) {
|
||||
throw new Error('Birthday field detected, but no birthday data provided.');
|
||||
throw new Error('检测到生日字段,但未提供生日数据。');
|
||||
}
|
||||
|
||||
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
|
||||
@@ -401,7 +747,7 @@ async function step5_fillNameBirthday(payload) {
|
||||
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
|
||||
|
||||
if (yearSpinner && monthSpinner && daySpinner) {
|
||||
log('Step 5: Birthday fields detected, filling birthday...');
|
||||
log('步骤 5:检测到生日字段,正在填写生日...');
|
||||
|
||||
async function setSpinButton(el, value) {
|
||||
el.focus();
|
||||
@@ -429,7 +775,7 @@ async function step5_fillNameBirthday(payload) {
|
||||
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')}`);
|
||||
log(`步骤 5:生日已填写:${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
|
||||
}
|
||||
|
||||
const hiddenBirthday = document.querySelector('input[name="birthday"]');
|
||||
@@ -437,17 +783,17 @@ async function step5_fillNameBirthday(payload) {
|
||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
hiddenBirthday.value = dateStr;
|
||||
hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
log(`Step 5: Hidden birthday input set: ${dateStr}`);
|
||||
log(`步骤 5:已设置隐藏生日输入框:${dateStr}`);
|
||||
}
|
||||
} else if (ageInput) {
|
||||
if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) {
|
||||
throw new Error('Age field detected, but no age data provided.');
|
||||
throw new Error('检测到年龄字段,但未提供年龄数据。');
|
||||
}
|
||||
await humanPause(500, 1300);
|
||||
fillInput(ageInput, String(resolvedAge));
|
||||
log(`Step 5: Age filled: ${resolvedAge}`);
|
||||
log(`步骤 5:年龄已填写:${resolvedAge}`);
|
||||
} else {
|
||||
throw new Error('Could not find birthday or age input. URL: ' + location.href);
|
||||
throw new Error('未找到生日或年龄输入项。URL: ' + location.href);
|
||||
}
|
||||
|
||||
// Click "完成帐户创建" button
|
||||
@@ -461,6 +807,6 @@ async function step5_fillNameBirthday(payload) {
|
||||
if (completeBtn) {
|
||||
await humanPause(500, 1300);
|
||||
simulateClick(completeBtn);
|
||||
log('Step 5: Clicked "完成帐户创建"');
|
||||
log('步骤 5:已点击“完成帐户创建”');
|
||||
}
|
||||
}
|
||||
|
||||
+26
-26
@@ -13,7 +13,7 @@ const SCRIPT_SOURCE = (() => {
|
||||
})();
|
||||
|
||||
const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
|
||||
const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
let flowStopped = false;
|
||||
|
||||
chrome.runtime.onMessage.addListener((message) => {
|
||||
@@ -50,14 +50,14 @@ function waitForElement(selector, timeout = 10000) {
|
||||
|
||||
const existing = document.querySelector(selector);
|
||||
if (existing) {
|
||||
console.log(LOG_PREFIX, `Found immediately: ${selector}`);
|
||||
log(`Found element: ${selector}`);
|
||||
console.log(LOG_PREFIX, `立即找到元素: ${selector}`);
|
||||
log(`已找到元素:${selector}`);
|
||||
resolve(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(LOG_PREFIX, `Waiting for: ${selector} (timeout: ${timeout}ms)`);
|
||||
log(`Waiting for selector: ${selector}...`);
|
||||
console.log(LOG_PREFIX, `等待元素: ${selector}(超时 ${timeout}ms)`);
|
||||
log(`正在等待选择器:${selector}...`);
|
||||
|
||||
let settled = false;
|
||||
let stopTimer = null;
|
||||
@@ -78,8 +78,8 @@ function waitForElement(selector, timeout = 10000) {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) {
|
||||
cleanup();
|
||||
console.log(LOG_PREFIX, `Found after wait: ${selector}`);
|
||||
log(`Found element: ${selector}`);
|
||||
console.log(LOG_PREFIX, `等待后找到元素: ${selector}`);
|
||||
log(`已找到元素:${selector}`);
|
||||
resolve(el);
|
||||
}
|
||||
});
|
||||
@@ -91,7 +91,7 @@ function waitForElement(selector, timeout = 10000) {
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
cleanup();
|
||||
const msg = `Timeout waiting for ${selector} after ${timeout}ms on ${location.href}`;
|
||||
const msg = `在 ${location.href} 等待 ${selector} 超时,已超过 ${timeout}ms`;
|
||||
console.error(LOG_PREFIX, msg);
|
||||
reject(new Error(msg));
|
||||
}, timeout);
|
||||
@@ -132,14 +132,14 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
|
||||
|
||||
const existing = search();
|
||||
if (existing) {
|
||||
console.log(LOG_PREFIX, `Found by text immediately: ${containerSelector} matching ${textPattern}`);
|
||||
log(`Found element by text: ${textPattern}`);
|
||||
console.log(LOG_PREFIX, `立即按文本找到元素: ${containerSelector} 匹配 ${textPattern}`);
|
||||
log(`已按文本找到元素:${textPattern}`);
|
||||
resolve(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(LOG_PREFIX, `Waiting for text match: ${containerSelector} / ${textPattern}`);
|
||||
log(`Waiting for element with text: ${textPattern}...`);
|
||||
console.log(LOG_PREFIX, `等待文本匹配: ${containerSelector} / ${textPattern}`);
|
||||
log(`正在等待包含文本的元素:${textPattern}...`);
|
||||
|
||||
let settled = false;
|
||||
let stopTimer = null;
|
||||
@@ -160,8 +160,8 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
|
||||
const el = search();
|
||||
if (el) {
|
||||
cleanup();
|
||||
console.log(LOG_PREFIX, `Found by text after wait: ${textPattern}`);
|
||||
log(`Found element by text: ${textPattern}`);
|
||||
console.log(LOG_PREFIX, `等待后按文本找到元素: ${textPattern}`);
|
||||
log(`已按文本找到元素:${textPattern}`);
|
||||
resolve(el);
|
||||
}
|
||||
});
|
||||
@@ -173,7 +173,7 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
cleanup();
|
||||
const msg = `Timeout waiting for text "${textPattern}" in "${containerSelector}" after ${timeout}ms on ${location.href}`;
|
||||
const msg = `在 ${location.href} 的 ${containerSelector} 中等待文本 "${textPattern}" 超时,已超过 ${timeout}ms`;
|
||||
console.error(LOG_PREFIX, msg);
|
||||
reject(new Error(msg));
|
||||
}, timeout);
|
||||
@@ -206,8 +206,8 @@ function fillInput(el, value) {
|
||||
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'}]`);
|
||||
console.log(LOG_PREFIX, `已填写输入框 ${el.name || el.id || el.type}: ${value}`);
|
||||
log(`已填写输入框 [${el.name || el.id || el.type || '未知'}]`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,8 +219,8 @@ function fillSelect(el, value) {
|
||||
throwIfStopped();
|
||||
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}`);
|
||||
console.log(LOG_PREFIX, `已在 ${el.name || el.id} 中选择值: ${value}`);
|
||||
log(`已选择 [${el.name || el.id || '未知'}] = ${value}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,7 +242,7 @@ function log(message, level = 'info') {
|
||||
* Report that this content script is loaded and ready.
|
||||
*/
|
||||
function reportReady() {
|
||||
console.log(LOG_PREFIX, 'Content script ready');
|
||||
console.log(LOG_PREFIX, '内容脚本已就绪');
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'CONTENT_SCRIPT_READY',
|
||||
source: SCRIPT_SOURCE,
|
||||
@@ -258,8 +258,8 @@ function reportReady() {
|
||||
* @param {Object} data - Step output data
|
||||
*/
|
||||
function reportComplete(step, data = {}) {
|
||||
console.log(LOG_PREFIX, `Step ${step} completed`, data);
|
||||
log(`Step ${step} completed successfully`, 'ok');
|
||||
console.log(LOG_PREFIX, `步骤 ${step} 已完成`, data);
|
||||
log(`步骤 ${step} 已成功完成`, 'ok');
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'STEP_COMPLETE',
|
||||
source: SCRIPT_SOURCE,
|
||||
@@ -275,8 +275,8 @@ function reportComplete(step, data = {}) {
|
||||
* @param {string} errorMessage
|
||||
*/
|
||||
function reportError(step, errorMessage) {
|
||||
console.error(LOG_PREFIX, `Step ${step} failed: ${errorMessage}`);
|
||||
log(`Step ${step} failed: ${errorMessage}`, 'error');
|
||||
console.error(LOG_PREFIX, `步骤 ${step} 失败: ${errorMessage}`);
|
||||
log(`步骤 ${step} 失败:${errorMessage}`, 'error');
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'STEP_ERROR',
|
||||
source: SCRIPT_SOURCE,
|
||||
@@ -293,8 +293,8 @@ function reportError(step, errorMessage) {
|
||||
function simulateClick(el) {
|
||||
throwIfStopped();
|
||||
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) || ''}"`);
|
||||
console.log(LOG_PREFIX, `已点击: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
|
||||
log(`已点击 [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+188
-52
@@ -33,7 +33,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
sendResponse({ ok: true });
|
||||
}).catch(err => {
|
||||
if (isStopError(err)) {
|
||||
log(`Step ${message.step}: Stopped by user.`, 'warn');
|
||||
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
@@ -46,65 +46,199 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
|
||||
async function handleStep(step, payload) {
|
||||
switch (step) {
|
||||
case 1: return await step1_getOAuthLink();
|
||||
case 1: return await step1_getOAuthLink(payload);
|
||||
case 9: return await step9_vpsVerify(payload);
|
||||
default:
|
||||
throw new Error(`vps-panel.js does not handle step ${step}`);
|
||||
throw new Error(`vps-panel.js 不处理步骤 ${step}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isVisibleElement(el) {
|
||||
if (!el) return false;
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return style.display !== 'none'
|
||||
&& style.visibility !== 'hidden'
|
||||
&& rect.width > 0
|
||||
&& rect.height > 0;
|
||||
}
|
||||
|
||||
function getActionText(el) {
|
||||
return [
|
||||
el?.textContent,
|
||||
el?.value,
|
||||
el?.getAttribute?.('aria-label'),
|
||||
el?.getAttribute?.('title'),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function findManagementKeyInput() {
|
||||
const candidates = document.querySelectorAll(
|
||||
'.LoginPage-module__loginCard___OgP-R input[type="password"], input[placeholder*="管理密钥"], input[aria-label*="管理密钥"]'
|
||||
);
|
||||
return Array.from(candidates).find(isVisibleElement) || null;
|
||||
}
|
||||
|
||||
function findManagementLoginButton() {
|
||||
const candidates = document.querySelectorAll('.LoginPage-module__loginCard___OgP-R button, .LoginPage-module__loginCard___OgP-R .btn');
|
||||
return Array.from(candidates).find((el) => {
|
||||
if (!isVisibleElement(el)) return false;
|
||||
return /登录|login/i.test(getActionText(el));
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findRememberPasswordCheckbox() {
|
||||
const candidates = document.querySelectorAll('.LoginPage-module__loginCard___OgP-R input[type="checkbox"]');
|
||||
return Array.from(candidates).find((el) => {
|
||||
const label = el.closest('label');
|
||||
const text = getActionText(label || el);
|
||||
return /记住密码|remember/i.test(text);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findOAuthNavLink() {
|
||||
const candidates = document.querySelectorAll('a[href*="#/oauth"], a.nav-item, button, [role="link"], [role="button"]');
|
||||
return Array.from(candidates).find((el) => {
|
||||
if (!isVisibleElement(el)) return false;
|
||||
const text = getActionText(el);
|
||||
const href = el.getAttribute('href') || '';
|
||||
return href.includes('#/oauth') || /oauth/i.test(text);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findCodexOAuthHeader() {
|
||||
const candidates = document.querySelectorAll('.card-header, [class*="cardHeader"], .card, [class*="card"]');
|
||||
return Array.from(candidates).find((el) => {
|
||||
if (!isVisibleElement(el)) return false;
|
||||
const text = (el.textContent || '').toLowerCase();
|
||||
return text.includes('codex') && text.includes('oauth');
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findOAuthCardLoginButton(header) {
|
||||
const card = header?.closest('.card, [class*="card"]') || header?.parentElement || document;
|
||||
const candidates = card.querySelectorAll('button.btn.btn-primary, button.btn-primary, button.btn');
|
||||
return Array.from(candidates).find((el) => isVisibleElement(el) && /登录|login/i.test(getActionText(el))) || null;
|
||||
}
|
||||
|
||||
function findAuthUrlElement() {
|
||||
const candidates = document.querySelectorAll('[class*="authUrlValue"], .OAuthPage-module__authUrlValue___axvUJ');
|
||||
return Array.from(candidates).find((el) => isVisibleElement(el) && /^https?:\/\//i.test((el.textContent || '').trim())) || null;
|
||||
}
|
||||
|
||||
async function ensureOAuthManagementPage(vpsPassword, step = 1, timeout = 45000) {
|
||||
const start = Date.now();
|
||||
let lastLoginAttemptAt = 0;
|
||||
let lastOauthNavAttemptAt = 0;
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
|
||||
const authUrlEl = findAuthUrlElement();
|
||||
if (authUrlEl) {
|
||||
return { header: findCodexOAuthHeader(), authUrlEl };
|
||||
}
|
||||
|
||||
const oauthHeader = findCodexOAuthHeader();
|
||||
if (oauthHeader) {
|
||||
return { header: oauthHeader, authUrlEl: null };
|
||||
}
|
||||
|
||||
const managementKeyInput = findManagementKeyInput();
|
||||
const managementLoginButton = findManagementLoginButton();
|
||||
if (managementKeyInput && managementLoginButton) {
|
||||
if (!vpsPassword) {
|
||||
throw new Error('VPS 面板需要管理密钥,请先在侧边栏填写 VPS Key(管理密钥)。');
|
||||
}
|
||||
|
||||
if ((managementKeyInput.value || '') !== vpsPassword) {
|
||||
await humanPause(350, 900);
|
||||
fillInput(managementKeyInput, vpsPassword);
|
||||
log(`步骤 ${step}:已填写 VPS 管理密钥。`);
|
||||
}
|
||||
|
||||
const rememberCheckbox = findRememberPasswordCheckbox();
|
||||
if (rememberCheckbox && !rememberCheckbox.checked) {
|
||||
simulateClick(rememberCheckbox);
|
||||
log(`步骤 ${step}:已勾选 VPS 面板“记住密码”。`);
|
||||
await sleep(300);
|
||||
}
|
||||
|
||||
if (Date.now() - lastLoginAttemptAt > 3000) {
|
||||
lastLoginAttemptAt = Date.now();
|
||||
await humanPause(350, 900);
|
||||
simulateClick(managementLoginButton);
|
||||
log(`步骤 ${step}:已提交 VPS 管理登录。`);
|
||||
}
|
||||
|
||||
await sleep(1500);
|
||||
continue;
|
||||
}
|
||||
|
||||
const oauthNavLink = findOAuthNavLink();
|
||||
if (oauthNavLink && Date.now() - lastOauthNavAttemptAt > 2000) {
|
||||
lastOauthNavAttemptAt = Date.now();
|
||||
await humanPause(300, 800);
|
||||
simulateClick(oauthNavLink);
|
||||
log(`步骤 ${step}:已打开“OAuth 登录”导航。`);
|
||||
await sleep(1200);
|
||||
continue;
|
||||
}
|
||||
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
throw new Error('无法进入 VPS 的 OAuth 管理页面,请检查面板是否正常加载。URL: ' + location.href);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Step 1: Get OAuth Link
|
||||
// ============================================================
|
||||
|
||||
async function step1_getOAuthLink() {
|
||||
log('Step 1: Waiting for VPS panel to load (auto-login may take a moment)...');
|
||||
async function step1_getOAuthLink(payload) {
|
||||
const { vpsPassword } = payload || {};
|
||||
|
||||
// The page may start at #/login and auto-redirect to #/oauth.
|
||||
// Wait for the Codex OAuth card to appear (up to 30s for auto-login + redirect).
|
||||
let loginBtn = null;
|
||||
try {
|
||||
// Wait for any card-header containing "Codex" to appear
|
||||
const header = await waitForElementByText('.card-header', /codex/i, 30000);
|
||||
loginBtn = header.querySelector('button.btn.btn-primary, button.btn');
|
||||
log('Step 1: Found Codex OAuth card');
|
||||
} catch {
|
||||
throw new Error(
|
||||
'Codex OAuth card did not appear after 30s. Page may still be loading or not logged in. ' +
|
||||
'Current URL: ' + location.href
|
||||
);
|
||||
}
|
||||
log('步骤 1:正在等待 VPS 面板加载并进入 OAuth 页面...');
|
||||
|
||||
if (!loginBtn) {
|
||||
throw new Error('Found Codex OAuth card but no login button inside it. URL: ' + location.href);
|
||||
}
|
||||
const { header, authUrlEl: existingAuthUrlEl } = await ensureOAuthManagementPage(vpsPassword, 1);
|
||||
let authUrlEl = existingAuthUrlEl;
|
||||
|
||||
// Check if button is disabled (already clicked / loading)
|
||||
if (loginBtn.disabled) {
|
||||
log('Step 1: Login button is disabled (already loading), waiting for auth URL...');
|
||||
if (!authUrlEl) {
|
||||
const loginBtn = findOAuthCardLoginButton(header);
|
||||
if (!loginBtn) {
|
||||
throw new Error('已找到 Codex OAuth 卡片,但卡片内没有登录按钮。URL: ' + location.href);
|
||||
}
|
||||
|
||||
if (loginBtn.disabled) {
|
||||
log('步骤 1:OAuth 登录按钮当前不可用,正在等待授权链接出现...');
|
||||
} else {
|
||||
await humanPause(500, 1400);
|
||||
simulateClick(loginBtn);
|
||||
log('步骤 1:已点击 OAuth 登录按钮,正在等待授权链接...');
|
||||
}
|
||||
|
||||
try {
|
||||
authUrlEl = await waitForElement('[class*="authUrlValue"]', 15000);
|
||||
} catch {
|
||||
throw new Error(
|
||||
'点击 OAuth 登录按钮后未出现授权链接。' +
|
||||
'请检查 VPS 面板服务是否正在运行。URL: ' + location.href
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await humanPause(500, 1400);
|
||||
simulateClick(loginBtn);
|
||||
log('Step 1: Clicked login button, waiting for auth URL...');
|
||||
}
|
||||
|
||||
// Wait for the auth URL to appear in the specific div
|
||||
let authUrlEl = null;
|
||||
try {
|
||||
authUrlEl = await waitForElement('[class*="authUrlValue"]', 15000);
|
||||
} catch {
|
||||
throw new Error(
|
||||
'Auth URL did not appear after clicking login. ' +
|
||||
'Check if VPS panel is logged in and Codex service is running. URL: ' + location.href
|
||||
);
|
||||
log('步骤 1:VPS 面板上已显示授权链接。');
|
||||
}
|
||||
|
||||
const oauthUrl = (authUrlEl.textContent || '').trim();
|
||||
if (!oauthUrl || !oauthUrl.startsWith('http')) {
|
||||
throw new Error(`Invalid OAuth URL found: "${oauthUrl.slice(0, 50)}". Expected URL starting with http.`);
|
||||
throw new Error(`拿到的 OAuth 链接无效:\"${oauthUrl.slice(0, 50)}\"。应为 http 开头的 URL。`);
|
||||
}
|
||||
|
||||
log(`Step 1: OAuth URL obtained: ${oauthUrl.slice(0, 80)}...`, 'ok');
|
||||
log(`步骤 1:已获取 OAuth 链接:${oauthUrl.slice(0, 80)}...`, 'ok');
|
||||
reportComplete(1, { oauthUrl });
|
||||
}
|
||||
|
||||
@@ -113,19 +247,21 @@ async function step1_getOAuthLink() {
|
||||
// ============================================================
|
||||
|
||||
async function step9_vpsVerify(payload) {
|
||||
await ensureOAuthManagementPage(payload?.vpsPassword, 9);
|
||||
|
||||
// Get localhostUrl from payload (passed directly by background) or fallback to state
|
||||
let localhostUrl = payload?.localhostUrl;
|
||||
if (!localhostUrl) {
|
||||
log('Step 9: localhostUrl not in payload, fetching from state...');
|
||||
log('步骤 9:payload 中没有 localhostUrl,正在从状态中读取...');
|
||||
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
|
||||
localhostUrl = state.localhostUrl;
|
||||
}
|
||||
if (!localhostUrl) {
|
||||
throw new Error('No localhost URL found. Complete step 8 first.');
|
||||
throw new Error('未找到 localhost 回调地址,请先完成步骤 8。');
|
||||
}
|
||||
log(`Step 9: Got localhostUrl: ${localhostUrl.slice(0, 60)}...`);
|
||||
log(`步骤 9:已获取 localhostUrl:${localhostUrl.slice(0, 60)}...`);
|
||||
|
||||
log('Step 9: Looking for callback URL input...');
|
||||
log('步骤 9:正在查找回调地址输入框...');
|
||||
|
||||
// Find the callback URL input
|
||||
// Actual DOM: <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=...">
|
||||
@@ -136,13 +272,13 @@ async function step9_vpsVerify(payload) {
|
||||
try {
|
||||
urlInput = await waitForElement('input[placeholder*="localhost"]', 5000);
|
||||
} catch {
|
||||
throw new Error('Could not find callback URL input on VPS panel. URL: ' + location.href);
|
||||
throw new Error('在 VPS 面板中未找到回调地址输入框。URL: ' + location.href);
|
||||
}
|
||||
}
|
||||
|
||||
await humanPause(600, 1500);
|
||||
fillInput(urlInput, localhostUrl);
|
||||
log(`Step 9: Filled callback URL: ${localhostUrl.slice(0, 80)}...`);
|
||||
log(`步骤 9:已填写回调地址:${localhostUrl.slice(0, 80)}...`);
|
||||
|
||||
// Find and click "提交回调 URL" button
|
||||
let submitBtn = null;
|
||||
@@ -156,26 +292,26 @@ async function step9_vpsVerify(payload) {
|
||||
try {
|
||||
submitBtn = await waitForElementByText('button.btn', /提交回调/, 5000);
|
||||
} catch {
|
||||
throw new Error('Could not find "提交回调 URL" button. URL: ' + location.href);
|
||||
throw new Error('未找到“提交回调 URL”按钮。URL: ' + location.href);
|
||||
}
|
||||
}
|
||||
|
||||
await humanPause(450, 1200);
|
||||
simulateClick(submitBtn);
|
||||
log('Step 9: Clicked "提交回调 URL", waiting for authentication result...');
|
||||
log('步骤 9:已点击“提交回调 URL”,正在等待认证结果...');
|
||||
|
||||
// Wait for "认证成功!" status badge to appear
|
||||
try {
|
||||
await waitForElementByText('.status-badge, [class*="status"]', /认证成功/, 30000);
|
||||
log('Step 9: Authentication successful!', 'ok');
|
||||
log('步骤 9:认证成功!', 'ok');
|
||||
} catch {
|
||||
// Check if there's an error message instead
|
||||
const statusEl = document.querySelector('.status-badge, [class*="status"]');
|
||||
const statusText = statusEl ? statusEl.textContent : 'unknown';
|
||||
if (/成功|success/i.test(statusText)) {
|
||||
log('Step 9: Authentication successful!', 'ok');
|
||||
log('步骤 9:认证成功!', 'ok');
|
||||
} else {
|
||||
log(`Step 9: Status after submit: "${statusText}". May still be processing.`, 'warn');
|
||||
log(`步骤 9:提交后的状态为“${statusText}”,可能仍在处理中。`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Multi-Page Automation",
|
||||
"name": "多页面自动化",
|
||||
"version": "1.1.0",
|
||||
"description": "Automates multi-step OAuth registration workflow",
|
||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||
"permissions": [
|
||||
"sidePanel",
|
||||
"tabs",
|
||||
|
||||
@@ -244,6 +244,10 @@ header {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.data-check-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.data-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -323,6 +327,25 @@ header {
|
||||
.data-select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
|
||||
[data-theme="dark"] .data-select { color-scheme: dark; }
|
||||
|
||||
.data-check {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.data-check input[type="checkbox"] {
|
||||
margin-top: 2px;
|
||||
accent-color: var(--blue);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Status Bar */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
|
||||
+50
-39
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Multi-Page Automation</title>
|
||||
<title>多页面自动化面板</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
@@ -15,23 +15,23 @@
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||
</svg>
|
||||
<h1>MultiPage</h1>
|
||||
<h1>多页面</h1>
|
||||
</div>
|
||||
<div class="header-btns">
|
||||
<div class="run-group">
|
||||
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="Number of runs" />
|
||||
<button id="btn-auto-run" class="btn btn-success" title="Run all steps automatically">
|
||||
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="运行次数" />
|
||||
<button id="btn-auto-run" class="btn btn-success" title="自动执行全部步骤">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
Auto
|
||||
自动
|
||||
</button>
|
||||
<button id="btn-stop" class="btn btn-danger" title="Stop current flow" disabled>Stop</button>
|
||||
<button id="btn-stop" class="btn btn-danger" title="停止当前流程" disabled>停止</button>
|
||||
</div>
|
||||
<button id="btn-reset" class="btn btn-ghost" title="Reset all steps">
|
||||
<button id="btn-reset" class="btn btn-ghost" title="重置全部步骤">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="btn-theme" class="theme-toggle" title="Toggle theme">
|
||||
<button id="btn-theme" class="theme-toggle" title="切换主题">
|
||||
<svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
@@ -49,104 +49,115 @@
|
||||
<input type="password" id="input-vps-url" class="data-input" placeholder="http://ip:port/management.html#/oauth" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">Mail</span>
|
||||
<span class="data-label">管理密钥</span>
|
||||
<input type="password" id="input-vps-password" class="data-input" placeholder="请输入管理密钥" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">邮箱服务</span>
|
||||
<select id="select-mail-provider" class="data-select">
|
||||
<option value="163">163 Mail (mail.163.com)</option>
|
||||
<option value="qq">QQ Mail (wx.mail.qq.com)</option>
|
||||
<option value="inbucket">Inbucket (custom host)</option>
|
||||
<option value="163">163 邮箱 (mail.163.com)</option>
|
||||
<option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
|
||||
<option value="inbucket">Inbucket(自定义主机)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row" id="row-inbucket-host" style="display:none;">
|
||||
<span class="data-label">Inbucket</span>
|
||||
<input type="text" id="input-inbucket-host" class="data-input" placeholder="your-inbucket-host or https://your-inbucket-host" />
|
||||
<input type="text" id="input-inbucket-host" class="data-input" placeholder="填写主机或 https://主机地址" />
|
||||
</div>
|
||||
<div class="data-row" id="row-inbucket-mailbox" style="display:none;">
|
||||
<span class="data-label">Mailbox</span>
|
||||
<input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="e.g. zju2001" />
|
||||
<span class="data-label">邮箱名</span>
|
||||
<input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="例如 zju2001" />
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">Email</span>
|
||||
<span class="data-label">邮箱</span>
|
||||
<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>
|
||||
<input type="text" id="input-email" class="data-input" placeholder="粘贴 DuckDuckGo 邮箱" />
|
||||
<button id="btn-fetch-email" class="btn btn-outline btn-sm" type="button">获取</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">Password</span>
|
||||
<span class="data-label">密码</span>
|
||||
<div class="data-inline">
|
||||
<input type="password" id="input-password" class="data-input" placeholder="Leave blank to auto-generate" />
|
||||
<button id="btn-toggle-password" class="btn btn-outline btn-sm" type="button">Show</button>
|
||||
<input type="password" id="input-password" class="data-input" placeholder="留空则自动生成" />
|
||||
<button id="btn-toggle-password" class="btn btn-outline btn-sm" type="button">显示</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row data-check-row">
|
||||
<span class="data-label">兜底</span>
|
||||
<label class="data-check" for="input-auto-skip-failures">
|
||||
<input type="checkbox" id="input-auto-skip-failures" />
|
||||
<span>出现错误无法继续时,直接丢弃当前线程并新开一轮,直到补足目标运行次数</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">OAuth</span>
|
||||
<span id="display-oauth-url" class="data-value mono truncate">Waiting...</span>
|
||||
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">Callback</span>
|
||||
<span id="display-localhost-url" class="data-value mono truncate">Waiting...</span>
|
||||
<span class="data-label">回调</span>
|
||||
<span id="display-localhost-url" class="data-value mono truncate">等待中...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="status-bar" class="status-bar">
|
||||
<div class="status-dot"></div>
|
||||
<span id="display-status">Ready</span>
|
||||
<span id="display-status">就绪</span>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
<span class="auto-hint">先自动获取 Duck 邮箱,或手动粘贴邮箱后再继续</span>
|
||||
<button id="btn-auto-continue" class="btn btn-primary btn-sm">继续</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="steps-section">
|
||||
<div class="steps-header">
|
||||
<span class="section-label">Workflow</span>
|
||||
<span class="section-label">流程</span>
|
||||
<span id="steps-progress" class="steps-progress">0 / 9</span>
|
||||
</div>
|
||||
<div class="steps-list">
|
||||
<div class="step-row" data-step="1">
|
||||
<div class="step-indicator" data-step="1"><span class="step-num">1</span></div>
|
||||
<button class="step-btn" data-step="1">Get OAuth Link</button>
|
||||
<button class="step-btn" data-step="1">获取 OAuth 链接</button>
|
||||
<span class="step-status" data-step="1"></span>
|
||||
</div>
|
||||
<div class="step-row" data-step="2">
|
||||
<div class="step-indicator" data-step="2"><span class="step-num">2</span></div>
|
||||
<button class="step-btn" data-step="2">Open Signup</button>
|
||||
<button class="step-btn" data-step="2">打开注册页</button>
|
||||
<span class="step-status" data-step="2"></span>
|
||||
</div>
|
||||
<div class="step-row" data-step="3">
|
||||
<div class="step-indicator" data-step="3"><span class="step-num">3</span></div>
|
||||
<button class="step-btn" data-step="3">Fill Email / Password</button>
|
||||
<button class="step-btn" data-step="3">填写邮箱和密码</button>
|
||||
<span class="step-status" data-step="3"></span>
|
||||
</div>
|
||||
<div class="step-row" data-step="4">
|
||||
<div class="step-indicator" data-step="4"><span class="step-num">4</span></div>
|
||||
<button class="step-btn" data-step="4">Get Signup Code</button>
|
||||
<button class="step-btn" data-step="4">获取注册验证码</button>
|
||||
<span class="step-status" data-step="4"></span>
|
||||
</div>
|
||||
<div class="step-row" data-step="5">
|
||||
<div class="step-indicator" data-step="5"><span class="step-num">5</span></div>
|
||||
<button class="step-btn" data-step="5">Fill Name / Birthday</button>
|
||||
<button class="step-btn" data-step="5">填写姓名和生日</button>
|
||||
<span class="step-status" data-step="5"></span>
|
||||
</div>
|
||||
<div class="step-row" data-step="6">
|
||||
<div class="step-indicator" data-step="6"><span class="step-num">6</span></div>
|
||||
<button class="step-btn" data-step="6">Login via OAuth</button>
|
||||
<button class="step-btn" data-step="6">OAuth 登录</button>
|
||||
<span class="step-status" data-step="6"></span>
|
||||
</div>
|
||||
<div class="step-row" data-step="7">
|
||||
<div class="step-indicator" data-step="7"><span class="step-num">7</span></div>
|
||||
<button class="step-btn" data-step="7">Get Login Code</button>
|
||||
<button class="step-btn" data-step="7">获取登录验证码</button>
|
||||
<span class="step-status" data-step="7"></span>
|
||||
</div>
|
||||
<div class="step-row" data-step="8">
|
||||
<div class="step-indicator" data-step="8"><span class="step-num">8</span></div>
|
||||
<button class="step-btn" data-step="8">OAuth Auto Confirm</button>
|
||||
<button class="step-btn" data-step="8">自动确认 OAuth</button>
|
||||
<span class="step-status" data-step="8"></span>
|
||||
</div>
|
||||
<div class="step-row" data-step="9">
|
||||
<div class="step-indicator" data-step="9"><span class="step-num">9</span></div>
|
||||
<button class="step-btn" data-step="9">VPS Verify</button>
|
||||
<button class="step-btn" data-step="9">VPS 回调验证</button>
|
||||
<span class="step-status" data-step="9"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,8 +165,8 @@
|
||||
|
||||
<section id="log-section">
|
||||
<div class="log-header">
|
||||
<span class="section-label">Console</span>
|
||||
<button id="btn-clear-log" class="btn btn-ghost btn-xs" title="Clear log">Clear</button>
|
||||
<span class="section-label">日志</span>
|
||||
<button id="btn-clear-log" class="btn btn-ghost btn-xs" title="清空日志">清空</button>
|
||||
</div>
|
||||
<div id="log-area"></div>
|
||||
</section>
|
||||
|
||||
+76
-35
@@ -25,12 +25,14 @@ const btnAutoContinue = document.getElementById('btn-auto-continue');
|
||||
const autoContinueBar = document.getElementById('auto-continue-bar');
|
||||
const btnClearLog = document.getElementById('btn-clear-log');
|
||||
const inputVpsUrl = document.getElementById('input-vps-url');
|
||||
const inputVpsPassword = document.getElementById('input-vps-password');
|
||||
const selectMailProvider = document.getElementById('select-mail-provider');
|
||||
const rowInbucketHost = document.getElementById('row-inbucket-host');
|
||||
const inputInbucketHost = document.getElementById('input-inbucket-host');
|
||||
const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
|
||||
const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
|
||||
const inputRunCount = document.getElementById('input-run-count');
|
||||
const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
|
||||
|
||||
// ============================================================
|
||||
// Toast Notifications
|
||||
@@ -45,6 +47,13 @@ const TOAST_ICONS = {
|
||||
info: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
|
||||
};
|
||||
|
||||
const LOG_LEVEL_LABELS = {
|
||||
info: '信息',
|
||||
ok: '成功',
|
||||
warn: '警告',
|
||||
error: '错误',
|
||||
};
|
||||
|
||||
function showToast(message, type = 'error', duration = 4000) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
@@ -87,6 +96,9 @@ async function restoreState() {
|
||||
if (state.vpsUrl) {
|
||||
inputVpsUrl.value = state.vpsUrl;
|
||||
}
|
||||
if (state.vpsPassword) {
|
||||
inputVpsPassword.value = state.vpsPassword;
|
||||
}
|
||||
if (state.mailProvider) {
|
||||
selectMailProvider.value = state.mailProvider;
|
||||
}
|
||||
@@ -96,6 +108,7 @@ async function restoreState() {
|
||||
if (state.inbucketMailbox) {
|
||||
inputInbucketMailbox.value = state.inbucketMailbox;
|
||||
}
|
||||
inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures);
|
||||
|
||||
if (state.stepStatuses) {
|
||||
for (const [step, status] of Object.entries(state.stepStatuses)) {
|
||||
@@ -194,21 +207,21 @@ function updateStatusDisplay(state) {
|
||||
|
||||
const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
|
||||
if (running) {
|
||||
displayStatus.textContent = `Step ${running[0]} running...`;
|
||||
displayStatus.textContent = `步骤 ${running[0]} 运行中...`;
|
||||
statusBar.classList.add('running');
|
||||
return;
|
||||
}
|
||||
|
||||
const failed = Object.entries(state.stepStatuses).find(([, s]) => s === 'failed');
|
||||
if (failed) {
|
||||
displayStatus.textContent = `Step ${failed[0]} failed`;
|
||||
displayStatus.textContent = `步骤 ${failed[0]} 失败`;
|
||||
statusBar.classList.add('failed');
|
||||
return;
|
||||
}
|
||||
|
||||
const stopped = Object.entries(state.stepStatuses).find(([, s]) => s === 'stopped');
|
||||
if (stopped) {
|
||||
displayStatus.textContent = `Step ${stopped[0]} stopped`;
|
||||
displayStatus.textContent = `步骤 ${stopped[0]} 已停止`;
|
||||
statusBar.classList.add('stopped');
|
||||
return;
|
||||
}
|
||||
@@ -219,28 +232,28 @@ function updateStatusDisplay(state) {
|
||||
.sort((a, b) => b - a)[0];
|
||||
|
||||
if (lastCompleted === 9) {
|
||||
displayStatus.textContent = 'All steps completed!';
|
||||
displayStatus.textContent = '全部步骤已完成';
|
||||
statusBar.classList.add('completed');
|
||||
} else if (lastCompleted) {
|
||||
displayStatus.textContent = `Step ${lastCompleted} done`;
|
||||
displayStatus.textContent = `步骤 ${lastCompleted} 已完成`;
|
||||
} else {
|
||||
displayStatus.textContent = 'Ready';
|
||||
displayStatus.textContent = '就绪';
|
||||
}
|
||||
}
|
||||
|
||||
function appendLog(entry) {
|
||||
const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
|
||||
const levelLabel = entry.level.toUpperCase();
|
||||
const time = new Date(entry.timestamp).toLocaleTimeString('zh-CN', { hour12: false });
|
||||
const levelLabel = LOG_LEVEL_LABELS[entry.level] || entry.level;
|
||||
const line = document.createElement('div');
|
||||
line.className = `log-line log-${entry.level}`;
|
||||
|
||||
const stepMatch = entry.message.match(/Step (\d)/);
|
||||
const stepNum = stepMatch ? stepMatch[1] : null;
|
||||
const stepMatch = entry.message.match(/(?:Step\s+(\d+)|步骤\s*(\d+))/);
|
||||
const stepNum = stepMatch ? (stepMatch[1] || stepMatch[2]) : null;
|
||||
|
||||
let html = `<span class="log-time">${time}</span> `;
|
||||
html += `<span class="log-level log-level-${entry.level}">${levelLabel}</span> `;
|
||||
if (stepNum) {
|
||||
html += `<span class="log-step-tag step-${stepNum}">S${stepNum}</span>`;
|
||||
html += `<span class="log-step-tag step-${stepNum}">步${stepNum}</span>`;
|
||||
}
|
||||
html += `<span class="log-msg">${escapeHtml(entry.message)}</span>`;
|
||||
|
||||
@@ -256,7 +269,7 @@ function escapeHtml(text) {
|
||||
}
|
||||
|
||||
async function fetchDuckEmail() {
|
||||
const defaultLabel = 'Auto';
|
||||
const defaultLabel = '获取';
|
||||
btnFetchEmail.disabled = true;
|
||||
btnFetchEmail.textContent = '...';
|
||||
|
||||
@@ -271,14 +284,14 @@ async function fetchDuckEmail() {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (!response?.email) {
|
||||
throw new Error('Duck email was not returned.');
|
||||
throw new Error('未返回 Duck 邮箱。');
|
||||
}
|
||||
|
||||
inputEmail.value = response.email;
|
||||
showToast(`Fetched ${response.email}`, 'success', 2500);
|
||||
showToast(`已获取 ${response.email}`, 'success', 2500);
|
||||
return response.email;
|
||||
} catch (err) {
|
||||
showToast(`Auto fetch failed: ${err.message}`, 'error');
|
||||
showToast(`自动获取失败:${err.message}`, 'error');
|
||||
throw err;
|
||||
} finally {
|
||||
btnFetchEmail.disabled = false;
|
||||
@@ -287,7 +300,7 @@ async function fetchDuckEmail() {
|
||||
}
|
||||
|
||||
function syncPasswordToggleLabel() {
|
||||
btnTogglePassword.textContent = inputPassword.type === 'password' ? 'Show' : 'Hide';
|
||||
btnTogglePassword.textContent = inputPassword.type === 'password' ? '显示' : '隐藏';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -300,7 +313,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
|
||||
if (step === 3) {
|
||||
const email = inputEmail.value.trim();
|
||||
if (!email) {
|
||||
showToast('Please paste email address or use Auto first', 'warn');
|
||||
showToast('请先粘贴邮箱,或先点击获取。', 'warn');
|
||||
return;
|
||||
}
|
||||
await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
|
||||
@@ -322,7 +335,7 @@ btnTogglePassword.addEventListener('click', () => {
|
||||
btnStop.addEventListener('click', async () => {
|
||||
btnStop.disabled = true;
|
||||
await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} });
|
||||
showToast('Stopping current flow...', 'warn', 2000);
|
||||
showToast('正在停止当前流程...', 'warn', 2000);
|
||||
});
|
||||
|
||||
// Auto Run
|
||||
@@ -330,14 +343,21 @@ btnAutoRun.addEventListener('click', async () => {
|
||||
const totalRuns = parseInt(inputRunCount.value) || 1;
|
||||
btnAutoRun.disabled = true;
|
||||
inputRunCount.disabled = true;
|
||||
btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> Running...';
|
||||
await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel', payload: { totalRuns } });
|
||||
btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 运行中...';
|
||||
await chrome.runtime.sendMessage({
|
||||
type: 'AUTO_RUN',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
totalRuns,
|
||||
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
btnAutoContinue.addEventListener('click', async () => {
|
||||
const email = inputEmail.value.trim();
|
||||
if (!email) {
|
||||
showToast('Please fetch or paste DuckDuckGo email first!', 'warn');
|
||||
showToast('请先获取或粘贴 DuckDuckGo 邮箱。', 'warn');
|
||||
return;
|
||||
}
|
||||
autoContinueBar.style.display = 'none';
|
||||
@@ -346,21 +366,21 @@ btnAutoContinue.addEventListener('click', async () => {
|
||||
|
||||
// Reset
|
||||
btnReset.addEventListener('click', async () => {
|
||||
if (confirm('Reset all steps and data?')) {
|
||||
if (confirm('确认重置全部步骤和数据吗?')) {
|
||||
await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
|
||||
displayOauthUrl.textContent = 'Waiting...';
|
||||
displayOauthUrl.textContent = '等待中...';
|
||||
displayOauthUrl.classList.remove('has-value');
|
||||
displayLocalhostUrl.textContent = 'Waiting...';
|
||||
displayLocalhostUrl.textContent = '等待中...';
|
||||
displayLocalhostUrl.classList.remove('has-value');
|
||||
inputEmail.value = '';
|
||||
displayStatus.textContent = 'Ready';
|
||||
displayStatus.textContent = '就绪';
|
||||
statusBar.className = 'status-bar';
|
||||
logArea.innerHTML = '';
|
||||
document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
|
||||
document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
|
||||
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> 自动';
|
||||
autoContinueBar.style.display = 'none';
|
||||
updateStopButtonState(false);
|
||||
updateButtonStates();
|
||||
@@ -388,6 +408,14 @@ inputVpsUrl.addEventListener('change', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
inputVpsPassword.addEventListener('change', async () => {
|
||||
await chrome.runtime.sendMessage({
|
||||
type: 'SAVE_SETTING',
|
||||
source: 'sidepanel',
|
||||
payload: { vpsPassword: inputVpsPassword.value },
|
||||
});
|
||||
});
|
||||
|
||||
inputPassword.addEventListener('change', async () => {
|
||||
await chrome.runtime.sendMessage({
|
||||
type: 'SAVE_SETTING',
|
||||
@@ -420,6 +448,14 @@ inputInbucketHost.addEventListener('change', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
inputAutoSkipFailures.addEventListener('change', async () => {
|
||||
await chrome.runtime.sendMessage({
|
||||
type: 'SAVE_SETTING',
|
||||
source: 'sidepanel',
|
||||
payload: { autoRunSkipFailures: inputAutoSkipFailures.checked },
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Listen for Background broadcasts
|
||||
// ============================================================
|
||||
@@ -455,12 +491,12 @@ chrome.runtime.onMessage.addListener((message) => {
|
||||
|
||||
case 'AUTO_RUN_RESET': {
|
||||
// Full UI reset for next run
|
||||
displayOauthUrl.textContent = 'Waiting...';
|
||||
displayOauthUrl.textContent = '等待中...';
|
||||
displayOauthUrl.classList.remove('has-value');
|
||||
displayLocalhostUrl.textContent = 'Waiting...';
|
||||
displayLocalhostUrl.textContent = '等待中...';
|
||||
displayLocalhostUrl.classList.remove('has-value');
|
||||
inputEmail.value = '';
|
||||
displayStatus.textContent = 'Ready';
|
||||
displayStatus.textContent = '就绪';
|
||||
statusBar.className = 'status-bar';
|
||||
logArea.innerHTML = '';
|
||||
document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
|
||||
@@ -489,29 +525,34 @@ chrome.runtime.onMessage.addListener((message) => {
|
||||
}
|
||||
|
||||
case 'AUTO_RUN_STATUS': {
|
||||
const { phase, currentRun, totalRuns } = message.payload;
|
||||
const runLabel = totalRuns > 1 ? ` (${currentRun}/${totalRuns})` : '';
|
||||
const { phase, currentRun, totalRuns, attemptRun } = message.payload;
|
||||
const attemptLabel = attemptRun ? ` · 尝试${attemptRun}` : '';
|
||||
const runLabel = totalRuns > 1 ? ` (${currentRun}/${totalRuns}${attemptLabel})` : (attemptLabel ? ` (${attemptLabel.slice(3)})` : '');
|
||||
switch (phase) {
|
||||
case 'waiting_email':
|
||||
autoContinueBar.style.display = 'flex';
|
||||
btnAutoRun.innerHTML = `Paused${runLabel}`;
|
||||
btnAutoRun.innerHTML = `已暂停${runLabel}`;
|
||||
updateStopButtonState(true);
|
||||
break;
|
||||
case 'running':
|
||||
btnAutoRun.innerHTML = `Running${runLabel}`;
|
||||
btnAutoRun.innerHTML = `运行中${runLabel}`;
|
||||
updateStopButtonState(true);
|
||||
break;
|
||||
case 'retrying':
|
||||
btnAutoRun.innerHTML = `重试中${runLabel}`;
|
||||
updateStopButtonState(true);
|
||||
break;
|
||||
case 'complete':
|
||||
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> 自动';
|
||||
autoContinueBar.style.display = 'none';
|
||||
updateStopButtonState(false);
|
||||
break;
|
||||
case 'stopped':
|
||||
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> 自动';
|
||||
autoContinueBar.style.display = 'none';
|
||||
updateStopButtonState(false);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user