Răsfoiți Sursa

feat: 添加跳过步骤功能,优化手动完成交互,更新状态显示

QLHazyCoder 4 luni în urmă
părinte
comite
40a006bd1e
5 a modificat fișierele cu 44 adăugiri și 226 ștergeri
  1. 5 5
      README.md
  2. 11 115
      background.js
  3. 0 61
      content/signup-page.js
  4. 8 4
      sidepanel/sidepanel.css
  5. 20 41
      sidepanel/sidepanel.js

+ 5 - 5
README.md

@@ -160,7 +160,7 @@ Step 3 使用的注册邮箱。
 - 如果 Duck 邮箱可自动获取,整套流程更接近全自动
 - 如果不能自动获取,Auto 会在邮箱阶段暂停
 - Auto 的暂停状态会保存在会话状态中,重新打开侧边栏后仍可继续
-- 如果你在 Auto 暂停时改为手动点步骤或手动同步步骤,面板会先确认并停止 Auto,再切回手动控制
+- 如果你在 Auto 暂停时改为手动点步骤或跳过步骤,面板会先确认并停止 Auto,再切回手动控制
 
 ## 详细步骤说明
 
@@ -363,11 +363,11 @@ sidepanel/                 侧边栏 UI
 - 手动点 `Step 3` 时,如果邮箱为空,脚本会先自动尝试获取 Duck 邮箱;失败后再改为手填
 - Auto 暂停时,仍可手动粘贴邮箱后点击 `Continue`
 
-### 4. 页面已手动完成时同步面板状态
+### 4. 跳过步骤
 
-- 每个可同步的步骤右侧会出现一个小按钮,用来把该步标记为“已手动完成”
-- 点击后会先弹窗确认;它不会真正执行脚本,只会在校验当前页面已进入下一阶段后,放行后续步骤
-- 该按钮只会在合理边界内显示;例如最后一步不会显示,依赖数据缺失时会禁用
+- 每个步骤右侧都会在满足顺序条件时出现一个小按钮,用来直接跳过该步骤
+- 点击后会先弹窗确认;它不会真正执行脚本,只会把该步骤状态改为“已跳过”,从而放行后续步骤
+- 跳过按钮的规则很简单:只要上一步已完成、当前步骤没在运行,就可以使用;Step 1 没有前置步骤,也可直接跳过
 - 如果 Auto 处于暂停状态,点击该按钮会先确认是否接管 Auto
 
 ### 5. Step 8 失败时重点检查

+ 11 - 115
background.js

