Auto 正在真正运行或重试时,步骤按钮和手动完成按钮不会放开。
Auto 处于暂停等待邮箱时,可以继续,也可以显式接管为手动。 Step 1 和 Step 9 不提供手动完成同步。 Step 3 会要求已知邮箱和密码;Step 8 会要求已经捕获 localhostUrl。
This commit is contained in:
@@ -158,6 +158,8 @@ Step 3 使用的注册邮箱。
|
|||||||
|
|
||||||
- 如果 Duck 邮箱可自动获取,整套流程更接近全自动
|
- 如果 Duck 邮箱可自动获取,整套流程更接近全自动
|
||||||
- 如果不能自动获取,Auto 会在邮箱阶段暂停
|
- 如果不能自动获取,Auto 会在邮箱阶段暂停
|
||||||
|
- Auto 的暂停状态会保存在会话状态中,重新打开侧边栏后仍可继续
|
||||||
|
- 如果你在 Auto 暂停时改为手动点步骤或手动同步步骤,面板会先确认并停止 Auto,再切回手动控制
|
||||||
|
|
||||||
## 详细步骤说明
|
## 详细步骤说明
|
||||||
|
|
||||||
@@ -281,6 +283,7 @@ Step 3 使用的注册邮箱。
|
|||||||
- 卡在某一步
|
- 卡在某一步
|
||||||
- 邮件迟迟不来
|
- 邮件迟迟不来
|
||||||
- 页面结构变化导致等待超时
|
- 页面结构变化导致等待超时
|
||||||
|
- Auto 暂停后,明确放弃 Auto、改为手动接管
|
||||||
|
|
||||||
## 状态与数据
|
## 状态与数据
|
||||||
|
|
||||||
@@ -295,6 +298,7 @@ Step 3 使用的注册邮箱。
|
|||||||
- 账号记录
|
- 账号记录
|
||||||
- tab 注册信息
|
- tab 注册信息
|
||||||
- 自定义设置
|
- 自定义设置
|
||||||
|
- Auto 当前阶段、当前轮次、暂停信息
|
||||||
|
|
||||||
特点:
|
特点:
|
||||||
|
|
||||||
@@ -346,7 +350,14 @@ sidepanel/ 侧边栏 UI
|
|||||||
- 手动点 `Step 3` 时,如果邮箱为空,脚本会先自动尝试获取 Duck 邮箱;失败后再改为手填
|
- 手动点 `Step 3` 时,如果邮箱为空,脚本会先自动尝试获取 Duck 邮箱;失败后再改为手填
|
||||||
- Auto 暂停时,仍可手动粘贴邮箱后点击 `Continue`
|
- Auto 暂停时,仍可手动粘贴邮箱后点击 `Continue`
|
||||||
|
|
||||||
### 4. Step 8 失败时重点检查
|
### 4. 页面已手动完成时同步面板状态
|
||||||
|
|
||||||
|
- 每个可同步的步骤右侧会出现一个小按钮,用来把该步标记为“已手动完成”
|
||||||
|
- 点击后会先弹窗确认;它不会真正执行脚本,只会在校验当前页面已进入下一阶段后,放行后续步骤
|
||||||
|
- 该按钮只会在合理边界内显示;例如最后一步不会显示,依赖数据缺失时会禁用
|
||||||
|
- 如果 Auto 处于暂停状态,点击该按钮会先确认是否接管 Auto
|
||||||
|
|
||||||
|
### 5. Step 8 失败时重点检查
|
||||||
|
|
||||||
- OAuth 同意页 DOM 是否变化
|
- OAuth 同意页 DOM 是否变化
|
||||||
- “继续”按钮是否变成了别的文案
|
- “继续”按钮是否变成了别的文案
|
||||||
|
|||||||
+288
-38
@@ -35,6 +35,11 @@ const DEFAULT_STATE = {
|
|||||||
vpsPassword: '',
|
vpsPassword: '',
|
||||||
customPassword: '',
|
customPassword: '',
|
||||||
autoRunSkipFailures: false,
|
autoRunSkipFailures: false,
|
||||||
|
autoRunning: false,
|
||||||
|
autoRunPhase: 'idle',
|
||||||
|
autoRunCurrentRun: 0,
|
||||||
|
autoRunTotalRuns: 1,
|
||||||
|
autoRunAttemptRun: 0,
|
||||||
mailProvider: '163', // 'qq' or '163'
|
mailProvider: '163', // 'qq' or '163'
|
||||||
inbucketHost: '',
|
inbucketHost: '',
|
||||||
inbucketMailbox: '',
|
inbucketMailbox: '',
|
||||||
@@ -425,10 +430,197 @@ function isStopError(error) {
|
|||||||
return message === STOP_ERROR_MESSAGE;
|
return message === STOP_ERROR_MESSAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStepDoneStatus(status) {
|
||||||
|
return status === 'completed' || status === 'manual_completed';
|
||||||
|
}
|
||||||
|
|
||||||
function clearStopRequest() {
|
function clearStopRequest() {
|
||||||
stopRequested = false;
|
stopRequested = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAutoRunStatusPayload(phase, payload = {}) {
|
||||||
|
const currentRun = payload.currentRun ?? autoRunCurrentRun;
|
||||||
|
const totalRuns = payload.totalRuns ?? autoRunTotalRuns;
|
||||||
|
const attemptRun = payload.attemptRun ?? autoRunAttemptRun;
|
||||||
|
const autoRunning = phase === 'running' || phase === 'waiting_email' || phase === 'retrying';
|
||||||
|
|
||||||
|
return {
|
||||||
|
autoRunning,
|
||||||
|
autoRunPhase: phase,
|
||||||
|
autoRunCurrentRun: currentRun,
|
||||||
|
autoRunTotalRuns: totalRuns,
|
||||||
|
autoRunAttemptRun: attemptRun,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function broadcastAutoRunStatus(phase, payload = {}) {
|
||||||
|
const statusPayload = {
|
||||||
|
phase,
|
||||||
|
currentRun: payload.currentRun ?? autoRunCurrentRun,
|
||||||
|
totalRuns: payload.totalRuns ?? autoRunTotalRuns,
|
||||||
|
attemptRun: payload.attemptRun ?? autoRunAttemptRun,
|
||||||
|
};
|
||||||
|
|
||||||
|
await setState(getAutoRunStatusPayload(phase, statusPayload));
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: 'AUTO_RUN_STATUS',
|
||||||
|
payload: statusPayload,
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAutoRunLockedState(state) {
|
||||||
|
return Boolean(state.autoRunning) && (state.autoRunPhase === 'running' || state.autoRunPhase === 'retrying');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAutoRunPausedState(state) {
|
||||||
|
return Boolean(state.autoRunning) && state.autoRunPhase === 'waiting_email';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureManualInteractionAllowed(actionLabel) {
|
||||||
|
const state = await getState();
|
||||||
|
|
||||||
|
if (isAutoRunLockedState(state)) {
|
||||||
|
throw new Error(`自动流程运行中,请先停止后再${actionLabel}。`);
|
||||||
|
}
|
||||||
|
if (isAutoRunPausedState(state)) {
|
||||||
|
throw new Error(`自动流程当前已暂停。请点击“继续”,或先确认接管自动流程后再${actionLabel}。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSignupManualCompletionState() {
|
||||||
|
const signupTabId = await getTabId('signup-page');
|
||||||
|
if (!signupTabId) {
|
||||||
|
throw new Error('认证页标签页不存在,请先打开认证页并手动完成相应操作。');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await sendToContentScript('signup-page', {
|
||||||
|
type: 'GET_MANUAL_COMPLETION_STATE',
|
||||||
|
source: 'background',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureKnownPassword(state, step) {
|
||||||
|
const resolvedPassword = state.password || state.customPassword;
|
||||||
|
if (!resolvedPassword) {
|
||||||
|
throw new Error(`步骤 ${step} 需要已知密码,请先在侧边栏填写固定密码后再手动同步。`);
|
||||||
|
}
|
||||||
|
return resolvedPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateManualStepCompletion(step, state) {
|
||||||
|
switch (step) {
|
||||||
|
case 2: {
|
||||||
|
const authState = await getSignupManualCompletionState();
|
||||||
|
if (!['credentials', 'verification', 'profile', 'add_phone', 'consent'].includes(authState.stage)) {
|
||||||
|
throw new Error(`认证页当前还没有进入注册流程后续页面,不能将步骤 2 标记为手动完成。当前状态:${authState.summary || authState.stage || '未知'}`);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 3: {
|
||||||
|
if (!state.email) {
|
||||||
|
throw new Error('步骤 3 需要邮箱地址,请先在侧边栏填写邮箱。');
|
||||||
|
}
|
||||||
|
const password = ensureKnownPassword(state, 3);
|
||||||
|
const authState = await getSignupManualCompletionState();
|
||||||
|
if (!['verification', 'profile', 'add_phone', 'consent'].includes(authState.stage)) {
|
||||||
|
throw new Error(`认证页当前还没有离开邮箱/密码页面,不能将步骤 3 标记为手动完成。当前状态:${authState.summary || authState.stage || '未知'}`);
|
||||||
|
}
|
||||||
|
return state.password ? {} : { stateUpdates: { password } };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4: {
|
||||||
|
const authState = await getSignupManualCompletionState();
|
||||||
|
if (!['profile', 'add_phone', 'consent'].includes(authState.stage)) {
|
||||||
|
throw new Error(`认证页当前还没有进入验证码后的下一阶段,不能将步骤 4 标记为手动完成。当前状态:${authState.summary || authState.stage || '未知'}`);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 5: {
|
||||||
|
const authState = await getSignupManualCompletionState();
|
||||||
|
if (!['add_phone', 'consent'].includes(authState.stage)) {
|
||||||
|
throw new Error(`认证页当前还没有离开姓名/生日页面,不能将步骤 5 标记为手动完成。当前状态:${authState.summary || authState.stage || '未知'}`);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 6: {
|
||||||
|
if (!state.email) {
|
||||||
|
throw new Error('步骤 6 需要已知邮箱地址,请先在侧边栏填写邮箱。');
|
||||||
|
}
|
||||||
|
const authState = await getSignupManualCompletionState();
|
||||||
|
if (!['verification', 'add_phone', 'consent'].includes(authState.stage)) {
|
||||||
|
throw new Error(`认证页当前还没有进入登录后的下一阶段,不能将步骤 6 标记为手动完成。当前状态:${authState.summary || authState.stage || '未知'}`);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 7: {
|
||||||
|
const authState = await getSignupManualCompletionState();
|
||||||
|
if (!['add_phone', 'consent'].includes(authState.stage)) {
|
||||||
|
throw new Error(`认证页当前还没有离开登录验证码页面,不能将步骤 7 标记为手动完成。当前状态:${authState.summary || authState.stage || '未知'}`);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 8: {
|
||||||
|
if (!state.localhostUrl) {
|
||||||
|
throw new Error('尚未捕获 localhost 回调地址,不能将步骤 8 标记为手动完成。');
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`步骤 ${step} 不支持手动完成同步。`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markStepManualComplete(step) {
|
||||||
|
const state = await ensureManualInteractionAllowed('手动同步步骤');
|
||||||
|
|
||||||
|
if (!Number.isInteger(step) || step < 1 || step > 9) {
|
||||||
|
throw new Error(`无效步骤:${step}`);
|
||||||
|
}
|
||||||
|
if (step === 1 || step === 9) {
|
||||||
|
throw new Error(`步骤 ${step} 不支持手动完成同步。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statuses = { ...(state.stepStatuses || {}) };
|
||||||
|
const currentStatus = statuses[step];
|
||||||
|
if (currentStatus === 'running') {
|
||||||
|
throw new Error(`步骤 ${step} 正在运行中,不能手动同步。`);
|
||||||
|
}
|
||||||
|
if (isStepDoneStatus(currentStatus)) {
|
||||||
|
throw new Error(`步骤 ${step} 已完成,无需再手动同步。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step > 1) {
|
||||||
|
const prevStatus = statuses[step - 1];
|
||||||
|
if (!isStepDoneStatus(prevStatus)) {
|
||||||
|
throw new Error(`请先完成步骤 ${step - 1},再同步步骤 ${step}。`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = await validateManualStepCompletion(step, state);
|
||||||
|
if (validation?.stateUpdates) {
|
||||||
|
await setState(validation.stateUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
|
await setStepStatus(step, 'manual_completed');
|
||||||
|
await addLog(`步骤 ${step} 已标记为手动完成`, 'warn');
|
||||||
|
return { ok: true, step, status: 'manual_completed' };
|
||||||
|
}
|
||||||
|
|
||||||
function throwIfStopped() {
|
function throwIfStopped() {
|
||||||
if (stopRequested) {
|
if (stopRequested) {
|
||||||
throw new Error(STOP_ERROR_MESSAGE);
|
throw new Error(STOP_ERROR_MESSAGE);
|
||||||
@@ -590,6 +782,9 @@ async function handleMessage(message, sender) {
|
|||||||
|
|
||||||
case 'EXECUTE_STEP': {
|
case 'EXECUTE_STEP': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
|
if (message.source === 'sidepanel') {
|
||||||
|
await ensureManualInteractionAllowed('手动执行步骤');
|
||||||
|
}
|
||||||
const step = message.payload.step;
|
const step = message.payload.step;
|
||||||
// Save email if provided (from side panel step 3)
|
// Save email if provided (from side panel step 3)
|
||||||
if (message.payload.email) {
|
if (message.payload.email) {
|
||||||
@@ -617,6 +812,17 @@ async function handleMessage(message, sender) {
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'TAKEOVER_AUTO_RUN': {
|
||||||
|
await requestStop({ logMessage: '已确认手动接管,正在停止自动流程并切换为手动控制...' });
|
||||||
|
await addLog('自动流程已切换为手动控制。', 'warn');
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MARK_STEP_MANUAL_COMPLETE': {
|
||||||
|
const step = Number(message.payload?.step);
|
||||||
|
return await markStepManualComplete(step);
|
||||||
|
}
|
||||||
|
|
||||||
case 'SAVE_SETTING': {
|
case 'SAVE_SETTING': {
|
||||||
const updates = {};
|
const updates = {};
|
||||||
if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
|
if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
|
||||||
@@ -632,12 +838,20 @@ async function handleMessage(message, sender) {
|
|||||||
|
|
||||||
// Side panel data updates
|
// Side panel data updates
|
||||||
case 'SAVE_EMAIL': {
|
case 'SAVE_EMAIL': {
|
||||||
|
const state = await getState();
|
||||||
|
if (isAutoRunLockedState(state)) {
|
||||||
|
throw new Error('自动流程运行中,当前不能手动修改邮箱。');
|
||||||
|
}
|
||||||
await setEmailState(message.payload.email);
|
await setEmailState(message.payload.email);
|
||||||
return { ok: true, email: message.payload.email };
|
return { ok: true, email: message.payload.email };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'FETCH_DUCK_EMAIL': {
|
case 'FETCH_DUCK_EMAIL': {
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
|
const state = await getState();
|
||||||
|
if (isAutoRunLockedState(state)) {
|
||||||
|
throw new Error('自动流程运行中,当前不能手动获取 Duck 邮箱。');
|
||||||
|
}
|
||||||
const email = await fetchDuckEmail(message.payload || {});
|
const email = await fetchDuckEmail(message.payload || {});
|
||||||
return { ok: true, email };
|
return { ok: true, email };
|
||||||
}
|
}
|
||||||
@@ -737,7 +951,8 @@ async function markRunningStepsStopped() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestStop() {
|
async function requestStop(options = {}) {
|
||||||
|
const { logMessage = '已收到停止请求,正在取消当前操作...' } = options;
|
||||||
if (stopRequested) return;
|
if (stopRequested) return;
|
||||||
|
|
||||||
stopRequested = true;
|
stopRequested = true;
|
||||||
@@ -747,7 +962,7 @@ async function requestStop() {
|
|||||||
webNavListener = null;
|
webNavListener = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog('已收到停止请求,正在取消当前操作...', 'warn');
|
await addLog(logMessage, 'warn');
|
||||||
await broadcastStopToContentScripts();
|
await broadcastStopToContentScripts();
|
||||||
|
|
||||||
for (const waiter of stepWaiters.values()) {
|
for (const waiter of stepWaiters.values()) {
|
||||||
@@ -762,11 +977,11 @@ async function requestStop() {
|
|||||||
|
|
||||||
await markRunningStepsStopped();
|
await markRunningStepsStopped();
|
||||||
autoRunActive = false;
|
autoRunActive = false;
|
||||||
await setState({ autoRunning: false });
|
await broadcastAutoRunStatus('stopped', {
|
||||||
chrome.runtime.sendMessage({
|
currentRun: autoRunCurrentRun,
|
||||||
type: 'AUTO_RUN_STATUS',
|
totalRuns: autoRunTotalRuns,
|
||||||
payload: { phase: 'stopped', currentRun: autoRunCurrentRun, totalRuns: autoRunTotalRuns },
|
attemptRun: autoRunAttemptRun,
|
||||||
}).catch(() => {});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -861,6 +1076,7 @@ async function fetchDuckEmail(options = {}) {
|
|||||||
let autoRunActive = false;
|
let autoRunActive = false;
|
||||||
let autoRunCurrentRun = 0;
|
let autoRunCurrentRun = 0;
|
||||||
let autoRunTotalRuns = 1;
|
let autoRunTotalRuns = 1;
|
||||||
|
let autoRunAttemptRun = 0;
|
||||||
|
|
||||||
// Outer loop: keep retrying until the target number of successful runs is reached.
|
// Outer loop: keep retrying until the target number of successful runs is reached.
|
||||||
async function autoRunLoop(totalRuns, options = {}) {
|
async function autoRunLoop(totalRuns, options = {}) {
|
||||||
@@ -872,18 +1088,24 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
autoRunActive = true;
|
autoRunActive = true;
|
||||||
autoRunTotalRuns = totalRuns;
|
autoRunTotalRuns = totalRuns;
|
||||||
|
autoRunCurrentRun = 0;
|
||||||
|
autoRunAttemptRun = 0;
|
||||||
const autoRunSkipFailures = Boolean(options.autoRunSkipFailures);
|
const autoRunSkipFailures = Boolean(options.autoRunSkipFailures);
|
||||||
const maxAttempts = autoRunSkipFailures ? Math.max(totalRuns * 10, totalRuns + 20) : totalRuns;
|
const maxAttempts = autoRunSkipFailures ? Math.max(totalRuns * 10, totalRuns + 20) : totalRuns;
|
||||||
let successfulRuns = 0;
|
let successfulRuns = 0;
|
||||||
let attemptRuns = 0;
|
let attemptRuns = 0;
|
||||||
let forceFreshTabsNextRun = false;
|
let forceFreshTabsNextRun = false;
|
||||||
|
|
||||||
await setState({ autoRunning: true, autoRunSkipFailures });
|
await setState({
|
||||||
|
autoRunSkipFailures,
|
||||||
|
...getAutoRunStatusPayload('running', { currentRun: 0, totalRuns, attemptRun: 0 }),
|
||||||
|
});
|
||||||
|
|
||||||
while (successfulRuns < totalRuns && attemptRuns < maxAttempts) {
|
while (successfulRuns < totalRuns && attemptRuns < maxAttempts) {
|
||||||
attemptRuns += 1;
|
attemptRuns += 1;
|
||||||
const targetRun = successfulRuns + 1;
|
const targetRun = successfulRuns + 1;
|
||||||
autoRunCurrentRun = targetRun;
|
autoRunCurrentRun = targetRun;
|
||||||
|
autoRunAttemptRun = attemptRuns;
|
||||||
|
|
||||||
// Reset everything at the start of each attempt (keep user settings).
|
// Reset everything at the start of each attempt (keep user settings).
|
||||||
const prevState = await getState();
|
const prevState = await getState();
|
||||||
@@ -895,7 +1117,7 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
mailProvider: prevState.mailProvider,
|
mailProvider: prevState.mailProvider,
|
||||||
inbucketHost: prevState.inbucketHost,
|
inbucketHost: prevState.inbucketHost,
|
||||||
inbucketMailbox: prevState.inbucketMailbox,
|
inbucketMailbox: prevState.inbucketMailbox,
|
||||||
autoRunning: true,
|
...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }),
|
||||||
...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}),
|
...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}),
|
||||||
};
|
};
|
||||||
await resetState();
|
await resetState();
|
||||||
@@ -908,16 +1130,15 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
forceFreshTabsNextRun = false;
|
forceFreshTabsNextRun = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = (phase) => ({
|
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info');
|
||||||
type: 'AUTO_RUN_STATUS',
|
|
||||||
payload: { phase, currentRun: targetRun, totalRuns, attemptRun: attemptRuns },
|
|
||||||
});
|
|
||||||
|
|
||||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
chrome.runtime.sendMessage(status('running')).catch(() => {});
|
await broadcastAutoRunStatus('running', {
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
|
|
||||||
await executeStepAndWait(1, 2000);
|
await executeStepAndWait(1, 2000);
|
||||||
await executeStepAndWait(2, 2000);
|
await executeStepAndWait(2, 2000);
|
||||||
@@ -933,7 +1154,11 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
|
|
||||||
if (!emailReady) {
|
if (!emailReady) {
|
||||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先获取 Duck 邮箱或手动粘贴邮箱,然后继续 ===`, 'warn');
|
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先获取 Duck 邮箱或手动粘贴邮箱,然后继续 ===`, 'warn');
|
||||||
chrome.runtime.sendMessage(status('waiting_email')).catch(() => {});
|
await broadcastAutoRunStatus('waiting_email', {
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
|
|
||||||
await waitForResume();
|
await waitForResume();
|
||||||
|
|
||||||
@@ -944,7 +1169,11 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,注册、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info');
|
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,注册、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info');
|
||||||
chrome.runtime.sendMessage(status('running')).catch(() => {});
|
await broadcastAutoRunStatus('running', {
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
|
|
||||||
const signupTabId = await getTabId('signup-page');
|
const signupTabId = await getTabId('signup-page');
|
||||||
if (signupTabId) {
|
if (signupTabId) {
|
||||||
@@ -966,13 +1195,21 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isStopError(err)) {
|
if (isStopError(err)) {
|
||||||
await addLog(`目标 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
await addLog(`目标 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
|
||||||
chrome.runtime.sendMessage(status('stopped')).catch(() => {});
|
await broadcastAutoRunStatus('stopped', {
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!autoRunSkipFailures) {
|
if (!autoRunSkipFailures) {
|
||||||
await addLog(`目标 ${targetRun}/${totalRuns} 轮失败:${err.message}`, 'error');
|
await addLog(`目标 ${targetRun}/${totalRuns} 轮失败:${err.message}`, 'error');
|
||||||
chrome.runtime.sendMessage(status('stopped')).catch(() => {});
|
await broadcastAutoRunStatus('stopped', {
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,38 +1217,51 @@ async function autoRunLoop(totalRuns, options = {}) {
|
|||||||
await addLog('兜底开关已开启:将放弃当前线程,重新开一轮继续补足目标次数。', 'warn');
|
await addLog('兜底开关已开启:将放弃当前线程,重新开一轮继续补足目标次数。', 'warn');
|
||||||
cancelPendingCommands('当前尝试已放弃。');
|
cancelPendingCommands('当前尝试已放弃。');
|
||||||
await broadcastStopToContentScripts();
|
await broadcastStopToContentScripts();
|
||||||
chrome.runtime.sendMessage(status('retrying')).catch(() => {});
|
await broadcastAutoRunStatus('retrying', {
|
||||||
|
currentRun: targetRun,
|
||||||
|
totalRuns,
|
||||||
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
forceFreshTabsNextRun = true;
|
forceFreshTabsNextRun = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stopRequested && autoRunSkipFailures && successfulRuns < totalRuns && attemptRuns >= maxAttempts) {
|
if (!stopRequested && autoRunSkipFailures && successfulRuns < totalRuns && attemptRuns >= maxAttempts) {
|
||||||
await addLog(`已达到安全重试上限(${attemptRuns} 次尝试),当前仅完成 ${successfulRuns}/${totalRuns} 轮。`, 'error');
|
await addLog(`已达到安全重试上限(${attemptRuns} 次尝试),当前仅完成 ${successfulRuns}/${totalRuns} 轮。`, 'error');
|
||||||
chrome.runtime.sendMessage({
|
await broadcastAutoRunStatus('stopped', {
|
||||||
type: 'AUTO_RUN_STATUS',
|
currentRun: successfulRuns,
|
||||||
payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
|
totalRuns: autoRunTotalRuns,
|
||||||
}).catch(() => {});
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
} else if (stopRequested) {
|
} else if (stopRequested) {
|
||||||
await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
|
await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
|
||||||
chrome.runtime.sendMessage({
|
await broadcastAutoRunStatus('stopped', {
|
||||||
type: 'AUTO_RUN_STATUS',
|
currentRun: successfulRuns,
|
||||||
payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
|
totalRuns: autoRunTotalRuns,
|
||||||
}).catch(() => {});
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
} else if (successfulRuns >= autoRunTotalRuns) {
|
} else if (successfulRuns >= autoRunTotalRuns) {
|
||||||
await addLog(`=== 全部 ${autoRunTotalRuns} 轮均已成功完成,共尝试 ${attemptRuns} 次 ===`, 'ok');
|
await addLog(`=== 全部 ${autoRunTotalRuns} 轮均已成功完成,共尝试 ${attemptRuns} 次 ===`, 'ok');
|
||||||
chrome.runtime.sendMessage({
|
await broadcastAutoRunStatus('complete', {
|
||||||
type: 'AUTO_RUN_STATUS',
|
currentRun: successfulRuns,
|
||||||
payload: { phase: 'complete', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
|
totalRuns: autoRunTotalRuns,
|
||||||
}).catch(() => {});
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
|
await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
|
||||||
chrome.runtime.sendMessage({
|
await broadcastAutoRunStatus('stopped', {
|
||||||
type: 'AUTO_RUN_STATUS',
|
currentRun: successfulRuns,
|
||||||
payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
|
totalRuns: autoRunTotalRuns,
|
||||||
}).catch(() => {});
|
attemptRun: attemptRuns,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
autoRunActive = false;
|
autoRunActive = false;
|
||||||
await setState({ autoRunning: false });
|
autoRunAttemptRun = attemptRuns;
|
||||||
|
await setState(getAutoRunStatusPayload(stopRequested ? 'stopped' : (successfulRuns >= autoRunTotalRuns ? 'complete' : 'stopped'), {
|
||||||
|
currentRun: successfulRuns,
|
||||||
|
totalRuns: autoRunTotalRuns,
|
||||||
|
attemptRun: attemptRuns,
|
||||||
|
}));
|
||||||
clearStopRequest();
|
clearStopRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
|| message.type === 'STEP8_FIND_AND_CLICK'
|
|| message.type === 'STEP8_FIND_AND_CLICK'
|
||||||
|| message.type === 'PREPARE_LOGIN_CODE'
|
|| message.type === 'PREPARE_LOGIN_CODE'
|
||||||
|| message.type === 'RESEND_VERIFICATION_CODE'
|
|| message.type === 'RESEND_VERIFICATION_CODE'
|
||||||
|
|| message.type === 'GET_MANUAL_COMPLETION_STATE'
|
||||||
) {
|
) {
|
||||||
resetStopState();
|
resetStopState();
|
||||||
handleCommand(message).then((result) => {
|
handleCommand(message).then((result) => {
|
||||||
@@ -22,6 +23,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === 'GET_MANUAL_COMPLETION_STATE') {
|
||||||
|
sendResponse({ error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === 'STEP8_FIND_AND_CLICK') {
|
if (message.type === 'STEP8_FIND_AND_CLICK') {
|
||||||
log(`步骤 8:${err.message}`, 'error');
|
log(`步骤 8:${err.message}`, 'error');
|
||||||
sendResponse({ error: err.message });
|
sendResponse({ error: err.message });
|
||||||
@@ -53,6 +59,8 @@ async function handleCommand(message) {
|
|||||||
return await prepareLoginCodeFlow();
|
return await prepareLoginCodeFlow();
|
||||||
case 'RESEND_VERIFICATION_CODE':
|
case 'RESEND_VERIFICATION_CODE':
|
||||||
return await resendVerificationCode(message.step);
|
return await resendVerificationCode(message.step);
|
||||||
|
case 'GET_MANUAL_COMPLETION_STATE':
|
||||||
|
return getManualCompletionState();
|
||||||
case 'STEP8_FIND_AND_CLICK':
|
case 'STEP8_FIND_AND_CLICK':
|
||||||
return await step8_findAndClick();
|
return await step8_findAndClick();
|
||||||
}
|
}
|
||||||
@@ -145,6 +153,25 @@ function findOneTimeCodeLoginTrigger() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findRegisterEntryTrigger() {
|
||||||
|
const candidates = document.querySelectorAll(
|
||||||
|
'a, button, [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 = getActionText(el);
|
||||||
|
const href = el.getAttribute('href') || '';
|
||||||
|
if ((text && /sign\s*up|register|create\s*account|注册/i.test(text)) || /signup|register/i.test(href)) {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function findResendVerificationCodeTrigger({ allowDisabled = false } = {}) {
|
function findResendVerificationCodeTrigger({ allowDisabled = false } = {}) {
|
||||||
const candidates = document.querySelectorAll(
|
const candidates = document.querySelectorAll(
|
||||||
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
|
'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
|
||||||
@@ -394,6 +421,18 @@ function isStep5Ready() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCredentialsPageReady() {
|
||||||
|
const emailInput = document.querySelector(
|
||||||
|
'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i]'
|
||||||
|
);
|
||||||
|
if (emailInput && isVisibleElement(emailInput)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordInput = document.querySelector('input[type="password"]');
|
||||||
|
return Boolean(passwordInput && isVisibleElement(passwordInput));
|
||||||
|
}
|
||||||
|
|
||||||
function getPageTextSnapshot() {
|
function getPageTextSnapshot() {
|
||||||
return (document.body?.innerText || document.body?.textContent || '')
|
return (document.body?.innerText || document.body?.textContent || '')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
@@ -443,6 +482,29 @@ function isStep8Ready() {
|
|||||||
return OAUTH_CONSENT_PAGE_PATTERN.test(getPageTextSnapshot());
|
return OAUTH_CONSENT_PAGE_PATTERN.test(getPageTextSnapshot());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getManualCompletionState() {
|
||||||
|
if (isStep8Ready()) {
|
||||||
|
return { stage: 'consent', summary: '已进入 OAuth 授权同意页', url: location.href };
|
||||||
|
}
|
||||||
|
if (isAddPhonePageReady()) {
|
||||||
|
return { stage: 'add_phone', summary: '已进入手机号页面', url: location.href };
|
||||||
|
}
|
||||||
|
if (isStep5Ready()) {
|
||||||
|
return { stage: 'profile', summary: '已进入姓名/生日页面', url: location.href };
|
||||||
|
}
|
||||||
|
if (isVerificationPageStillVisible()) {
|
||||||
|
return { stage: 'verification', summary: '仍停留在验证码页面', url: location.href };
|
||||||
|
}
|
||||||
|
if (isCredentialsPageReady()) {
|
||||||
|
return { stage: 'credentials', summary: '已进入邮箱或密码输入页面', url: location.href };
|
||||||
|
}
|
||||||
|
if (findRegisterEntryTrigger()) {
|
||||||
|
return { stage: 'register', summary: '仍停留在注册入口页面', url: location.href };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stage: 'unknown', summary: '无法识别当前认证页状态', url: location.href };
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForVerificationSubmitOutcome(step, timeout) {
|
async function waitForVerificationSubmitOutcome(step, timeout) {
|
||||||
const resolvedTimeout = timeout ?? (step === 7 ? 30000 : 12000);
|
const resolvedTimeout = timeout ?? (step === 7 ? 30000 : 12000);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|||||||
@@ -386,6 +386,12 @@ header {
|
|||||||
.status-bar.stopped .status-dot { background: var(--cyan); }
|
.status-bar.stopped .status-dot { background: var(--cyan); }
|
||||||
.status-bar.stopped { color: var(--cyan); }
|
.status-bar.stopped { color: var(--cyan); }
|
||||||
|
|
||||||
|
.status-bar.paused .status-dot {
|
||||||
|
background: var(--orange);
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.status-bar.paused { color: var(--orange); }
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
50% { opacity: 0.4; transform: scale(0.85); }
|
50% { opacity: 0.4; transform: scale(0.85); }
|
||||||
@@ -485,6 +491,9 @@ header {
|
|||||||
.step-row.stopped .step-indicator { border-color: var(--cyan); background: rgba(8, 145, 178, 0.08); }
|
.step-row.stopped .step-indicator { border-color: var(--cyan); background: rgba(8, 145, 178, 0.08); }
|
||||||
.step-row.stopped .step-num { color: var(--cyan); }
|
.step-row.stopped .step-num { color: var(--cyan); }
|
||||||
|
|
||||||
|
.step-row.manual_completed .step-indicator { border-color: var(--blue); background: var(--blue-soft); }
|
||||||
|
.step-row.manual_completed .step-num { color: var(--blue); }
|
||||||
|
|
||||||
/* Step Button */
|
/* Step Button */
|
||||||
.step-btn {
|
.step-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -508,6 +517,43 @@ header {
|
|||||||
.step-row.completed .step-btn { border-color: var(--border-subtle); color: var(--text-secondary); opacity: 0.7; }
|
.step-row.completed .step-btn { border-color: var(--border-subtle); color: var(--text-secondary); opacity: 0.7; }
|
||||||
.step-row.failed .step-btn { border-color: var(--red); color: var(--red); }
|
.step-row.failed .step-btn { border-color: var(--red); color: var(--red); }
|
||||||
.step-row.stopped .step-btn { border-color: var(--cyan); color: var(--cyan); }
|
.step-row.stopped .step-btn { border-color: var(--cyan); color: var(--cyan); }
|
||||||
|
.step-row.manual_completed .step-btn { border-color: rgba(37, 99, 235, 0.25); color: var(--blue); opacity: 0.82; }
|
||||||
|
|
||||||
|
.step-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
width: 48px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-manual-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition);
|
||||||
|
}
|
||||||
|
.step-manual-btn:hover:not(:disabled) {
|
||||||
|
border-color: var(--blue);
|
||||||
|
color: var(--blue);
|
||||||
|
background: var(--blue-soft);
|
||||||
|
}
|
||||||
|
.step-manual-btn:disabled {
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-color: var(--border-subtle);
|
||||||
|
background: var(--bg-base);
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.step-status {
|
.step-status {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
@@ -519,6 +565,7 @@ header {
|
|||||||
.step-row.completed .step-status { color: var(--green); }
|
.step-row.completed .step-status { color: var(--green); }
|
||||||
.step-row.failed .step-status { color: var(--red); }
|
.step-row.failed .step-status { color: var(--red); }
|
||||||
.step-row.stopped .step-status { color: var(--cyan); }
|
.step-row.stopped .step-status { color: var(--cyan); }
|
||||||
|
.step-row.manual_completed .step-status { color: var(--blue); }
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Log / Console Section
|
Log / Console Section
|
||||||
|
|||||||
+337
-74
@@ -6,6 +6,7 @@ const STATUS_ICONS = {
|
|||||||
completed: '\u2713', // ✓
|
completed: '\u2713', // ✓
|
||||||
failed: '\u2717', // ✗
|
failed: '\u2717', // ✗
|
||||||
stopped: '\u25A0', // ■
|
stopped: '\u25A0', // ■
|
||||||
|
manual_completed: '手',
|
||||||
};
|
};
|
||||||
|
|
||||||
const logArea = document.getElementById('log-area');
|
const logArea = document.getElementById('log-area');
|
||||||
@@ -33,6 +34,27 @@ const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
|
|||||||
const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
|
const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
|
||||||
const inputRunCount = document.getElementById('input-run-count');
|
const inputRunCount = document.getElementById('input-run-count');
|
||||||
const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
|
const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
|
||||||
|
const STEP_DEFAULT_STATUSES = {
|
||||||
|
1: 'pending',
|
||||||
|
2: 'pending',
|
||||||
|
3: 'pending',
|
||||||
|
4: 'pending',
|
||||||
|
5: 'pending',
|
||||||
|
6: 'pending',
|
||||||
|
7: 'pending',
|
||||||
|
8: 'pending',
|
||||||
|
9: 'pending',
|
||||||
|
};
|
||||||
|
const MANUAL_COMPLETION_STEPS = new Set([2, 3, 4, 5, 6, 7, 8]);
|
||||||
|
|
||||||
|
let latestState = null;
|
||||||
|
let currentAutoRun = {
|
||||||
|
autoRunning: false,
|
||||||
|
phase: 'idle',
|
||||||
|
currentRun: 0,
|
||||||
|
totalRuns: 1,
|
||||||
|
attemptRun: 0,
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Toast Notifications
|
// Toast Notifications
|
||||||
@@ -73,6 +95,133 @@ function dismissToast(toast) {
|
|||||||
toast.addEventListener('animationend', () => toast.remove());
|
toast.addEventListener('animationend', () => toast.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDoneStatus(status) {
|
||||||
|
return status === 'completed' || status === 'manual_completed';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStepStatuses(state = latestState) {
|
||||||
|
return { ...STEP_DEFAULT_STATUSES, ...(state?.stepStatuses || {}) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLatestState(nextState) {
|
||||||
|
const mergedStepStatuses = nextState?.stepStatuses
|
||||||
|
? { ...STEP_DEFAULT_STATUSES, ...(latestState?.stepStatuses || {}), ...nextState.stepStatuses }
|
||||||
|
: getStepStatuses(latestState);
|
||||||
|
|
||||||
|
latestState = {
|
||||||
|
...(latestState || {}),
|
||||||
|
...(nextState || {}),
|
||||||
|
stepStatuses: mergedStepStatuses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAutoRunState(source = {}) {
|
||||||
|
const phase = source.autoRunPhase ?? source.phase ?? currentAutoRun.phase;
|
||||||
|
const autoRunning = source.autoRunning !== undefined
|
||||||
|
? Boolean(source.autoRunning)
|
||||||
|
: (source.autoRunPhase !== undefined || source.phase !== undefined
|
||||||
|
? ['running', 'waiting_email', 'retrying'].includes(phase)
|
||||||
|
: currentAutoRun.autoRunning);
|
||||||
|
|
||||||
|
currentAutoRun = {
|
||||||
|
autoRunning,
|
||||||
|
phase,
|
||||||
|
currentRun: source.autoRunCurrentRun ?? source.currentRun ?? currentAutoRun.currentRun,
|
||||||
|
totalRuns: source.autoRunTotalRuns ?? source.totalRuns ?? currentAutoRun.totalRuns,
|
||||||
|
attemptRun: source.autoRunAttemptRun ?? source.attemptRun ?? currentAutoRun.attemptRun,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAutoRunLockedPhase() {
|
||||||
|
return currentAutoRun.phase === 'running' || currentAutoRun.phase === 'retrying';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAutoRunPausedPhase() {
|
||||||
|
return currentAutoRun.phase === 'waiting_email';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAutoRunLabel(payload = currentAutoRun) {
|
||||||
|
const attemptLabel = payload.attemptRun ? ` · 尝试${payload.attemptRun}` : '';
|
||||||
|
if ((payload.totalRuns || 1) > 1) {
|
||||||
|
return ` (${payload.currentRun}/${payload.totalRuns}${attemptLabel})`;
|
||||||
|
}
|
||||||
|
return attemptLabel ? ` (${attemptLabel.slice(3)})` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultAutoRunButton() {
|
||||||
|
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> 自动';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAutoRunStatus(payload = currentAutoRun) {
|
||||||
|
syncAutoRunState(payload);
|
||||||
|
const runLabel = getAutoRunLabel(currentAutoRun);
|
||||||
|
const locked = isAutoRunLockedPhase();
|
||||||
|
const paused = isAutoRunPausedPhase();
|
||||||
|
|
||||||
|
inputRunCount.disabled = currentAutoRun.autoRunning;
|
||||||
|
btnAutoRun.disabled = currentAutoRun.autoRunning;
|
||||||
|
btnFetchEmail.disabled = locked;
|
||||||
|
inputEmail.disabled = locked;
|
||||||
|
|
||||||
|
switch (currentAutoRun.phase) {
|
||||||
|
case 'waiting_email':
|
||||||
|
autoContinueBar.style.display = 'flex';
|
||||||
|
btnAutoRun.innerHTML = `已暂停${runLabel}`;
|
||||||
|
break;
|
||||||
|
case 'running':
|
||||||
|
autoContinueBar.style.display = 'none';
|
||||||
|
btnAutoRun.innerHTML = `运行中${runLabel}`;
|
||||||
|
break;
|
||||||
|
case 'retrying':
|
||||||
|
autoContinueBar.style.display = 'none';
|
||||||
|
btnAutoRun.innerHTML = `重试中${runLabel}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
autoContinueBar.style.display = 'none';
|
||||||
|
setDefaultAutoRunButton();
|
||||||
|
inputEmail.disabled = false;
|
||||||
|
if (!locked) {
|
||||||
|
btnFetchEmail.disabled = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStopButtonState(paused || locked || Object.values(getStepStatuses()).some(status => status === 'running'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeManualStepActions() {
|
||||||
|
document.querySelectorAll('.step-row').forEach((row) => {
|
||||||
|
const step = Number(row.dataset.step);
|
||||||
|
const statusEl = row.querySelector('.step-status');
|
||||||
|
if (!statusEl) return;
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'step-actions';
|
||||||
|
|
||||||
|
const manualBtn = document.createElement('button');
|
||||||
|
manualBtn.type = 'button';
|
||||||
|
manualBtn.className = 'step-manual-btn';
|
||||||
|
manualBtn.dataset.step = String(step);
|
||||||
|
manualBtn.title = '标记此步已手动完成';
|
||||||
|
manualBtn.setAttribute('aria-label', `标记步骤 ${step} 已手动完成`);
|
||||||
|
manualBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>';
|
||||||
|
manualBtn.addEventListener('click', async (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
try {
|
||||||
|
await handleManualComplete(step);
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
statusEl.parentNode.replaceChild(actions, statusEl);
|
||||||
|
actions.appendChild(manualBtn);
|
||||||
|
actions.appendChild(statusEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// State Restore on load
|
// State Restore on load
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -80,6 +229,8 @@ function dismissToast(toast) {
|
|||||||
async function restoreState() {
|
async function restoreState() {
|
||||||
try {
|
try {
|
||||||
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
|
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
|
||||||
|
syncLatestState(state);
|
||||||
|
syncAutoRunState(state);
|
||||||
|
|
||||||
if (state.oauthUrl) {
|
if (state.oauthUrl) {
|
||||||
displayOauthUrl.textContent = state.oauthUrl;
|
displayOauthUrl.textContent = state.oauthUrl;
|
||||||
@@ -122,9 +273,11 @@ async function restoreState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatusDisplay(state);
|
applyAutoRunStatus(state);
|
||||||
|
updateStatusDisplay(latestState);
|
||||||
updateProgressCounter();
|
updateProgressCounter();
|
||||||
updateMailProviderUI();
|
updateMailProviderUI();
|
||||||
|
updateButtonStates();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to restore state:', err);
|
console.error('Failed to restore state:', err);
|
||||||
}
|
}
|
||||||
@@ -148,6 +301,13 @@ function updateStepUI(step, status) {
|
|||||||
const statusEl = document.querySelector(`.step-status[data-step="${step}"]`);
|
const statusEl = document.querySelector(`.step-status[data-step="${step}"]`);
|
||||||
const row = document.querySelector(`.step-row[data-step="${step}"]`);
|
const row = document.querySelector(`.step-row[data-step="${step}"]`);
|
||||||
|
|
||||||
|
syncLatestState({
|
||||||
|
stepStatuses: {
|
||||||
|
...getStepStatuses(),
|
||||||
|
[step]: status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (statusEl) statusEl.textContent = STATUS_ICONS[status] || '';
|
if (statusEl) statusEl.textContent = STATUS_ICONS[status] || '';
|
||||||
if (row) {
|
if (row) {
|
||||||
row.className = `step-row ${status}`;
|
row.className = `step-row ${status}`;
|
||||||
@@ -158,42 +318,68 @@ function updateStepUI(step, status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateProgressCounter() {
|
function updateProgressCounter() {
|
||||||
let completed = 0;
|
const completed = Object.values(getStepStatuses()).filter(isDoneStatus).length;
|
||||||
document.querySelectorAll('.step-row').forEach(row => {
|
|
||||||
if (row.classList.contains('completed')) completed++;
|
|
||||||
});
|
|
||||||
stepsProgress.textContent = `${completed} / 9`;
|
stepsProgress.textContent = `${completed} / 9`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateButtonStates() {
|
function updateButtonStates() {
|
||||||
const statuses = {};
|
const statuses = getStepStatuses();
|
||||||
document.querySelectorAll('.step-row').forEach(row => {
|
|
||||||
const step = Number(row.dataset.step);
|
|
||||||
if (row.classList.contains('completed')) statuses[step] = 'completed';
|
|
||||||
else if (row.classList.contains('running')) statuses[step] = 'running';
|
|
||||||
else if (row.classList.contains('failed')) statuses[step] = 'failed';
|
|
||||||
else if (row.classList.contains('stopped')) statuses[step] = 'stopped';
|
|
||||||
else statuses[step] = 'pending';
|
|
||||||
});
|
|
||||||
|
|
||||||
const anyRunning = Object.values(statuses).some(s => s === 'running');
|
const anyRunning = Object.values(statuses).some(s => s === 'running');
|
||||||
|
const autoLocked = isAutoRunLockedPhase();
|
||||||
|
|
||||||
for (let step = 1; step <= 9; step++) {
|
for (let step = 1; step <= 9; step++) {
|
||||||
const btn = document.querySelector(`.step-btn[data-step="${step}"]`);
|
const btn = document.querySelector(`.step-btn[data-step="${step}"]`);
|
||||||
if (!btn) continue;
|
if (!btn) continue;
|
||||||
|
|
||||||
if (anyRunning) {
|
if (anyRunning || autoLocked) {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
} else if (step === 1) {
|
} else if (step === 1) {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
const prevStatus = statuses[step - 1];
|
const prevStatus = statuses[step - 1];
|
||||||
const currentStatus = statuses[step];
|
const currentStatus = statuses[step];
|
||||||
btn.disabled = !(prevStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'completed' || currentStatus === 'stopped');
|
btn.disabled = !(isDoneStatus(prevStatus) || currentStatus === 'failed' || isDoneStatus(currentStatus) || currentStatus === 'stopped');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStopButtonState(anyRunning || autoContinueBar.style.display !== 'none');
|
document.querySelectorAll('.step-manual-btn').forEach((btn) => {
|
||||||
|
const step = Number(btn.dataset.step);
|
||||||
|
const currentStatus = statuses[step];
|
||||||
|
const prevStatus = statuses[step - 1];
|
||||||
|
|
||||||
|
if (!MANUAL_COMPLETION_STEPS.has(step) || anyRunning || autoLocked || currentStatus === 'running' || isDoneStatus(currentStatus)) {
|
||||||
|
btn.style.display = 'none';
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.title = '当前不可手动同步';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step > 1 && !isDoneStatus(prevStatus)) {
|
||||||
|
btn.style.display = 'none';
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.title = `请先完成步骤 ${step - 1}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let disabledReason = '';
|
||||||
|
if (step === 3) {
|
||||||
|
if (!(latestState?.email || inputEmail.value.trim())) {
|
||||||
|
disabledReason = '请先填写邮箱';
|
||||||
|
} else if (!(latestState?.password || latestState?.customPassword || inputPassword.value)) {
|
||||||
|
disabledReason = '请先在侧边栏填写固定密码';
|
||||||
|
}
|
||||||
|
} else if (step === 6 && !(latestState?.email || inputEmail.value.trim())) {
|
||||||
|
disabledReason = '请先填写邮箱';
|
||||||
|
} else if (step === 8 && !latestState?.localhostUrl) {
|
||||||
|
disabledReason = '尚未捕获 localhost 回调地址';
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.style.display = '';
|
||||||
|
btn.disabled = Boolean(disabledReason);
|
||||||
|
btn.title = disabledReason || `将步骤 ${step} 标记为已手动完成`;
|
||||||
|
});
|
||||||
|
|
||||||
|
updateStopButtonState(anyRunning || isAutoRunPausedPhase() || autoLocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStopButtonState(active) {
|
function updateStopButtonState(active) {
|
||||||
@@ -205,6 +391,12 @@ function updateStatusDisplay(state) {
|
|||||||
|
|
||||||
statusBar.className = 'status-bar';
|
statusBar.className = 'status-bar';
|
||||||
|
|
||||||
|
if (isAutoRunPausedPhase()) {
|
||||||
|
displayStatus.textContent = `自动已暂停${getAutoRunLabel()},等待邮箱后继续`;
|
||||||
|
statusBar.classList.add('paused');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
|
const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
|
||||||
if (running) {
|
if (running) {
|
||||||
displayStatus.textContent = `步骤 ${running[0]} 运行中...`;
|
displayStatus.textContent = `步骤 ${running[0]} 运行中...`;
|
||||||
@@ -212,6 +404,12 @@ function updateStatusDisplay(state) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAutoRunLockedPhase()) {
|
||||||
|
displayStatus.textContent = `${currentAutoRun.phase === 'retrying' ? '自动重试中' : '自动运行中'}${getAutoRunLabel()}`;
|
||||||
|
statusBar.classList.add('running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const failed = Object.entries(state.stepStatuses).find(([, s]) => s === 'failed');
|
const failed = Object.entries(state.stepStatuses).find(([, s]) => s === 'failed');
|
||||||
if (failed) {
|
if (failed) {
|
||||||
displayStatus.textContent = `步骤 ${failed[0]} 失败`;
|
displayStatus.textContent = `步骤 ${failed[0]} 失败`;
|
||||||
@@ -227,15 +425,17 @@ function updateStatusDisplay(state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lastCompleted = Object.entries(state.stepStatuses)
|
const lastCompleted = Object.entries(state.stepStatuses)
|
||||||
.filter(([, s]) => s === 'completed')
|
.filter(([, s]) => isDoneStatus(s))
|
||||||
.map(([k]) => Number(k))
|
.map(([k]) => Number(k))
|
||||||
.sort((a, b) => b - a)[0];
|
.sort((a, b) => b - a)[0];
|
||||||
|
|
||||||
if (lastCompleted === 9) {
|
if (lastCompleted === 9) {
|
||||||
displayStatus.textContent = '全部步骤已完成';
|
displayStatus.textContent = isDoneStatus(state.stepStatuses[9]) && state.stepStatuses[9] === 'manual_completed' ? '全部步骤已手动完成' : '全部步骤已完成';
|
||||||
statusBar.classList.add('completed');
|
statusBar.classList.add('completed');
|
||||||
} else if (lastCompleted) {
|
} else if (lastCompleted) {
|
||||||
displayStatus.textContent = `步骤 ${lastCompleted} 已完成`;
|
displayStatus.textContent = state.stepStatuses[lastCompleted] === 'manual_completed'
|
||||||
|
? `步骤 ${lastCompleted} 已手动完成`
|
||||||
|
: `步骤 ${lastCompleted} 已完成`;
|
||||||
} else {
|
} else {
|
||||||
displayStatus.textContent = '就绪';
|
displayStatus.textContent = '就绪';
|
||||||
}
|
}
|
||||||
@@ -306,26 +506,93 @@ function syncPasswordToggleLabel() {
|
|||||||
btnTogglePassword.textContent = inputPassword.type === 'password' ? '显示' : '隐藏';
|
btnTogglePassword.textContent = inputPassword.type === 'password' ? '显示' : '隐藏';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function maybeTakeoverAutoRun(actionLabel) {
|
||||||
|
if (!isAutoRunPausedPhase()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = confirm(`当前自动流程已暂停。若继续${actionLabel},将停止自动流程并切换为手动控制。是否继续?`);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await chrome.runtime.sendMessage({ type: 'TAKEOVER_AUTO_RUN', source: 'sidepanel', payload: {} });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleManualComplete(step) {
|
||||||
|
if (!(await maybeTakeoverAutoRun(`手动同步步骤 ${step}`))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === 3 && inputPassword.value !== (latestState?.customPassword || '')) {
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
type: 'SAVE_SETTING',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { customPassword: inputPassword.value },
|
||||||
|
});
|
||||||
|
syncLatestState({ customPassword: inputPassword.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = confirm(`这不会真正执行步骤 ${step},只会将面板状态同步为“已手动完成”。请确认你已经在网页中手动完成该步骤。`);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'MARK_STEP_MANUAL_COMPLETE',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { step },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`步骤 ${step} 已同步为手动完成`, 'success', 2200);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Button Handlers
|
// Button Handlers
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
document.querySelectorAll('.step-btn').forEach(btn => {
|
document.querySelectorAll('.step-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const step = Number(btn.dataset.step);
|
try {
|
||||||
if (step === 3) {
|
const step = Number(btn.dataset.step);
|
||||||
let email = inputEmail.value.trim();
|
if (!(await maybeTakeoverAutoRun(`执行步骤 ${step}`))) {
|
||||||
if (!email) {
|
return;
|
||||||
try {
|
}
|
||||||
email = await fetchDuckEmail({ showFailureToast: false });
|
if (step === 3) {
|
||||||
} catch (err) {
|
if (inputPassword.value !== (latestState?.customPassword || '')) {
|
||||||
showToast(`自动获取失败:${err.message},请手动粘贴邮箱后重试。`, 'warn');
|
await chrome.runtime.sendMessage({
|
||||||
return;
|
type: 'SAVE_SETTING',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { customPassword: inputPassword.value },
|
||||||
|
});
|
||||||
|
syncLatestState({ customPassword: inputPassword.value });
|
||||||
|
}
|
||||||
|
let email = inputEmail.value.trim();
|
||||||
|
if (!email) {
|
||||||
|
try {
|
||||||
|
email = await fetchDuckEmail({ showFailureToast: false });
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`自动获取失败:${err.message},请手动粘贴邮箱后重试。`, 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
|
||||||
|
if (response?.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
|
} catch (err) {
|
||||||
} else {
|
showToast(err.message, 'error');
|
||||||
await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -375,6 +642,8 @@ btnAutoContinue.addEventListener('click', async () => {
|
|||||||
btnReset.addEventListener('click', async () => {
|
btnReset.addEventListener('click', async () => {
|
||||||
if (confirm('确认重置全部步骤和数据吗?')) {
|
if (confirm('确认重置全部步骤和数据吗?')) {
|
||||||
await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
|
await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
|
||||||
|
syncLatestState({ stepStatuses: STEP_DEFAULT_STATUSES });
|
||||||
|
syncAutoRunState({ autoRunning: false, autoRunPhase: 'idle', autoRunCurrentRun: 0, autoRunTotalRuns: 1, autoRunAttemptRun: 0 });
|
||||||
displayOauthUrl.textContent = '等待中...';
|
displayOauthUrl.textContent = '等待中...';
|
||||||
displayOauthUrl.classList.remove('has-value');
|
displayOauthUrl.classList.remove('has-value');
|
||||||
displayLocalhostUrl.textContent = '等待中...';
|
displayLocalhostUrl.textContent = '等待中...';
|
||||||
@@ -385,10 +654,8 @@ btnReset.addEventListener('click', async () => {
|
|||||||
logArea.innerHTML = '';
|
logArea.innerHTML = '';
|
||||||
document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
|
document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
|
||||||
document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
|
document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
|
||||||
btnAutoRun.disabled = false;
|
setDefaultAutoRunButton();
|
||||||
inputRunCount.disabled = false;
|
applyAutoRunStatus(currentAutoRun);
|
||||||
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);
|
updateStopButtonState(false);
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
updateProgressCounter();
|
updateProgressCounter();
|
||||||
@@ -407,6 +674,8 @@ inputEmail.addEventListener('change', async () => {
|
|||||||
await chrome.runtime.sendMessage({ type: 'SAVE_EMAIL', source: 'sidepanel', payload: { email } });
|
await chrome.runtime.sendMessage({ type: 'SAVE_EMAIL', source: 'sidepanel', payload: { email } });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
inputEmail.addEventListener('input', updateButtonStates);
|
||||||
|
inputPassword.addEventListener('input', updateButtonStates);
|
||||||
|
|
||||||
inputVpsUrl.addEventListener('change', async () => {
|
inputVpsUrl.addEventListener('change', async () => {
|
||||||
const vpsUrl = inputVpsUrl.value.trim();
|
const vpsUrl = inputVpsUrl.value.trim();
|
||||||
@@ -479,9 +748,12 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
case 'STEP_STATUS_CHANGED': {
|
case 'STEP_STATUS_CHANGED': {
|
||||||
const { step, status } = message.payload;
|
const { step, status } = message.payload;
|
||||||
updateStepUI(step, status);
|
updateStepUI(step, status);
|
||||||
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay);
|
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => {
|
||||||
if (status === 'completed') {
|
syncLatestState(state);
|
||||||
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => {
|
syncAutoRunState(state);
|
||||||
|
updateStatusDisplay(latestState);
|
||||||
|
updateButtonStates();
|
||||||
|
if (status === 'completed' || status === 'manual_completed') {
|
||||||
syncPasswordField(state);
|
syncPasswordField(state);
|
||||||
if (state.oauthUrl) {
|
if (state.oauthUrl) {
|
||||||
displayOauthUrl.textContent = state.oauthUrl;
|
displayOauthUrl.textContent = state.oauthUrl;
|
||||||
@@ -491,13 +763,22 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
displayLocalhostUrl.textContent = state.localhostUrl;
|
displayLocalhostUrl.textContent = state.localhostUrl;
|
||||||
displayLocalhostUrl.classList.add('has-value');
|
displayLocalhostUrl.classList.add('has-value');
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
).catch(() => {});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'AUTO_RUN_RESET': {
|
case 'AUTO_RUN_RESET': {
|
||||||
// Full UI reset for next run
|
// Full UI reset for next run
|
||||||
|
syncLatestState({
|
||||||
|
oauthUrl: null,
|
||||||
|
localhostUrl: null,
|
||||||
|
email: null,
|
||||||
|
password: null,
|
||||||
|
stepStatuses: STEP_DEFAULT_STATUSES,
|
||||||
|
logs: [],
|
||||||
|
});
|
||||||
displayOauthUrl.textContent = '等待中...';
|
displayOauthUrl.textContent = '等待中...';
|
||||||
displayOauthUrl.classList.remove('has-value');
|
displayOauthUrl.classList.remove('has-value');
|
||||||
displayLocalhostUrl.textContent = '等待中...';
|
displayLocalhostUrl.textContent = '等待中...';
|
||||||
@@ -508,12 +789,14 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
logArea.innerHTML = '';
|
logArea.innerHTML = '';
|
||||||
document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
|
document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
|
||||||
document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
|
document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
|
||||||
updateStopButtonState(false);
|
applyAutoRunStatus(currentAutoRun);
|
||||||
updateProgressCounter();
|
updateProgressCounter();
|
||||||
|
updateButtonStates();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'DATA_UPDATED': {
|
case 'DATA_UPDATED': {
|
||||||
|
syncLatestState(message.payload);
|
||||||
if (message.payload.email) {
|
if (message.payload.email) {
|
||||||
inputEmail.value = message.payload.email;
|
inputEmail.value = message.payload.email;
|
||||||
}
|
}
|
||||||
@@ -532,38 +815,16 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'AUTO_RUN_STATUS': {
|
case 'AUTO_RUN_STATUS': {
|
||||||
const { phase, currentRun, totalRuns, attemptRun } = message.payload;
|
syncLatestState({
|
||||||
const attemptLabel = attemptRun ? ` · 尝试${attemptRun}` : '';
|
autoRunning: ['running', 'waiting_email', 'retrying'].includes(message.payload.phase),
|
||||||
const runLabel = totalRuns > 1 ? ` (${currentRun}/${totalRuns}${attemptLabel})` : (attemptLabel ? ` (${attemptLabel.slice(3)})` : '');
|
autoRunPhase: message.payload.phase,
|
||||||
switch (phase) {
|
autoRunCurrentRun: message.payload.currentRun,
|
||||||
case 'waiting_email':
|
autoRunTotalRuns: message.payload.totalRuns,
|
||||||
autoContinueBar.style.display = 'flex';
|
autoRunAttemptRun: message.payload.attemptRun,
|
||||||
btnAutoRun.innerHTML = `已暂停${runLabel}`;
|
});
|
||||||
updateStopButtonState(true);
|
applyAutoRunStatus(message.payload);
|
||||||
break;
|
updateStatusDisplay(latestState);
|
||||||
case 'running':
|
updateButtonStates();
|
||||||
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> 自动';
|
|
||||||
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> 自动';
|
|
||||||
autoContinueBar.style.display = 'none';
|
|
||||||
updateStopButtonState(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -598,8 +859,10 @@ btnTheme.addEventListener('click', () => {
|
|||||||
// Init
|
// Init
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
initializeManualStepActions();
|
||||||
initTheme();
|
initTheme();
|
||||||
restoreState().then(() => {
|
restoreState().then(() => {
|
||||||
syncPasswordToggleLabel();
|
syncPasswordToggleLabel();
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
|
updateStatusDisplay(latestState);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user