From cdf46cde55ce5cbcd2d555a83ecbeab4f4cb6a9b Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Wed, 8 Apr 2026 10:52:36 +0800 Subject: [PATCH] =?UTF-8?q?Auto=20=E6=AD=A3=E5=9C=A8=E7=9C=9F=E6=AD=A3?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=88=96=E9=87=8D=E8=AF=95=E6=97=B6=EF=BC=8C?= =?UTF-8?q?=E6=AD=A5=E9=AA=A4=E6=8C=89=E9=92=AE=E5=92=8C=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=8C=89=E9=92=AE=E4=B8=8D=E4=BC=9A=E6=94=BE?= =?UTF-8?q?=E5=BC=80=E3=80=82=20Auto=20=E5=A4=84=E4=BA=8E=E6=9A=82?= =?UTF-8?q?=E5=81=9C=E7=AD=89=E5=BE=85=E9=82=AE=E7=AE=B1=E6=97=B6=EF=BC=8C?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E7=BB=A7=E7=BB=AD=EF=BC=8C=E4=B9=9F=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E6=98=BE=E5=BC=8F=E6=8E=A5=E7=AE=A1=E4=B8=BA=E6=89=8B?= =?UTF-8?q?=E5=8A=A8=E3=80=82=20Step=201=20=E5=92=8C=20Step=209=20?= =?UTF-8?q?=E4=B8=8D=E6=8F=90=E4=BE=9B=E6=89=8B=E5=8A=A8=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E3=80=82=20Step=203=20=E4=BC=9A=E8=A6=81?= =?UTF-8?q?=E6=B1=82=E5=B7=B2=E7=9F=A5=E9=82=AE=E7=AE=B1=E5=92=8C=E5=AF=86?= =?UTF-8?q?=E7=A0=81=EF=BC=9BStep=208=20=E4=BC=9A=E8=A6=81=E6=B1=82?= =?UTF-8?q?=E5=B7=B2=E7=BB=8F=E6=8D=95=E8=8E=B7=20localhostUrl=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +- background.js | 326 +++++++++++++++++++++++++++---- content/signup-page.js | 62 ++++++ sidepanel/sidepanel.css | 47 +++++ sidepanel/sidepanel.js | 411 ++++++++++++++++++++++++++++++++-------- 5 files changed, 746 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 15046cb..0db9326 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,8 @@ Step 3 使用的注册邮箱。 - 如果 Duck 邮箱可自动获取,整套流程更接近全自动 - 如果不能自动获取,Auto 会在邮箱阶段暂停 +- Auto 的暂停状态会保存在会话状态中,重新打开侧边栏后仍可继续 +- 如果你在 Auto 暂停时改为手动点步骤或手动同步步骤,面板会先确认并停止 Auto,再切回手动控制 ## 详细步骤说明 @@ -281,6 +283,7 @@ Step 3 使用的注册邮箱。 - 卡在某一步 - 邮件迟迟不来 - 页面结构变化导致等待超时 +- Auto 暂停后,明确放弃 Auto、改为手动接管 ## 状态与数据 @@ -295,6 +298,7 @@ Step 3 使用的注册邮箱。 - 账号记录 - tab 注册信息 - 自定义设置 +- Auto 当前阶段、当前轮次、暂停信息 特点: @@ -346,7 +350,14 @@ sidepanel/ 侧边栏 UI - 手动点 `Step 3` 时,如果邮箱为空,脚本会先自动尝试获取 Duck 邮箱;失败后再改为手填 - Auto 暂停时,仍可手动粘贴邮箱后点击 `Continue` -### 4. Step 8 失败时重点检查 +### 4. 页面已手动完成时同步面板状态 + +- 每个可同步的步骤右侧会出现一个小按钮,用来把该步标记为“已手动完成” +- 点击后会先弹窗确认;它不会真正执行脚本,只会在校验当前页面已进入下一阶段后,放行后续步骤 +- 该按钮只会在合理边界内显示;例如最后一步不会显示,依赖数据缺失时会禁用 +- 如果 Auto 处于暂停状态,点击该按钮会先确认是否接管 Auto + +### 5. Step 8 失败时重点检查 - OAuth 同意页 DOM 是否变化 - “继续”按钮是否变成了别的文案 diff --git a/background.js b/background.js index a6ec585..ad43bbc 100644 --- a/background.js +++ b/background.js @@ -35,6 +35,11 @@ const DEFAULT_STATE = { vpsPassword: '', customPassword: '', autoRunSkipFailures: false, + autoRunning: false, + autoRunPhase: 'idle', + autoRunCurrentRun: 0, + autoRunTotalRuns: 1, + autoRunAttemptRun: 0, mailProvider: '163', // 'qq' or '163' inbucketHost: '', inbucketMailbox: '', @@ -425,10 +430,197 @@ function isStopError(error) { return message === STOP_ERROR_MESSAGE; } +function isStepDoneStatus(status) { + return status === 'completed' || status === 'manual_completed'; +} + function clearStopRequest() { 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() { if (stopRequested) { throw new Error(STOP_ERROR_MESSAGE); @@ -590,6 +782,9 @@ async function handleMessage(message, sender) { case 'EXECUTE_STEP': { clearStopRequest(); + if (message.source === 'sidepanel') { + await ensureManualInteractionAllowed('手动执行步骤'); + } const step = message.payload.step; // Save email if provided (from side panel step 3) if (message.payload.email) { @@ -617,6 +812,17 @@ async function handleMessage(message, sender) { 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': { const updates = {}; if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl; @@ -632,12 +838,20 @@ async function handleMessage(message, sender) { // Side panel data updates case 'SAVE_EMAIL': { + const state = await getState(); + if (isAutoRunLockedState(state)) { + throw new Error('自动流程运行中,当前不能手动修改邮箱。'); + } await setEmailState(message.payload.email); return { ok: true, email: message.payload.email }; } case 'FETCH_DUCK_EMAIL': { clearStopRequest(); + const state = await getState(); + if (isAutoRunLockedState(state)) { + throw new Error('自动流程运行中,当前不能手动获取 Duck 邮箱。'); + } const email = await fetchDuckEmail(message.payload || {}); return { ok: true, email }; } @@ -737,7 +951,8 @@ async function markRunningStepsStopped() { } } -async function requestStop() { +async function requestStop(options = {}) { + const { logMessage = '已收到停止请求,正在取消当前操作...' } = options; if (stopRequested) return; stopRequested = true; @@ -747,7 +962,7 @@ async function requestStop() { webNavListener = null; } - await addLog('已收到停止请求,正在取消当前操作...', 'warn'); + await addLog(logMessage, 'warn'); await broadcastStopToContentScripts(); for (const waiter of stepWaiters.values()) { @@ -762,11 +977,11 @@ async function requestStop() { await markRunningStepsStopped(); autoRunActive = false; - await setState({ autoRunning: false }); - chrome.runtime.sendMessage({ - type: 'AUTO_RUN_STATUS', - payload: { phase: 'stopped', currentRun: autoRunCurrentRun, totalRuns: autoRunTotalRuns }, - }).catch(() => {}); + await broadcastAutoRunStatus('stopped', { + currentRun: autoRunCurrentRun, + totalRuns: autoRunTotalRuns, + attemptRun: autoRunAttemptRun, + }); } // ============================================================ @@ -861,6 +1076,7 @@ async function fetchDuckEmail(options = {}) { let autoRunActive = false; let autoRunCurrentRun = 0; let autoRunTotalRuns = 1; +let autoRunAttemptRun = 0; // Outer loop: keep retrying until the target number of successful runs is reached. async function autoRunLoop(totalRuns, options = {}) { @@ -872,18 +1088,24 @@ async function autoRunLoop(totalRuns, options = {}) { clearStopRequest(); autoRunActive = true; autoRunTotalRuns = totalRuns; + autoRunCurrentRun = 0; + autoRunAttemptRun = 0; const autoRunSkipFailures = Boolean(options.autoRunSkipFailures); const maxAttempts = autoRunSkipFailures ? Math.max(totalRuns * 10, totalRuns + 20) : totalRuns; let successfulRuns = 0; let attemptRuns = 0; let forceFreshTabsNextRun = false; - await setState({ autoRunning: true, autoRunSkipFailures }); + await setState({ + autoRunSkipFailures, + ...getAutoRunStatusPayload('running', { currentRun: 0, totalRuns, attemptRun: 0 }), + }); while (successfulRuns < totalRuns && attemptRuns < maxAttempts) { attemptRuns += 1; const targetRun = successfulRuns + 1; autoRunCurrentRun = targetRun; + autoRunAttemptRun = attemptRuns; // Reset everything at the start of each attempt (keep user settings). const prevState = await getState(); @@ -895,7 +1117,7 @@ async function autoRunLoop(totalRuns, options = {}) { mailProvider: prevState.mailProvider, inbucketHost: prevState.inbucketHost, inbucketMailbox: prevState.inbucketMailbox, - autoRunning: true, + ...getAutoRunStatusPayload('running', { currentRun: targetRun, totalRuns, attemptRun: attemptRuns }), ...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}), }; await resetState(); @@ -908,16 +1130,15 @@ async function autoRunLoop(totalRuns, options = {}) { forceFreshTabsNextRun = false; } - const status = (phase) => ({ - type: 'AUTO_RUN_STATUS', - payload: { phase, currentRun: targetRun, totalRuns, attemptRun: attemptRuns }, - }); - - await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info'); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info'); try { throwIfStopped(); - chrome.runtime.sendMessage(status('running')).catch(() => {}); + await broadcastAutoRunStatus('running', { + currentRun: targetRun, + totalRuns, + attemptRun: attemptRuns, + }); await executeStepAndWait(1, 2000); await executeStepAndWait(2, 2000); @@ -933,7 +1154,11 @@ async function autoRunLoop(totalRuns, options = {}) { if (!emailReady) { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先获取 Duck 邮箱或手动粘贴邮箱,然后继续 ===`, 'warn'); - chrome.runtime.sendMessage(status('waiting_email')).catch(() => {}); + await broadcastAutoRunStatus('waiting_email', { + currentRun: targetRun, + totalRuns, + attemptRun: attemptRuns, + }); await waitForResume(); @@ -944,7 +1169,11 @@ async function autoRunLoop(totalRuns, options = {}) { } 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'); if (signupTabId) { @@ -966,13 +1195,21 @@ async function autoRunLoop(totalRuns, options = {}) { } catch (err) { if (isStopError(err)) { await addLog(`目标 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn'); - chrome.runtime.sendMessage(status('stopped')).catch(() => {}); + await broadcastAutoRunStatus('stopped', { + currentRun: targetRun, + totalRuns, + attemptRun: attemptRuns, + }); break; } if (!autoRunSkipFailures) { await addLog(`目标 ${targetRun}/${totalRuns} 轮失败:${err.message}`, 'error'); - chrome.runtime.sendMessage(status('stopped')).catch(() => {}); + await broadcastAutoRunStatus('stopped', { + currentRun: targetRun, + totalRuns, + attemptRun: attemptRuns, + }); break; } @@ -980,38 +1217,51 @@ async function autoRunLoop(totalRuns, options = {}) { await addLog('兜底开关已开启:将放弃当前线程,重新开一轮继续补足目标次数。', 'warn'); cancelPendingCommands('当前尝试已放弃。'); await broadcastStopToContentScripts(); - chrome.runtime.sendMessage(status('retrying')).catch(() => {}); + await broadcastAutoRunStatus('retrying', { + currentRun: targetRun, + totalRuns, + attemptRun: attemptRuns, + }); forceFreshTabsNextRun = true; } } 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(() => {}); + await broadcastAutoRunStatus('stopped', { + currentRun: successfulRuns, + totalRuns: autoRunTotalRuns, + attemptRun: attemptRuns, + }); } 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(() => {}); + await broadcastAutoRunStatus('stopped', { + currentRun: successfulRuns, + totalRuns: autoRunTotalRuns, + attemptRun: attemptRuns, + }); } 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(() => {}); + await broadcastAutoRunStatus('complete', { + currentRun: successfulRuns, + totalRuns: autoRunTotalRuns, + attemptRun: attemptRuns, + }); } else { await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn'); - chrome.runtime.sendMessage({ - type: 'AUTO_RUN_STATUS', - payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns }, - }).catch(() => {}); + await broadcastAutoRunStatus('stopped', { + currentRun: successfulRuns, + totalRuns: autoRunTotalRuns, + attemptRun: attemptRuns, + }); } autoRunActive = false; - await setState({ autoRunning: false }); + autoRunAttemptRun = attemptRuns; + await setState(getAutoRunStatusPayload(stopRequested ? 'stopped' : (successfulRuns >= autoRunTotalRuns ? 'complete' : 'stopped'), { + currentRun: successfulRuns, + totalRuns: autoRunTotalRuns, + attemptRun: attemptRuns, + })); clearStopRequest(); } diff --git a/content/signup-page.js b/content/signup-page.js index dfd9764..71ebfa6 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -11,6 +11,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { || message.type === 'STEP8_FIND_AND_CLICK' || message.type === 'PREPARE_LOGIN_CODE' || message.type === 'RESEND_VERIFICATION_CODE' + || message.type === 'GET_MANUAL_COMPLETION_STATE' ) { resetStopState(); handleCommand(message).then((result) => { @@ -22,6 +23,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return; } + if (message.type === 'GET_MANUAL_COMPLETION_STATE') { + sendResponse({ error: err.message }); + return; + } + if (message.type === 'STEP8_FIND_AND_CLICK') { log(`步骤 8:${err.message}`, 'error'); sendResponse({ error: err.message }); @@ -53,6 +59,8 @@ async function handleCommand(message) { return await prepareLoginCodeFlow(); case 'RESEND_VERIFICATION_CODE': return await resendVerificationCode(message.step); + case 'GET_MANUAL_COMPLETION_STATE': + return getManualCompletionState(); case 'STEP8_FIND_AND_CLICK': return await step8_findAndClick(); } @@ -145,6 +153,25 @@ function findOneTimeCodeLoginTrigger() { 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 } = {}) { const candidates = document.querySelectorAll( '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() { return (document.body?.innerText || document.body?.textContent || '') .replace(/\s+/g, ' ') @@ -443,6 +482,29 @@ function isStep8Ready() { 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) { const resolvedTimeout = timeout ?? (step === 7 ? 30000 : 12000); const start = Date.now(); diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index da6895a..9a4a501 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -386,6 +386,12 @@ header { .status-bar.stopped .status-dot { background: 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 { 0%, 100% { opacity: 1; transform: scale(1); } 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-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-btn { 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.failed .step-btn { border-color: var(--red); color: var(--red); } .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 { width: 20px; @@ -519,6 +565,7 @@ header { .step-row.completed .step-status { color: var(--green); } .step-row.failed .step-status { color: var(--red); } .step-row.stopped .step-status { color: var(--cyan); } +.step-row.manual_completed .step-status { color: var(--blue); } /* ============================================================ Log / Console Section diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 11a4564..56009c8 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -6,6 +6,7 @@ const STATUS_ICONS = { completed: '\u2713', // ✓ failed: '\u2717', // ✗ stopped: '\u25A0', // ■ + manual_completed: '手', }; 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 inputRunCount = document.getElementById('input-run-count'); 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 @@ -73,6 +95,133 @@ function dismissToast(toast) { 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 = ' 自动'; +} + +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 = ''; + 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 // ============================================================ @@ -80,6 +229,8 @@ function dismissToast(toast) { async function restoreState() { try { const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }); + syncLatestState(state); + syncAutoRunState(state); if (state.oauthUrl) { displayOauthUrl.textContent = state.oauthUrl; @@ -122,9 +273,11 @@ async function restoreState() { } } - updateStatusDisplay(state); + applyAutoRunStatus(state); + updateStatusDisplay(latestState); updateProgressCounter(); updateMailProviderUI(); + updateButtonStates(); } catch (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 row = document.querySelector(`.step-row[data-step="${step}"]`); + syncLatestState({ + stepStatuses: { + ...getStepStatuses(), + [step]: status, + }, + }); + if (statusEl) statusEl.textContent = STATUS_ICONS[status] || ''; if (row) { row.className = `step-row ${status}`; @@ -158,42 +318,68 @@ function updateStepUI(step, status) { } function updateProgressCounter() { - let completed = 0; - document.querySelectorAll('.step-row').forEach(row => { - if (row.classList.contains('completed')) completed++; - }); + const completed = Object.values(getStepStatuses()).filter(isDoneStatus).length; stepsProgress.textContent = `${completed} / 9`; } function updateButtonStates() { - const statuses = {}; - 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 statuses = getStepStatuses(); const anyRunning = Object.values(statuses).some(s => s === 'running'); + const autoLocked = isAutoRunLockedPhase(); for (let step = 1; step <= 9; step++) { const btn = document.querySelector(`.step-btn[data-step="${step}"]`); if (!btn) continue; - if (anyRunning) { + if (anyRunning || autoLocked) { btn.disabled = true; } else if (step === 1) { btn.disabled = false; } else { const prevStatus = statuses[step - 1]; 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) { @@ -205,6 +391,12 @@ function updateStatusDisplay(state) { statusBar.className = 'status-bar'; + if (isAutoRunPausedPhase()) { + displayStatus.textContent = `自动已暂停${getAutoRunLabel()},等待邮箱后继续`; + statusBar.classList.add('paused'); + return; + } + const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running'); if (running) { displayStatus.textContent = `步骤 ${running[0]} 运行中...`; @@ -212,6 +404,12 @@ function updateStatusDisplay(state) { return; } + if (isAutoRunLockedPhase()) { + displayStatus.textContent = `${currentAutoRun.phase === 'retrying' ? '自动重试中' : '自动运行中'}${getAutoRunLabel()}`; + statusBar.classList.add('running'); + return; + } + const failed = Object.entries(state.stepStatuses).find(([, s]) => s === 'failed'); if (failed) { displayStatus.textContent = `步骤 ${failed[0]} 失败`; @@ -227,15 +425,17 @@ function updateStatusDisplay(state) { } const lastCompleted = Object.entries(state.stepStatuses) - .filter(([, s]) => s === 'completed') + .filter(([, s]) => isDoneStatus(s)) .map(([k]) => Number(k)) .sort((a, b) => b - a)[0]; if (lastCompleted === 9) { - displayStatus.textContent = '全部步骤已完成'; + displayStatus.textContent = isDoneStatus(state.stepStatuses[9]) && state.stepStatuses[9] === 'manual_completed' ? '全部步骤已手动完成' : '全部步骤已完成'; statusBar.classList.add('completed'); } else if (lastCompleted) { - displayStatus.textContent = `步骤 ${lastCompleted} 已完成`; + displayStatus.textContent = state.stepStatuses[lastCompleted] === 'manual_completed' + ? `步骤 ${lastCompleted} 已手动完成` + : `步骤 ${lastCompleted} 已完成`; } else { displayStatus.textContent = '就绪'; } @@ -306,26 +506,93 @@ function syncPasswordToggleLabel() { 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 // ============================================================ document.querySelectorAll('.step-btn').forEach(btn => { btn.addEventListener('click', async () => { - const step = Number(btn.dataset.step); - if (step === 3) { - let email = inputEmail.value.trim(); - if (!email) { - try { - email = await fetchDuckEmail({ showFailureToast: false }); - } catch (err) { - showToast(`自动获取失败:${err.message},请手动粘贴邮箱后重试。`, 'warn'); - return; + try { + const step = Number(btn.dataset.step); + if (!(await maybeTakeoverAutoRun(`执行步骤 ${step}`))) { + return; + } + if (step === 3) { + if (inputPassword.value !== (latestState?.customPassword || '')) { + await chrome.runtime.sendMessage({ + 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 } }); - } else { - await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } }); + } catch (err) { + showToast(err.message, 'error'); } }); }); @@ -375,6 +642,8 @@ btnAutoContinue.addEventListener('click', async () => { btnReset.addEventListener('click', async () => { if (confirm('确认重置全部步骤和数据吗?')) { 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.classList.remove('has-value'); displayLocalhostUrl.textContent = '等待中...'; @@ -385,10 +654,8 @@ btnReset.addEventListener('click', async () => { 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 = ' 自动'; - autoContinueBar.style.display = 'none'; + setDefaultAutoRunButton(); + applyAutoRunStatus(currentAutoRun); updateStopButtonState(false); updateButtonStates(); updateProgressCounter(); @@ -407,6 +674,8 @@ inputEmail.addEventListener('change', async () => { await chrome.runtime.sendMessage({ type: 'SAVE_EMAIL', source: 'sidepanel', payload: { email } }); } }); +inputEmail.addEventListener('input', updateButtonStates); +inputPassword.addEventListener('input', updateButtonStates); inputVpsUrl.addEventListener('change', async () => { const vpsUrl = inputVpsUrl.value.trim(); @@ -479,9 +748,12 @@ chrome.runtime.onMessage.addListener((message) => { case 'STEP_STATUS_CHANGED': { const { step, status } = message.payload; updateStepUI(step, status); - chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay); - if (status === 'completed') { - chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => { + chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => { + syncLatestState(state); + syncAutoRunState(state); + updateStatusDisplay(latestState); + updateButtonStates(); + if (status === 'completed' || status === 'manual_completed') { syncPasswordField(state); if (state.oauthUrl) { displayOauthUrl.textContent = state.oauthUrl; @@ -491,13 +763,22 @@ chrome.runtime.onMessage.addListener((message) => { displayLocalhostUrl.textContent = state.localhostUrl; displayLocalhostUrl.classList.add('has-value'); } - }); + } } + ).catch(() => {}); break; } case 'AUTO_RUN_RESET': { // Full UI reset for next run + syncLatestState({ + oauthUrl: null, + localhostUrl: null, + email: null, + password: null, + stepStatuses: STEP_DEFAULT_STATUSES, + logs: [], + }); displayOauthUrl.textContent = '等待中...'; displayOauthUrl.classList.remove('has-value'); displayLocalhostUrl.textContent = '等待中...'; @@ -508,12 +789,14 @@ chrome.runtime.onMessage.addListener((message) => { logArea.innerHTML = ''; document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row'); document.querySelectorAll('.step-status').forEach(el => el.textContent = ''); - updateStopButtonState(false); + applyAutoRunStatus(currentAutoRun); updateProgressCounter(); + updateButtonStates(); break; } case 'DATA_UPDATED': { + syncLatestState(message.payload); if (message.payload.email) { inputEmail.value = message.payload.email; } @@ -532,38 +815,16 @@ chrome.runtime.onMessage.addListener((message) => { } case 'AUTO_RUN_STATUS': { - 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 = `已暂停${runLabel}`; - updateStopButtonState(true); - break; - case 'running': - btnAutoRun.innerHTML = `运行中${runLabel}`; - updateStopButtonState(true); - break; - case 'retrying': - btnAutoRun.innerHTML = `重试中${runLabel}`; - updateStopButtonState(true); - break; - case 'complete': - btnAutoRun.disabled = false; - inputRunCount.disabled = false; - btnAutoRun.innerHTML = ' 自动'; - autoContinueBar.style.display = 'none'; - updateStopButtonState(false); - break; - case 'stopped': - btnAutoRun.disabled = false; - inputRunCount.disabled = false; - btnAutoRun.innerHTML = ' 自动'; - autoContinueBar.style.display = 'none'; - updateStopButtonState(false); - break; - } + syncLatestState({ + autoRunning: ['running', 'waiting_email', 'retrying'].includes(message.payload.phase), + autoRunPhase: message.payload.phase, + autoRunCurrentRun: message.payload.currentRun, + autoRunTotalRuns: message.payload.totalRuns, + autoRunAttemptRun: message.payload.attemptRun, + }); + applyAutoRunStatus(message.payload); + updateStatusDisplay(latestState); + updateButtonStates(); break; } } @@ -598,8 +859,10 @@ btnTheme.addEventListener('click', () => { // Init // ============================================================ +initializeManualStepActions(); initTheme(); restoreState().then(() => { syncPasswordToggleLabel(); updateButtonStates(); + updateStatusDisplay(latestState); });