@@ -537,7 +537,7 @@ function isStopError(error) {
 }
 
 function isStepDoneStatus(status) {
-  return status === 'completed' || status === 'manual_completed';
+  return status === 'completed' || status === 'manual_completed' || status === 'skipped';
 }
 
 function clearStopRequest() {
@@ -595,136 +595,32 @@ async function ensureManualInteractionAllowed(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('手动同步步骤');
+async function skipStep(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} 正在运行中,不能手动同步。`);
+    throw new Error(`步骤 ${step} 正在运行中,不能跳过。`);
   }
   if (isStepDoneStatus(currentStatus)) {
-    throw new Error(`步骤 ${step} 已完成,无需再手动同步。`);
+    throw new Error(`步骤 ${step} 已完成,无需再跳过。`);
   }
 
   if (step > 1) {
     const prevStatus = statuses[step - 1];
     if (!isStepDoneStatus(prevStatus)) {
-      throw new Error(`请先完成步骤 ${step - 1},再同步步骤 ${step}。`);
+      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' };
+  await setStepStatus(step, 'skipped');
+  await addLog(`步骤 ${step} 已跳过`, 'warn');
+  return { ok: true, step, status: 'skipped' };
 }
 
 function throwIfStopped() {
@@ -924,9 +820,9 @@ async function handleMessage(message, sender) {
       return { ok: true };
     }
 
-    case 'MARK_STEP_MANUAL_COMPLETE': {
+    case 'SKIP_STEP': {
       const step = Number(message.payload?.step);
-      return await markStepManualComplete(step);
+      return await skipStep(step);
     }
 
     case 'SAVE_SETTING': {

+ 0 - 61
content/signup-page.js

@@ -11,7 +11,6 @@ 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) => {
@@ -23,11 +22,6 @@ 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 });
@@ -59,8 +53,6 @@ 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();
   }
@@ -153,25 +145,6 @@ 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"]'
@@ -421,18 +394,6 @@ 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, ' ')
@@ -482,28 +443,6 @@ 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);

+ 8 - 4
sidepanel/sidepanel.css

@@ -521,8 +521,10 @@ 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-row.manual_completed .step-indicator,
+.step-row.skipped .step-indicator { border-color: var(--blue); background: var(--blue-soft); }
+.step-row.manual_completed .step-num,
+.step-row.skipped .step-num { color: var(--blue); }
 
 /* Step Button */
 .step-btn {
@@ -547,7 +549,8 @@ 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-row.manual_completed .step-btn,
+.step-row.skipped .step-btn { border-color: rgba(37, 99, 235, 0.25); color: var(--blue); opacity: 0.82; }
 
 .step-actions {
   display: flex;
@@ -595,7 +598,8 @@ 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); }
+.step-row.manual_completed .step-status,
+.step-row.skipped .step-status { color: var(--blue); }
 
 /* ============================================================
    Log / Console Section

+ 20 - 41
sidepanel/sidepanel.js

@@ -6,7 +6,8 @@ const STATUS_ICONS = {
   completed: '\u2713',  // ✓
   failed: '\u2717',     // ✗
   stopped: '\u25A0',    // ■
-  manual_completed: '手',
+  manual_completed: '跳',
+  skipped: '跳',
 };
 
 const logArea = document.getElementById('log-area');
@@ -46,7 +47,7 @@ const STEP_DEFAULT_STATUSES = {
   8: 'pending',
   9: 'pending',
 };
-const MANUAL_COMPLETION_STEPS = new Set([2, 3, 4, 5, 6, 7, 8]);
+const SKIPPABLE_STEPS = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]);
 
 let latestState = null;
 let currentAutoRun = {
@@ -103,7 +104,7 @@ function dismissToast(toast) {
 }
 
 function isDoneStatus(status) {
-  return status === 'completed' || status === 'manual_completed';
+  return status === 'completed' || status === 'manual_completed' || status === 'skipped';
 }
 
 function getStepStatuses(state = latestState) {
@@ -282,13 +283,13 @@ function initializeManualStepActions() {
     manualBtn.type = 'button';
     manualBtn.className = 'step-manual-btn';
     manualBtn.dataset.step = String(step);
-    manualBtn.title = '标记此步已手动完成';
-    manualBtn.setAttribute('aria-label', `标记步骤 ${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);
+        await handleSkipStep(step);
       } catch (err) {
         showToast(err.message, 'error');
       }
@@ -426,10 +427,10 @@ function updateButtonStates() {
     const currentStatus = statuses[step];
     const prevStatus = statuses[step - 1];
 
-    if (!MANUAL_COMPLETION_STEPS.has(step) || anyRunning || autoLocked || currentStatus === 'running' || isDoneStatus(currentStatus)) {
+    if (!SKIPPABLE_STEPS.has(step) || anyRunning || autoLocked || currentStatus === 'running' || isDoneStatus(currentStatus)) {
       btn.style.display = 'none';
       btn.disabled = true;
-      btn.title = '当前不可手动同步';
+      btn.title = '当前不可跳过';
       return;
     }
 
@@ -440,22 +441,9 @@ function updateButtonStates() {
       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} 标记为已手动完成`;
+    btn.disabled = false;
+    btn.title = `跳过步骤 ${step}`;
   });
 
   updateStopButtonState(anyRunning || isAutoRunPausedPhase() || autoLocked);
@@ -509,11 +497,11 @@ function updateStatusDisplay(state) {
     .sort((a, b) => b - a)[0];
 
   if (lastCompleted === 9) {
-    displayStatus.textContent = isDoneStatus(state.stepStatuses[9]) && state.stepStatuses[9] === 'manual_completed' ? '全部步骤已手动完成' : '全部步骤已完成';
+    displayStatus.textContent = (state.stepStatuses[9] === 'manual_completed' || state.stepStatuses[9] === 'skipped') ? '全部步骤已跳过/完成' : '全部步骤已完成';
     statusBar.classList.add('completed');
   } else if (lastCompleted) {
-    displayStatus.textContent = state.stepStatuses[lastCompleted] === 'manual_completed'
-      ? `步骤 ${lastCompleted} 已手动完成`
+    displayStatus.textContent = (state.stepStatuses[lastCompleted] === 'manual_completed' || state.stepStatuses[lastCompleted] === 'skipped')
+      ? `步骤 ${lastCompleted} 已跳过`
       : `步骤 ${lastCompleted} 已完成`;
   } else {
     displayStatus.textContent = '就绪';
@@ -602,27 +590,18 @@ async function maybeTakeoverAutoRun(actionLabel) {
   return true;
 }
 
-async function handleManualComplete(step) {
-  if (!(await maybeTakeoverAutoRun(`手动同步步骤 ${step}`))) {
+async function handleSkipStep(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},只会将面板状态同步为“已手动完成”。请确认你已经在网页中手动完成该步骤。`);
+  const confirmed = confirm(`这不会真正执行步骤 ${step},只会直接跳过该步骤并放行后续步骤。是否继续?`);
   if (!confirmed) {
     return;
   }
 
   const response = await chrome.runtime.sendMessage({
-    type: 'MARK_STEP_MANUAL_COMPLETE',
+    type: 'SKIP_STEP',
     source: 'sidepanel',
     payload: { step },
   });
@@ -631,7 +610,7 @@ async function handleManualComplete(step) {
     throw new Error(response.error);
   }
 
-  showToast(`步骤 ${step} 已同步为手动完成`, 'success', 2200);
+  showToast(`步骤 ${step} 已跳过`, 'success', 2200);
 }
 
 // ============================================================
@@ -839,7 +818,7 @@ chrome.runtime.onMessage.addListener((message) => {
         syncAutoRunState(state);
         updateStatusDisplay(latestState);
         updateButtonStates();
-        if (status === 'completed' || status === 'manual_completed') {
+        if (status === 'completed' || status === 'manual_completed' || status === 'skipped') {
           syncPasswordField(state);
           if (state.oauthUrl) {
             displayOauthUrl.textContent = state.oauthUrl;