Quellcode durchsuchen

翻译界面和日志信息为中文,更新 OAuth 流程中的步骤描述,添加管理密钥输入框和相关功能,增强用户体验,优化 CSS 样式,修复部分逻辑错误,确保自动化流程的稳定性和可用性。

QLHazyCoder vor 4 Monaten
Ursprung
Commit
96867783dd
12 geänderte Dateien mit 1228 neuen und 439 gelöschten Zeilen
  1. 373 155
      background.js
  2. 8 8
      content/duck-mail.js
  3. 21 15
      content/inbucket-mail.js
  4. 30 27
      content/mail-163.js
  5. 18 13
      content/qq-mail.js
  6. 413 67
      content/signup-page.js
  7. 26 26
      content/utils.js
  8. 188 52
      content/vps-panel.js
  9. 2 2
      manifest.json
  10. 23 0
      sidepanel/sidepanel.css
  11. 50 39
      sidepanel/sidepanel.html
  12. 76 35
      sidepanel/sidepanel.js

+ 373 - 155
background.js

@@ -4,7 +4,7 @@ importScripts('data/names.js');
 
 const LOG_PREFIX = '[MultiPage:bg]';
 const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill';
-const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
+const STOP_ERROR_MESSAGE = '流程已被用户停止。';
 const HUMAN_STEP_DELAY_MIN = 700;
 const HUMAN_STEP_DELAY_MAX = 2200;
 
@@ -25,12 +25,16 @@ const DEFAULT_STATE = {
   password: null,
   accounts: [], // { email, password, createdAt }
   lastEmailTimestamp: null,
+  lastSignupCode: null,
+  lastLoginCode: null,
   localhostUrl: null,
   flowStartTime: null,
   tabRegistry: {},
   logs: [],
   vpsUrl: '',
+  vpsPassword: '',
   customPassword: '',
+  autoRunSkipFailures: false,
   mailProvider: '163', // 'qq' or '163'
   inbucketHost: '',
   inbucketMailbox: '',
@@ -85,7 +89,9 @@ async function resetState() {
     'accounts',
     'tabRegistry',
     'vpsUrl',
+    'vpsPassword',
     'customPassword',
+    'autoRunSkipFailures',
     'mailProvider',
     'inbucketHost',
     'inbucketMailbox',
@@ -98,7 +104,9 @@ async function resetState() {
     accounts: prev.accounts || [],
     tabRegistry: prev.tabRegistry || {},
     vpsUrl: prev.vpsUrl || '',
+    vpsPassword: prev.vpsPassword || '',
     customPassword: prev.customPassword || '',
+    autoRunSkipFailures: Boolean(prev.autoRunSkipFailures),
     mailProvider: prev.mailProvider || '163',
     inbucketHost: prev.inbucketHost || '',
     inbucketMailbox: prev.inbucketMailbox || '',
@@ -383,6 +391,19 @@ async function addLog(message, level = 'info') {
   chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {});
 }
 
+function getSourceLabel(source) {
+  const labels = {
+    'sidepanel': '侧边栏',
+    'signup-page': '认证页',
+    'vps-panel': 'VPS 面板',
+    'qq-mail': 'QQ 邮箱',
+    'mail-163': '163 邮箱',
+    'inbucket-mail': 'Inbucket 邮箱',
+    'duck-mail': 'Duck 邮箱',
+  };
+  return labels[source] || source || '未知来源';
+}
+
 // ============================================================
 // Step Status Management
 // ============================================================
@@ -429,10 +450,10 @@ async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY
 
 async function clickWithDebugger(tabId, rect) {
   if (!tabId) {
-    throw new Error('No auth tab found for debugger click.');
+    throw new Error('未找到用于调试点击的认证页面标签页。');
   }
   if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) {
-    throw new Error('Step 8 debugger fallback needs a valid button position.');
+    throw new Error('步骤 8 的调试器兜底点击需要有效的按钮坐标。');
   }
 
   const target = { tabId };
@@ -440,8 +461,8 @@ async function clickWithDebugger(tabId, rect) {
     await chrome.debugger.attach(target, '1.3');
   } catch (err) {
     throw new Error(
-      `Debugger attach failed during step 8 fallback: ${err.message}. ` +
-      'If DevTools is open on the auth tab, close it and retry.'
+      `步骤 8 的调试器兜底点击附加失败:${err.message}。` +
+      '如果认证页标签已打开 DevTools,请先关闭后重试。'
     );
   }
 
@@ -519,14 +540,14 @@ async function handleMessage(message, sender) {
       if (tabId && message.source) {
         await registerTab(message.source, tabId);
         flushCommand(message.source, tabId);
-        await addLog(`Content script ready: ${message.source} (tab ${tabId})`);
+        await addLog(`内容脚本已就绪:${getSourceLabel(message.source)}(标签页 ${tabId})`);
       }
       return { ok: true };
     }
 
     case 'LOG': {
       const { message: msg, level } = message.payload;
-      await addLog(`[${message.source}] ${msg}`, level);
+      await addLog(`[${getSourceLabel(message.source)}] ${msg}`, level);
       return { ok: true };
     }
 
@@ -537,7 +558,7 @@ async function handleMessage(message, sender) {
         return { ok: true };
       }
       await setStepStatus(message.step, 'completed');
-      await addLog(`Step ${message.step} completed`, 'ok');
+      await addLog(`步骤 ${message.step} 已完成`, 'ok');
       await handleStepData(message.step, message.payload);
       notifyStepComplete(message.step, message.payload);
       return { ok: true };
@@ -546,11 +567,11 @@ async function handleMessage(message, sender) {
     case 'STEP_ERROR': {
       if (isStopError(message.error)) {
         await setStepStatus(message.step, 'stopped');
-        await addLog(`Step ${message.step} stopped by user`, 'warn');
+        await addLog(`步骤 ${message.step} 已被用户停止`, 'warn');
         notifyStepError(message.step, message.error);
       } else {
         await setStepStatus(message.step, 'failed');
-        await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
+        await addLog(`步骤 ${message.step} 失败:${message.error}`, 'error');
         notifyStepError(message.step, message.error);
       }
       return { ok: true };
@@ -563,7 +584,7 @@ async function handleMessage(message, sender) {
     case 'RESET': {
       clearStopRequest();
       await resetState();
-      await addLog('Flow reset', 'info');
+      await addLog('流程已重置', 'info');
       return { ok: true };
     }
 
@@ -581,7 +602,9 @@ async function handleMessage(message, sender) {
     case 'AUTO_RUN': {
       clearStopRequest();
       const totalRuns = message.payload?.totalRuns || 1;
-      autoRunLoop(totalRuns);  // fire-and-forget
+      const autoRunSkipFailures = Boolean(message.payload?.autoRunSkipFailures);
+      await setState({ autoRunSkipFailures });
+      autoRunLoop(totalRuns, { autoRunSkipFailures });  // fire-and-forget
       return { ok: true };
     }
 
@@ -597,7 +620,9 @@ async function handleMessage(message, sender) {
     case 'SAVE_SETTING': {
       const updates = {};
       if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
+      if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword;
       if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
+      if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures);
       if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
       if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
       if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
@@ -688,6 +713,19 @@ function notifyStepError(step, error) {
   if (waiter) waiter.reject(new Error(error));
 }
 
+async function completeStepFromBackground(step, payload = {}) {
+  if (stopRequested) {
+    await setStepStatus(step, 'stopped');
+    notifyStepError(step, STOP_ERROR_MESSAGE);
+    return;
+  }
+
+  await setStepStatus(step, 'completed');
+  await addLog(`步骤 ${step} 已完成`, 'ok');
+  await handleStepData(step, payload);
+  notifyStepComplete(step, payload);
+}
+
 async function markRunningStepsStopped() {
   const state = await getState();
   const runningSteps = Object.entries(state.stepStatuses || {})
@@ -709,7 +747,7 @@ async function requestStop() {
     webNavListener = null;
   }
 
-  await addLog('Stop requested. Cancelling current operations...', 'warn');
+  await addLog('已收到停止请求,正在取消当前操作...', 'warn');
   await broadcastStopToContentScripts();
 
   for (const waiter of stepWaiters.values()) {
@@ -739,7 +777,7 @@ async function executeStep(step) {
   console.log(LOG_PREFIX, `Executing step ${step}`);
   throwIfStopped();
   await setStepStatus(step, 'running');
-  await addLog(`Step ${step} started`);
+  await addLog(`步骤 ${step} 开始执行`);
   await humanStepDelay();
 
   const state = await getState();
@@ -761,16 +799,16 @@ async function executeStep(step) {
       case 8: await executeStep8(state); break;
       case 9: await executeStep9(state); break;
       default:
-        throw new Error(`Unknown step: ${step}`);
+        throw new Error(`未知步骤:${step}`);
     }
   } catch (err) {
     if (isStopError(err)) {
       await setStepStatus(step, 'stopped');
-      await addLog(`Step ${step} stopped by user`, 'warn');
+      await addLog(`步骤 ${step} 已被用户停止`, 'warn');
       throw err;
     }
     await setStepStatus(step, 'failed');
-    await addLog(`Step ${step} failed: ${err.message}`, 'error');
+    await addLog(`步骤 ${step} 失败:${err.message}`, 'error');
     throw err;
   }
 }
@@ -795,7 +833,7 @@ async function fetchDuckEmail(options = {}) {
   throwIfStopped();
   const { generateNew = true } = options;
 
-  await addLog(`Duck Mail: Opening autofill settings (${generateNew ? 'generate new' : 'reuse current'})...`);
+  await addLog(`Duck 邮箱:正在打开自动填充设置(${generateNew ? '生成新地址' : '复用当前地址'})...`);
   await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL);
 
   const result = await sendToContentScript('duck-mail', {
@@ -808,11 +846,11 @@ async function fetchDuckEmail(options = {}) {
     throw new Error(result.error);
   }
   if (!result?.email) {
-    throw new Error('Duck email not returned.');
+    throw new Error('未返回 Duck 邮箱地址。');
   }
 
   await setEmailState(result.email);
-  await addLog(`Duck Mail: ${result.generated ? 'Generated' : 'Loaded'} ${result.email}`, 'ok');
+  await addLog(`Duck 邮箱:${result.generated ? '已生成' : '已读取'} ${result.email}`, 'ok');
   return result.email;
 }
 
@@ -824,38 +862,58 @@ let autoRunActive = false;
 let autoRunCurrentRun = 0;
 let autoRunTotalRuns = 1;
 
-// Outer loop: runs the full flow N times
-async function autoRunLoop(totalRuns) {
+// Outer loop: keep retrying until the target number of successful runs is reached.
+async function autoRunLoop(totalRuns, options = {}) {
   if (autoRunActive) {
-    await addLog('Auto run already in progress', 'warn');
+    await addLog('自动运行已在进行中', 'warn');
     return;
   }
 
   clearStopRequest();
   autoRunActive = true;
   autoRunTotalRuns = totalRuns;
-  await setState({ autoRunning: true });
+  const autoRunSkipFailures = Boolean(options.autoRunSkipFailures);
+  const maxAttempts = autoRunSkipFailures ? Math.max(totalRuns * 10, totalRuns + 20) : totalRuns;
+  let successfulRuns = 0;
+  let attemptRuns = 0;
+  let forceFreshTabsNextRun = false;
+
+  await setState({ autoRunning: true, autoRunSkipFailures });
 
-  for (let run = 1; run <= totalRuns; run++) {
-    autoRunCurrentRun = run;
+  while (successfulRuns < totalRuns && attemptRuns < maxAttempts) {
+    attemptRuns += 1;
+    const targetRun = successfulRuns + 1;
+    autoRunCurrentRun = targetRun;
 
-    // Reset everything at the start of each run (keep VPS/mail settings)
+    // Reset everything at the start of each attempt (keep user settings).
     const prevState = await getState();
     const keepSettings = {
       vpsUrl: prevState.vpsUrl,
+      vpsPassword: prevState.vpsPassword,
+      customPassword: prevState.customPassword,
+      autoRunSkipFailures: prevState.autoRunSkipFailures,
       mailProvider: prevState.mailProvider,
       inbucketHost: prevState.inbucketHost,
       inbucketMailbox: prevState.inbucketMailbox,
       autoRunning: true,
+      ...(forceFreshTabsNextRun ? { tabRegistry: {} } : {}),
     };
     await resetState();
     await setState(keepSettings);
-    // Tell side panel to reset all UI
     chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {});
     await sleepWithStop(500);
 
-    await addLog(`=== Auto Run ${run}/${totalRuns} — Phase 1: Get OAuth link & open signup ===`, 'info');
-    const status = (phase) => ({ type: 'AUTO_RUN_STATUS', payload: { phase, currentRun: run, totalRuns } });
+    if (forceFreshTabsNextRun) {
+      await addLog(`兜底模式:上一轮已放弃,当前开始第 ${attemptRuns} 次尝试,将使用新线程继续补足第 ${targetRun}/${totalRuns} 轮。`, 'warn');
+      forceFreshTabsNextRun = false;
+    }
+
+    const status = (phase) => ({
+      type: 'AUTO_RUN_STATUS',
+      payload: { phase, currentRun: targetRun, totalRuns, attemptRun: attemptRuns },
+    });
+
+    await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:第 ${attemptRuns} 次尝试,阶段 1,获取 OAuth 链接并打开注册页 ===`, 'info');
 
     try {
       throwIfStopped();
@@ -867,27 +925,25 @@ async function autoRunLoop(totalRuns) {
       let emailReady = false;
       try {
         const duckEmail = await fetchDuckEmail({ generateNew: true });
-        await addLog(`=== Run ${run}/${totalRuns} — Duck email ready: ${duckEmail} ===`, 'ok');
+        await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:Duck 邮箱已就绪:${duckEmail}(第 ${attemptRuns} 次尝试)===`, 'ok');
         emailReady = true;
       } catch (err) {
-        await addLog(`Duck Mail auto-fetch failed: ${err.message}`, 'warn');
+        await addLog(`Duck 邮箱自动获取失败:${err.message}`, 'warn');
       }
 
       if (!emailReady) {
-        await addLog(`=== Run ${run}/${totalRuns} PAUSED: Fetch Duck email or paste manually, then continue ===`, 'warn');
+        await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先获取 Duck 邮箱或手动粘贴邮箱,然后继续 ===`, 'warn');
         chrome.runtime.sendMessage(status('waiting_email')).catch(() => {});
 
-        // Wait for RESUME_AUTO_RUN — sets a promise that resumeAutoRun resolves
         await waitForResume();
 
         const resumedState = await getState();
         if (!resumedState.email) {
-          await addLog('Cannot resume: no email address.', 'error');
-          break;
+          throw new Error('无法继续:当前没有邮箱地址。');
         }
       }
 
-      await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info');
+      await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,注册、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info');
       chrome.runtime.sendMessage(status('running')).catch(() => {});
 
       const signupTabId = await getTabId('signup-page');
@@ -903,29 +959,56 @@ async function autoRunLoop(totalRuns) {
       await executeStepAndWait(8, 2000);
       await executeStepAndWait(9, 1000);
 
-      await addLog(`=== Run ${run}/${totalRuns} COMPLETE! ===`, 'ok');
-
+      successfulRuns += 1;
+      autoRunCurrentRun = successfulRuns;
+      await addLog(`=== 目标 ${successfulRuns}/${totalRuns} 轮已完成(第 ${attemptRuns} 次尝试成功)===`, 'ok');
+      continue;
     } catch (err) {
       if (isStopError(err)) {
-        await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn');
-      } else {
-        await addLog(`Run ${run}/${totalRuns} failed: ${err.message}`, 'error');
+        await addLog(`目标 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
+        chrome.runtime.sendMessage(status('stopped')).catch(() => {});
+        break;
+      }
+
+      if (!autoRunSkipFailures) {
+        await addLog(`目标 ${targetRun}/${totalRuns} 轮失败:${err.message}`, 'error');
+        chrome.runtime.sendMessage(status('stopped')).catch(() => {});
+        break;
       }
-      chrome.runtime.sendMessage(status('stopped')).catch(() => {});
-      break; // Stop on error
+
+      await addLog(`目标 ${targetRun}/${totalRuns} 轮的第 ${attemptRuns} 次尝试失败:${err.message}`, 'error');
+      await addLog('兜底开关已开启:将放弃当前线程,重新开一轮继续补足目标次数。', 'warn');
+      cancelPendingCommands('当前尝试已放弃。');
+      await broadcastStopToContentScripts();
+      chrome.runtime.sendMessage(status('retrying')).catch(() => {});
+      forceFreshTabsNextRun = true;
     }
   }
 
-  const completedRuns = autoRunCurrentRun;
-  if (stopRequested) {
-    await addLog(`=== Stopped after ${Math.max(0, completedRuns - 1)}/${autoRunTotalRuns} runs ===`, 'warn');
-    chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
-  } else if (completedRuns >= autoRunTotalRuns) {
-    await addLog(`=== All ${autoRunTotalRuns} runs completed successfully ===`, 'ok');
-    chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
+  if (!stopRequested && autoRunSkipFailures && successfulRuns < totalRuns && attemptRuns >= maxAttempts) {
+    await addLog(`已达到安全重试上限(${attemptRuns} 次尝试),当前仅完成 ${successfulRuns}/${totalRuns} 轮。`, 'error');
+    chrome.runtime.sendMessage({
+      type: 'AUTO_RUN_STATUS',
+      payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
+    }).catch(() => {});
+  } else if (stopRequested) {
+    await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
+    chrome.runtime.sendMessage({
+      type: 'AUTO_RUN_STATUS',
+      payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
+    }).catch(() => {});
+  } else if (successfulRuns >= autoRunTotalRuns) {
+    await addLog(`=== 全部 ${autoRunTotalRuns} 轮均已成功完成,共尝试 ${attemptRuns} 次 ===`, 'ok');
+    chrome.runtime.sendMessage({
+      type: 'AUTO_RUN_STATUS',
+      payload: { phase: 'complete', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
+    }).catch(() => {});
   } else {
-    await addLog(`=== Stopped after ${completedRuns}/${autoRunTotalRuns} runs ===`, 'warn');
-    chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
+    await addLog(`=== 已停止,完成 ${successfulRuns}/${autoRunTotalRuns} 轮,共尝试 ${attemptRuns} 次 ===`, 'warn');
+    chrome.runtime.sendMessage({
+      type: 'AUTO_RUN_STATUS',
+      payload: { phase: 'stopped', currentRun: successfulRuns, totalRuns: autoRunTotalRuns, attemptRun: attemptRuns },
+    }).catch(() => {});
   }
   autoRunActive = false;
   await setState({ autoRunning: false });
@@ -943,7 +1026,7 @@ async function resumeAutoRun() {
   throwIfStopped();
   const state = await getState();
   if (!state.email) {
-    await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error');
+    await addLog('无法继续:当前没有邮箱地址,请先在侧边栏填写邮箱。', 'error');
     return;
   }
   if (resumeWaiter) {
@@ -958,9 +1041,9 @@ async function resumeAutoRun() {
 
 async function executeStep1(state) {
   if (!state.vpsUrl) {
-    throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.');
+    throw new Error('尚未配置 VPS 地址,请先在侧边栏填写。');
   }
-  await addLog(`Step 1: Opening VPS panel...`);
+  await addLog('步骤 1:正在打开 VPS 面板...');
   await reuseOrCreateTab('vps-panel', state.vpsUrl, {
     inject: ['content/utils.js', 'content/vps-panel.js'],
     reloadIfSameUrl: true,
@@ -970,7 +1053,7 @@ async function executeStep1(state) {
     type: 'EXECUTE_STEP',
     step: 1,
     source: 'background',
-    payload: {},
+    payload: { vpsPassword: state.vpsPassword },
   });
 }
 
@@ -980,9 +1063,9 @@ async function executeStep1(state) {
 
 async function executeStep2(state) {
   if (!state.oauthUrl) {
-    throw new Error('No OAuth URL. Complete step 1 first.');
+    throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
   }
-  await addLog(`Step 2: Opening auth URL...`);
+  await addLog('步骤 2:正在打开认证链接...');
   await reuseOrCreateTab('signup-page', state.oauthUrl);
 
   await sendToContentScript('signup-page', {
@@ -999,7 +1082,7 @@ async function executeStep2(state) {
 
 async function executeStep3(state) {
   if (!state.email) {
-    throw new Error('No email address. Paste email in Side Panel first.');
+    throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。');
   }
 
   const password = state.customPassword || generatePassword();
@@ -1011,7 +1094,7 @@ async function executeStep3(state) {
   await setState({ accounts });
 
   await addLog(
-    `Step 3: Filling email ${state.email}, password ${state.customPassword ? 'customized' : 'generated'} (${password.length} chars)`
+    `步骤 3:正在填写邮箱 ${state.email},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
   );
   await sendToContentScript('signup-page', {
     type: 'EXECUTE_STEP',
@@ -1028,27 +1111,27 @@ async function executeStep3(state) {
 function getMailConfig(state) {
   const provider = state.mailProvider || 'qq';
   if (provider === '163') {
-    return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 Mail' };
+    return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' };
   }
   if (provider === 'inbucket') {
     const host = normalizeInbucketOrigin(state.inbucketHost);
     const mailbox = (state.inbucketMailbox || '').trim();
     if (!host) {
-      return { error: 'Inbucket host is empty or invalid.' };
+      return { error: 'Inbucket 主机地址为空或无效。' };
     }
     if (!mailbox) {
-      return { error: 'Inbucket mailbox name is empty.' };
+      return { error: 'Inbucket 邮箱名称为空。' };
     }
     return {
       source: 'inbucket-mail',
       url: `${host}/m/${encodeURIComponent(mailbox)}/`,
-      label: `Inbucket Mailbox (${mailbox})`,
+      label: `Inbucket 邮箱(${mailbox})`,
       navigateOnReuse: true,
       inject: ['content/utils.js', 'content/inbucket-mail.js'],
       injectSource: 'inbucket-mail',
     };
   }
-  return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
+  return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ 邮箱' };
 }
 
 function normalizeInbucketOrigin(rawValue) {
@@ -1065,10 +1148,189 @@ function normalizeInbucketOrigin(rawValue) {
   }
 }
 
+function getVerificationCodeStateKey(step) {
+  return step === 4 ? 'lastSignupCode' : 'lastLoginCode';
+}
+
+function getVerificationCodeLabel(step) {
+  return step === 4 ? '注册' : '登录';
+}
+
+function getVerificationPollPayload(step, state, overrides = {}) {
+  if (step === 4) {
+    return {
+      filterAfterTimestamp: state.flowStartTime || 0,
+      senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
+      subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm'],
+      targetEmail: state.email,
+      maxAttempts: 5,
+      intervalMs: 3000,
+      ...overrides,
+    };
+  }
+
+  return {
+    filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
+    senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
+    subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm', 'login'],
+    targetEmail: state.email,
+    maxAttempts: 5,
+    intervalMs: 3000,
+    ...overrides,
+  };
+}
+
+async function requestVerificationCodeResend(step) {
+  const signupTabId = await getTabId('signup-page');
+  if (!signupTabId) {
+    throw new Error('认证页面标签页已关闭,无法重新请求验证码。');
+  }
+
+  await chrome.tabs.update(signupTabId, { active: true });
+  await addLog(`步骤 ${step}:正在请求新的${getVerificationCodeLabel(step)}验证码...`, 'warn');
+
+  const result = await sendToContentScript('signup-page', {
+    type: 'RESEND_VERIFICATION_CODE',
+    step,
+    source: 'background',
+    payload: {},
+  });
+
+  if (result && result.error) {
+    throw new Error(result.error);
+  }
+
+  return Date.now();
+}
+
+async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
+  const stateKey = getVerificationCodeStateKey(step);
+  const rejectedCodes = new Set();
+  if (state[stateKey]) {
+    rejectedCodes.add(state[stateKey]);
+  }
+  for (const code of (pollOverrides.excludeCodes || [])) {
+    if (code) rejectedCodes.add(code);
+  }
+
+  let lastError = null;
+  let filterAfterTimestamp = pollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
+  const maxRounds = pollOverrides.maxRounds || 3;
+
+  for (let round = 1; round <= maxRounds; round++) {
+    if (round > 1) {
+      filterAfterTimestamp = await requestVerificationCodeResend(step);
+    }
+
+    const payload = getVerificationPollPayload(step, state, {
+      ...pollOverrides,
+      filterAfterTimestamp,
+      excludeCodes: [...rejectedCodes],
+    });
+
+    try {
+      const result = await sendToContentScript(mail.source, {
+        type: 'POLL_EMAIL',
+        step,
+        source: 'background',
+        payload,
+      });
+
+      if (result && result.error) {
+        throw new Error(result.error);
+      }
+
+      if (!result || !result.code) {
+        throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
+      }
+
+      if (rejectedCodes.has(result.code)) {
+        throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`);
+      }
+
+      return result;
+    } catch (err) {
+      lastError = err;
+      await addLog(`步骤 ${step}:${err.message}`, 'warn');
+      if (round < maxRounds) {
+        await addLog(`步骤 ${step}:将重新发送验证码后重试(${round + 1}/${maxRounds})...`, 'warn');
+      }
+    }
+  }
+
+  throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
+}
+
+async function submitVerificationCode(step, code) {
+  const signupTabId = await getTabId('signup-page');
+  if (!signupTabId) {
+    throw new Error('认证页面标签页已关闭,无法填写验证码。');
+  }
+
+  await chrome.tabs.update(signupTabId, { active: true });
+  const result = await sendToContentScript('signup-page', {
+    type: 'FILL_CODE',
+    step,
+    source: 'background',
+    payload: { code },
+  });
+
+  if (result && result.error) {
+    throw new Error(result.error);
+  }
+
+  return result || {};
+}
+
+async function resolveVerificationStep(step, state, mail) {
+  const stateKey = getVerificationCodeStateKey(step);
+  const rejectedCodes = new Set();
+  if (state[stateKey]) {
+    rejectedCodes.add(state[stateKey]);
+  }
+
+  let nextFilterAfterTimestamp = null;
+  const maxSubmitAttempts = 3;
+
+  for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
+    const result = await pollFreshVerificationCode(step, state, mail, {
+      excludeCodes: [...rejectedCodes],
+      filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined,
+    });
+
+    await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`);
+    const submitResult = await submitVerificationCode(step, result.code);
+
+    if (submitResult.invalidCode) {
+      rejectedCodes.add(result.code);
+      await addLog(`步骤 ${step}:验证码被页面拒绝:${submitResult.errorText || result.code}`, 'warn');
+
+      if (attempt >= maxSubmitAttempts) {
+        throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`);
+      }
+
+      nextFilterAfterTimestamp = await requestVerificationCodeResend(step);
+      await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn');
+      continue;
+    }
+
+    await setState({
+      lastEmailTimestamp: result.emailTimestamp,
+      [stateKey]: result.code,
+    });
+
+    await completeStepFromBackground(step, {
+      emailTimestamp: result.emailTimestamp,
+      code: result.code,
+    });
+    return;
+  }
+}
+
 async function executeStep4(state) {
   const mail = getMailConfig(state);
   if (mail.error) throw new Error(mail.error);
-  await addLog(`Step 4: Opening ${mail.label}...`);
+  await addLog(`步骤 4:正在打开${mail.label}...`);
 
   // For mail tabs, only create if not alive — don't navigate (preserves login session)
   const alive = await isTabAlive(mail.source);
@@ -1089,42 +1351,8 @@ async function executeStep4(state) {
     });
   }
 
-  const result = await sendToContentScript(mail.source, {
-    type: 'POLL_EMAIL',
-    step: 4,
-    source: 'background',
-    payload: {
-      filterAfterTimestamp: state.flowStartTime || 0,
-      senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
-      subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
-      targetEmail: state.email,
-      maxAttempts: 20,
-      intervalMs: 3000,
-    },
-  });
-
-  if (result && result.error) {
-    throw new Error(result.error);
-  }
-
-  if (result && result.code) {
-    await setState({ lastEmailTimestamp: result.emailTimestamp });
-    await addLog(`Step 4: Got verification code: ${result.code}`);
-
-    // Switch to signup tab and fill code
-    const signupTabId = await getTabId('signup-page');
-    if (signupTabId) {
-      await chrome.tabs.update(signupTabId, { active: true });
-      await sendToContentScript('signup-page', {
-        type: 'FILL_CODE',
-        step: 4,
-        source: 'background',
-        payload: { code: result.code },
-      });
-    } else {
-      throw new Error('Signup page tab was closed. Cannot fill verification code.');
-    }
-  }
+  await resolveVerificationStep(4, state, mail);
+  return;
 }
 
 // ============================================================
@@ -1135,7 +1363,7 @@ async function executeStep5(state) {
   const { firstName, lastName } = generateRandomName();
   const { year, month, day } = generateRandomBirthday();
 
-  await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
+  await addLog(`步骤 5:已生成姓名 ${firstName} ${lastName},生日 ${year}-${month}-${day}`);
 
   await sendToContentScript('signup-page', {
     type: 'EXECUTE_STEP',
@@ -1151,13 +1379,13 @@ async function executeStep5(state) {
 
 async function executeStep6(state) {
   if (!state.oauthUrl) {
-    throw new Error('No OAuth URL. Complete step 1 first.');
+    throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
   }
   if (!state.email) {
-    throw new Error('No email. Complete step 3 first.');
+    throw new Error('缺少邮箱地址,请先完成步骤 3。');
   }
 
-  await addLog(`Step 6: Opening OAuth URL for login...`);
+  await addLog('步骤 6:正在打开用于登录的 OAuth 链接...');
   // Reuse the signup-page tab — navigate it to the OAuth URL
   await reuseOrCreateTab('signup-page', state.oauthUrl);
 
@@ -1177,7 +1405,30 @@ async function executeStep6(state) {
 async function executeStep7(state) {
   const mail = getMailConfig(state);
   if (mail.error) throw new Error(mail.error);
-  await addLog(`Step 7: Opening ${mail.label}...`);
+  const authTabId = await getTabId('signup-page');
+
+  if (authTabId) {
+    await chrome.tabs.update(authTabId, { active: true });
+  } else {
+    if (!state.oauthUrl) {
+      throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
+    }
+    await reuseOrCreateTab('signup-page', state.oauthUrl);
+  }
+
+  await addLog('步骤 7:正在准备认证页,必要时切换到一次性验证码登录...');
+  const prepareResult = await sendToContentScript('signup-page', {
+    type: 'PREPARE_LOGIN_CODE',
+    step: 7,
+    source: 'background',
+    payload: {},
+  });
+
+  if (prepareResult && prepareResult.error) {
+    throw new Error(prepareResult.error);
+  }
+
+  await addLog(`步骤 7:正在打开${mail.label}...`);
 
   const alive = await isTabAlive(mail.source);
   if (alive) {
@@ -1197,41 +1448,8 @@ async function executeStep7(state) {
     });
   }
 
-  const result = await sendToContentScript(mail.source, {
-    type: 'POLL_EMAIL',
-    step: 7,
-    source: 'background',
-    payload: {
-      filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
-      senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
-      subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
-      targetEmail: state.email,
-      maxAttempts: 20,
-      intervalMs: 3000,
-    },
-  });
-
-  if (result && result.error) {
-    throw new Error(result.error);
-  }
-
-  if (result && result.code) {
-    await addLog(`Step 7: Got login verification code: ${result.code}`);
-
-    // Switch to signup/auth tab and fill code
-    const signupTabId = await getTabId('signup-page');
-    if (signupTabId) {
-      await chrome.tabs.update(signupTabId, { active: true });
-      await sendToContentScript('signup-page', {
-        type: 'FILL_CODE',
-        step: 7,
-        source: 'background',
-        payload: { code: result.code },
-      });
-    } else {
-      throw new Error('Auth page tab was closed. Cannot fill verification code.');
-    }
-  }
+  await resolveVerificationStep(7, state, mail);
+  return;
 }
 
 // ============================================================
@@ -1242,10 +1460,10 @@ let webNavListener = null;
 
 async function executeStep8(state) {
   if (!state.oauthUrl) {
-    throw new Error('No OAuth URL. Complete step 1 first.');
+    throw new Error('缺少 OAuth 链接,请先完成步骤 1。');
   }
 
-  await addLog('Step 8: Setting up localhost redirect listener...');
+  await addLog('步骤 8:正在监听 localhost 回调地址...');
 
   // Register webNavigation listener (scoped to this step)
   return new Promise((resolve, reject) => {
@@ -1264,7 +1482,7 @@ async function executeStep8(state) {
 
     const timeout = setTimeout(() => {
       cleanupListener();
-      reject(new Error('Localhost redirect not captured after 120s. Step 8 click may have been blocked.'));
+      reject(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 的点击可能被拦截了。'));
     }, 120000);
 
     webNavListener = (details) => {
@@ -1276,7 +1494,7 @@ async function executeStep8(state) {
         if (resolveCaptureWait) resolveCaptureWait(details.url);
 
         setState({ localhostUrl: details.url }).then(() => {
-          addLog(`Step 8: Captured localhost URL: ${details.url}`, 'ok');
+          addLog(`步骤 8:已捕获 localhost 地址:${details.url}`, 'ok');
           setStepStatus(8, 'completed');
           notifyStepComplete(8, { localhostUrl: details.url });
           broadcastDataUpdate({ localhostUrl: details.url });
@@ -1295,10 +1513,10 @@ async function executeStep8(state) {
         let signupTabId = await getTabId('signup-page');
         if (signupTabId) {
           await chrome.tabs.update(signupTabId, { active: true });
-          await addLog('Step 8: Switched to auth page. Preparing debugger click...');
+          await addLog('步骤 8:已切回认证页,正在准备调试器点击...');
         } else {
           signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
-          await addLog('Step 8: Auth tab reopened. Preparing debugger click...');
+          await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...');
         }
 
         const clickResult = await sendToContentScript('signup-page', {
@@ -1313,7 +1531,7 @@ async function executeStep8(state) {
 
         if (!resolved) {
           await clickWithDebugger(signupTabId, clickResult?.rect);
-          await addLog('Step 8: Debugger click dispatched, waiting for redirect...');
+          await addLog('步骤 8:已发送调试器点击,正在等待跳转...');
         }
       } catch (err) {
         clearTimeout(timeout);
@@ -1330,13 +1548,13 @@ async function executeStep8(state) {
 
 async function executeStep9(state) {
   if (!state.localhostUrl) {
-    throw new Error('No localhost URL. Complete step 8 first.');
+    throw new Error('缺少 localhost 回调地址,请先完成步骤 8。');
   }
   if (!state.vpsUrl) {
-    throw new Error('VPS URL not set. Please enter VPS URL in the side panel.');
+    throw new Error('尚未填写 VPS 地址,请先在侧边栏输入。');
   }
 
-  await addLog('Step 9: Opening VPS panel...');
+  await addLog('步骤 9:正在打开 VPS 面板...');
 
   let tabId = await getTabId('vps-panel');
   const alive = tabId && await isTabAlive('vps-panel');
@@ -1366,12 +1584,12 @@ async function executeStep9(state) {
   await new Promise(r => setTimeout(r, 1000));
 
   // Send command directly — bypass queue/ready mechanism
-  await addLog(`Step 9: Filling callback URL...`);
+  await addLog('步骤 9:正在填写回调地址...');
   await chrome.tabs.sendMessage(tabId, {
     type: 'EXECUTE_STEP',
     step: 9,
     source: 'background',
-    payload: { localhostUrl: state.localhostUrl },
+    payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword },
   });
 }
 

+ 8 - 8
content/duck-mail.js

@@ -10,7 +10,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
     sendResponse(result);
   }).catch(err => {
     if (isStopError(err)) {
-      log('Duck Mail: Stopped by user.', 'warn');
+      log('Duck 邮箱:已被用户停止。', 'warn');
       sendResponse({ stopped: true, error: err.message });
       return;
     }
@@ -23,7 +23,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
 async function fetchDuckEmail(payload = {}) {
   const { generateNew = true } = payload;
 
-  log(`Duck Mail: ${generateNew ? 'Generating' : 'Reading'} private address...`);
+  log(`Duck 邮箱:正在${generateNew ? '生成' : '读取'}私有地址...`);
 
   await waitForElement(
     'input.AutofillSettingsPanel__PrivateDuckAddressValue, button.AutofillSettingsPanel__GeneratorButton',
@@ -46,12 +46,12 @@ async function fetchDuckEmail(payload = {}) {
       }
       await sleep(150);
     }
-    throw new Error('Timed out waiting for Duck address to appear.');
+    throw new Error('等待 Duck 地址出现超时。');
   };
 
   const currentEmail = readEmail();
   if (currentEmail && !generateNew) {
-    log(`Duck Mail: Found existing address ${currentEmail}`);
+    log(`Duck 邮箱:已发现现有地址 ${currentEmail}`);
     return { email: currentEmail, generated: false };
   }
 
@@ -59,16 +59,16 @@ async function fetchDuckEmail(payload = {}) {
   const generatorButton = getGeneratorButton();
   if (!generatorButton) {
     if (currentEmail) {
-      log(`Duck Mail: Reusing existing address ${currentEmail}`, 'warn');
+      log(`Duck 邮箱:正在复用现有地址 ${currentEmail}`, 'warn');
       return { email: currentEmail, generated: false };
     }
-    throw new Error('Could not find "Generate Private Duck Address" button.');
+    throw new Error('未找到“生成 Duck 私有地址”按钮。');
   }
 
   generatorButton.click();
-  log('Duck Mail: Clicked "Generate Private Duck Address"');
+  log('Duck 邮箱:已点击“生成 Duck 私有地址”按钮');
 
   const nextEmail = await waitForEmailValue(currentEmail);
-  log(`Duck Mail: Ready address ${nextEmail}`, 'ok');
+  log(`Duck 邮箱:地址已就绪 ${nextEmail}`, 'ok');
   return { email: nextEmail, generated: true };
 }

+ 21 - 15
content/inbucket-mail.js

@@ -45,11 +45,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
       sendResponse(result);
     }).catch(err => {
       if (isStopError(err)) {
-        log(`Step ${message.step}: Stopped by user.`, 'warn');
+        log(`步骤 ${message.step}:已被用户停止。`, 'warn');
         sendResponse({ stopped: true, error: err.message });
         return;
       }
-      reportError(message.step, err.message);
+      log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
       sendResponse({ error: err.message });
     });
     return true;
@@ -158,10 +158,10 @@ async function deleteCurrentMailboxMessage(step) {
   try {
     const deleteButton = await waitForElement('.button-bar button.danger', 5000);
     simulateClick(deleteButton);
-    log(`Step ${step}: Deleted mailbox message`, 'ok');
+    log(`步骤 ${step}:已删除邮箱消息`, 'ok');
     await sleep(1200);
   } catch (err) {
-    log(`Step ${step}: Failed to delete mailbox message: ${err.message}`, 'warn');
+    log(`步骤 ${step}:删除邮箱消息失败:${err.message}`, 'warn');
   }
 }
 
@@ -171,24 +171,26 @@ async function handleMailboxPollEmail(step, payload) {
     subjectFilters = [],
     maxAttempts = 20,
     intervalMs = 3000,
+    excludeCodes = [],
   } = payload || {};
+  const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
 
-  log(`Step ${step}: Starting email poll on Inbucket mailbox page (max ${maxAttempts} attempts)`);
+  log(`步骤 ${step}:开始轮询 Inbucket 邮箱页面(最多 ${maxAttempts} 次)`);
 
   try {
     await waitForElement('.message-list, .message-list-entry', 15000);
-    log(`Step ${step}: Mailbox page loaded`);
+    log(`步骤 ${step}:邮箱页面已加载`);
   } catch {
-    throw new Error('Inbucket mailbox page did not load. Make sure /m/<mailbox>/ is open.');
+    throw new Error('Inbucket 邮箱页面未加载完成,请确认已打开 /m/<mailbox>/ 页面。');
   }
 
   const existingMailIds = getCurrentMailboxIds();
-  log(`Step ${step}: Snapshotted ${existingMailIds.size} existing mailbox messages`);
+  log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧消息快照`);
 
   const FALLBACK_AFTER = 3;
 
   for (let attempt = 1; attempt <= maxAttempts; attempt++) {
-    log(`Polling Inbucket mailbox... attempt ${attempt}/${maxAttempts}`);
+    log(`步骤 ${step}:正在轮询 Inbucket 邮箱,第 ${attempt}/${maxAttempts} 次`);
 
     if (attempt > 1) {
       await refreshMailbox();
@@ -212,6 +214,10 @@ async function handleMailboxPollEmail(step, payload) {
     for (const mail of candidates) {
       const code = mail.code || extractVerificationCode(mail.combinedText);
       if (!code) continue;
+      if (excludedCodeSet.has(code)) {
+        log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
+        continue;
+      }
 
       await openMailboxEntry(mail.entry);
       await deleteCurrentMailboxMessage(step);
@@ -219,9 +225,9 @@ async function handleMailboxPollEmail(step, payload) {
       seenMailIds.add(mail.mailId);
       await persistSeenMailIds();
 
-      const source = existingMailIds.has(mail.mailId) ? 'fallback' : 'new';
+      const source = existingMailIds.has(mail.mailId) ? '回退匹配邮件' : '新邮件';
       log(
-        `Step ${step}: Code found: ${code} (${source}, sender: ${mail.sender || 'unknown'}, subject: ${(mail.subject || '').slice(0, 60)})`,
+        `步骤 ${step}:已找到验证码:${code}(来源:${source},发件人:${mail.sender || '未知'},主题:${(mail.subject || '').slice(0, 60)})`,
         'ok'
       );
 
@@ -234,7 +240,7 @@ async function handleMailboxPollEmail(step, payload) {
     }
 
     if (attempt === FALLBACK_AFTER + 1) {
-      log(`Step ${step}: No new mailbox messages yet, falling back to older matching messages`, 'warn');
+      log(`步骤 ${step}:暂未发现新消息,开始回退到较早的匹配邮件`, 'warn');
     }
 
     if (attempt < maxAttempts) {
@@ -243,14 +249,14 @@ async function handleMailboxPollEmail(step, payload) {
   }
 
   throw new Error(
-    `No matching verification email found in Inbucket mailbox after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
-    'Check the mailbox page manually.'
+    `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Inbucket 邮箱中找到匹配的验证码邮件。` +
+    '请手动检查邮箱页面。'
   );
 }
 
 async function handlePollEmail(step, payload) {
   if (!location.pathname.startsWith('/m/')) {
-    throw new Error('Inbucket now only supports mailbox pages like /m/<mailbox>/.');
+    throw new Error('当前 Inbucket 仅支持 /m/<mailbox>/ 这种邮箱页面。');
   }
   return handleMailboxPollEmail(step, payload);
 }

+ 30 - 27
content/mail-163.js

@@ -54,11 +54,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
       sendResponse(result);
     }).catch(err => {
       if (isStopError(err)) {
-        log(`Step ${message.step}: Stopped by user.`, 'warn');
+        log(`步骤 ${message.step}:已被用户停止。`, 'warn');
         sendResponse({ stopped: true, error: err.message });
         return;
       }
-      reportError(message.step, err.message);
+      log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
       sendResponse({ error: err.message });
     });
     return true;
@@ -87,22 +87,23 @@ function getCurrentMailIds() {
 // ============================================================
 
 async function handlePollEmail(step, payload) {
-  const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
+  const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload;
+  const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
 
-  log(`Step ${step}: Starting email poll on 163 Mail (max ${maxAttempts} attempts)`);
+  log(`步骤 ${step}:开始轮询 163 邮箱(最多 ${maxAttempts} 次)`);
 
   // Click inbox in sidebar to ensure we're in inbox view
-  log(`Step ${step}: Waiting for sidebar...`);
+  log(`步骤 ${step}:正在等待侧边栏加载...`);
   try {
     const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000);
     inboxLink.click();
-    log(`Step ${step}: Clicked inbox`);
+    log(`步骤 ${step}:已点击收件箱`);
   } catch {
-    log(`Step ${step}: Inbox link not found, proceeding...`, 'warn');
+    log(`步骤 ${step}:未找到收件箱入口,继续尝试后续流程...`, 'warn');
   }
 
   // Wait for mail list to appear
-  log(`Step ${step}: Waiting for mail list...`);
+  log(`步骤 ${step}:正在等待邮件列表加载...`);
   let items = [];
   for (let i = 0; i < 20; i++) {
     items = findMailItems();
@@ -117,19 +118,19 @@ async function handlePollEmail(step, payload) {
   }
 
   if (items.length === 0) {
-    throw new Error('163 Mail list did not load. Make sure inbox is open.');
+    throw new Error('163 邮箱列表未加载完成,请确认当前已打开收件箱。');
   }
 
-  log(`Step ${step}: Mail list loaded, ${items.length} items`);
+  log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
 
   // Snapshot existing mail IDs
   const existingMailIds = getCurrentMailIds();
-  log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails`);
+  log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
 
   const FALLBACK_AFTER = 3;
 
   for (let attempt = 1; attempt <= maxAttempts; attempt++) {
-    log(`Polling 163 Mail... attempt ${attempt}/${maxAttempts}`);
+    log(`步骤 ${step}:正在轮询 163 邮箱,第 ${attempt}/${maxAttempts} 次`);
 
     if (attempt > 1) {
       await refreshInbox();
@@ -157,11 +158,13 @@ async function handlePollEmail(step, payload) {
 
       if (senderMatch || subjectMatch) {
         const code = extractVerificationCode(subject + ' ' + ariaLabel);
-        if (code && !seenCodes.has(code)) {
+        if (code && excludedCodeSet.has(code)) {
+          log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
+        } else if (code && !seenCodes.has(code)) {
           seenCodes.add(code);
           persistSeenCodes();
-          const source = useFallback && existingMailIds.has(id) ? 'fallback' : 'new';
-          log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok');
+          const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件' : '新邮件';
+          log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)})`, 'ok');
 
           // Delete this email via right-click menu, WAIT for it to finish before returning
           await deleteEmail(item, step);
@@ -170,13 +173,13 @@ async function handlePollEmail(step, payload) {
 
           return { ok: true, code, emailTimestamp: Date.now(), mailId: id };
         } else if (code && seenCodes.has(code)) {
-          log(`Step ${step}: Skipping already-seen code: ${code}`, 'info');
+          log(`步骤 ${step}:跳过已处理过的验证码:${code}`, 'info');
         }
       }
     }
 
     if (attempt === FALLBACK_AFTER + 1) {
-      log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first match`, 'warn');
+      log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
     }
 
     if (attempt < maxAttempts) {
@@ -185,8 +188,8 @@ async function handlePollEmail(step, payload) {
   }
 
   throw new Error(
-    `No new matching email found on 163 Mail after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
-    'Check inbox manually.'
+    `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 163 邮箱中找到新的匹配邮件。` +
+    '请手动检查收件箱。'
   );
 }
 
@@ -196,7 +199,7 @@ async function handlePollEmail(step, payload) {
 
 async function deleteEmail(item, step) {
   try {
-    log(`Step ${step}: Deleting email...`);
+    log(`步骤 ${step}:正在删除邮件...`);
 
     // Strategy 1: Click the trash icon inside the mail item
     // Each mail item has: <b class="nui-ico nui-ico-delete" title="删除邮件" sign="trash">
@@ -208,21 +211,21 @@ async function deleteEmail(item, step) {
     const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]');
     if (trashIcon) {
       trashIcon.click();
-      log(`Step ${step}: Clicked trash icon`, 'ok');
+      log(`步骤 ${step}:已点击删除图标`, 'ok');
       await sleep(1500);
 
       // Check if item disappeared (confirm deletion)
       const stillExists = document.getElementById(item.id);
       if (!stillExists || stillExists.style.display === 'none') {
-        log(`Step ${step}: Email deleted successfully`);
+        log(`步骤 ${step}:邮件已成功删除`);
       } else {
-        log(`Step ${step}: Email may not have been deleted, item still visible`, 'warn');
+        log(`步骤 ${step}:邮件可能尚未删除,列表中仍可见`, 'warn');
       }
       return;
     }
 
     // Strategy 2: Select checkbox then click toolbar delete button
-    log(`Step ${step}: Trash icon not found, trying checkbox + toolbar delete...`);
+    log(`步骤 ${step}:未找到删除图标,尝试使用复选框加工具栏删除...`);
     const checkbox = item.querySelector('[sign="checkbox"], .nui-chk');
     if (checkbox) {
       checkbox.click();
@@ -233,16 +236,16 @@ async function deleteEmail(item, step) {
       for (const btn of toolbarBtns) {
         if (btn.textContent.replace(/\s/g, '').includes('删除')) {
           btn.closest('.nui-btn').click();
-          log(`Step ${step}: Clicked toolbar delete`, 'ok');
+          log(`步骤 ${step}:已点击工具栏删除`, 'ok');
           await sleep(1500);
           return;
         }
       }
     }
 
-    log(`Step ${step}: Could not delete email (no delete button found)`, 'warn');
+    log(`步骤 ${step}:无法删除邮件(未找到删除按钮)`, 'warn');
   } catch (err) {
-    log(`Step ${step}: Failed to delete email: ${err.message}`, 'warn');
+    log(`步骤 ${step}:删除邮件失败:${err.message}`, 'warn');
   }
 }
 

+ 18 - 13
content/qq-mail.js

@@ -27,11 +27,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
       sendResponse(result);
     }).catch(err => {
       if (isStopError(err)) {
-        log(`Step ${message.step}: Stopped by user.`, 'warn');
+        log(`步骤 ${message.step}:已被用户停止。`, 'warn');
         sendResponse({ stopped: true, error: err.message });
         return;
       }
-      reportError(message.step, err.message);
+      log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
       sendResponse({ error: err.message });
     });
     return true; // async response
@@ -55,28 +55,29 @@ function getCurrentMailIds() {
 // ============================================================
 
 async function handlePollEmail(step, payload) {
-  const { senderFilters, subjectFilters, maxAttempts, intervalMs } = payload;
+  const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload;
+  const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
 
-  log(`Step ${step}: Starting email poll (max ${maxAttempts} attempts, every ${intervalMs / 1000}s)`);
+  log(`步骤 ${step}:开始轮询邮箱(最多 ${maxAttempts} 次,每 ${intervalMs / 1000} 秒一次)`);
 
   // Wait for mail list to load
   try {
     await waitForElement('.mail-list-page-item', 10000);
-    log(`Step ${step}: Mail list loaded`);
+    log(`步骤 ${step}:邮件列表已加载`);
   } catch {
-    throw new Error('Mail list did not load. Make sure QQ Mail inbox is open.');
+    throw new Error('邮件列表未加载完成,请确认 QQ 邮箱已打开收件箱。');
   }
 
   // Step 1: Snapshot existing mail IDs BEFORE we start waiting for new email
   const existingMailIds = getCurrentMailIds();
-  log(`Step ${step}: Snapshotted ${existingMailIds.size} existing emails as "old"`);
+  log(`步骤 ${step}:已将当前 ${existingMailIds.size} 封邮件标记为旧邮件快照`);
 
   // Fallback after just 3 attempts (~10s). In practice, the email is usually
   // already in the list but has the same mailid (page was already open).
   const FALLBACK_AFTER = 3;
 
   for (let attempt = 1; attempt <= maxAttempts; attempt++) {
-    log(`Polling QQ Mail... attempt ${attempt}/${maxAttempts}`);
+    log(`步骤 ${step}:正在轮询 QQ 邮箱,第 ${attempt}/${maxAttempts} 次`);
 
     // Refresh inbox (skip on first attempt, list is fresh)
     if (attempt > 1) {
@@ -104,15 +105,19 @@ async function handlePollEmail(step, payload) {
       if (senderMatch || subjectMatch) {
         const code = extractVerificationCode(subject + ' ' + digest);
         if (code) {
-          const source = useFallback && existingMailIds.has(mailId) ? 'fallback-first-match' : 'new';
-          log(`Step ${step}: Code found: ${code} (${source}, subject: ${subject.slice(0, 40)})`, 'ok');
+          if (excludedCodeSet.has(code)) {
+            log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
+            continue;
+          }
+          const source = useFallback && existingMailIds.has(mailId) ? '回退首封匹配邮件' : '新邮件';
+          log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)})`, 'ok');
           return { ok: true, code, emailTimestamp: Date.now(), mailId };
         }
       }
     }
 
     if (attempt === FALLBACK_AFTER + 1) {
-      log(`Step ${step}: No new emails after ${FALLBACK_AFTER} attempts, falling back to first matching email`, 'warn');
+      log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
     }
 
     if (attempt < maxAttempts) {
@@ -121,8 +126,8 @@ async function handlePollEmail(step, payload) {
   }
 
   throw new Error(
-    `No new matching email found after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
-    'Check QQ Mail manually. Email may be delayed or in spam folder.'
+    `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未找到新的匹配邮件。` +
+    '请手动检查 QQ 邮箱,邮件可能延迟到达或进入垃圾箱。'
   );
 }
 

+ 413 - 67
content/signup-page.js

@@ -5,19 +5,25 @@ console.log('[MultiPage:signup-page] Content script loaded on', location.href);
 
 // Listen for commands from Background
 chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  if (message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE' || message.type === 'STEP8_FIND_AND_CLICK') {
+  if (
+    message.type === 'EXECUTE_STEP'
+    || message.type === 'FILL_CODE'
+    || message.type === 'STEP8_FIND_AND_CLICK'
+    || message.type === 'PREPARE_LOGIN_CODE'
+    || message.type === 'RESEND_VERIFICATION_CODE'
+  ) {
     resetStopState();
     handleCommand(message).then((result) => {
       sendResponse({ ok: true, ...(result || {}) });
     }).catch(err => {
       if (isStopError(err)) {
-        log(`Step ${message.step || 8}: Stopped by user.`, 'warn');
+        log(`步骤 ${message.step || 8}:已被用户停止。`, 'warn');
         sendResponse({ stopped: true, error: err.message });
         return;
       }
 
       if (message.type === 'STEP8_FIND_AND_CLICK') {
-        log(`Step 8: ${err.message}`, 'error');
+        log(`步骤 8:${err.message}`, 'error');
         sendResponse({ error: err.message });
         return;
       }
@@ -38,22 +44,212 @@ async function handleCommand(message) {
         case 5: return await step5_fillNameBirthday(message.payload);
         case 6: return await step6_login(message.payload);
         case 8: return await step8_findAndClick();
-        default: throw new Error(`signup-page.js does not handle step ${message.step}`);
+        default: throw new Error(`signup-page.js 不处理步骤 ${message.step}`);
       }
     case 'FILL_CODE':
       // Step 4 = signup code, Step 7 = login code (same handler)
       return await fillVerificationCode(message.step, message.payload);
+    case 'PREPARE_LOGIN_CODE':
+      return await prepareLoginCodeFlow();
+    case 'RESEND_VERIFICATION_CODE':
+      return await resendVerificationCode(message.step);
     case 'STEP8_FIND_AND_CLICK':
       return await step8_findAndClick();
   }
 }
 
+const VERIFICATION_CODE_INPUT_SELECTOR = [
+  'input[name="code"]',
+  'input[name="otp"]',
+  'input[autocomplete="one-time-code"]',
+  'input[type="text"][maxlength="6"]',
+  'input[type="tel"][maxlength="6"]',
+  'input[aria-label*="code" i]',
+  'input[placeholder*="code" i]',
+  'input[inputmode="numeric"]',
+].join(', ');
+
+const ONE_TIME_CODE_LOGIN_PATTERN = /使用一次性验证码登录|改用(?:一次性)?验证码(?:登录)?|使用验证码登录|一次性验证码|验证码登录|one[-\s]*time\s*(?:passcode|password|code)|use\s+(?:a\s+)?one[-\s]*time\s*(?:passcode|password|code)(?:\s+instead)?|use\s+(?:a\s+)?code(?:\s+instead)?|sign\s+in\s+with\s+(?:email|code)|email\s+(?:me\s+)?(?:a\s+)?code/i;
+
+const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i;
+
+function isVisibleElement(el) {
+  if (!el) return false;
+  const style = window.getComputedStyle(el);
+  const rect = el.getBoundingClientRect();
+  return style.display !== 'none'
+    && style.visibility !== 'hidden'
+    && rect.width > 0
+    && rect.height > 0;
+}
+
+function getVerificationCodeTarget() {
+  const codeInput = document.querySelector(VERIFICATION_CODE_INPUT_SELECTOR);
+  if (codeInput && isVisibleElement(codeInput)) {
+    return { type: 'single', element: codeInput };
+  }
+
+  const singleInputs = Array.from(document.querySelectorAll('input[maxlength="1"]'))
+    .filter(isVisibleElement);
+  if (singleInputs.length >= 6) {
+    return { type: 'split', elements: singleInputs };
+  }
+
+  return null;
+}
+
+function getActionText(el) {
+  return [
+    el?.textContent,
+    el?.value,
+    el?.getAttribute?.('aria-label'),
+    el?.getAttribute?.('title'),
+  ]
+    .filter(Boolean)
+    .join(' ')
+    .replace(/\s+/g, ' ')
+    .trim();
+}
+
+function isActionEnabled(el) {
+  return Boolean(el)
+    && !el.disabled
+    && el.getAttribute('aria-disabled') !== 'true';
+}
+
+function findOneTimeCodeLoginTrigger() {
+  const candidates = document.querySelectorAll(
+    'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
+  );
+
+  for (const el of candidates) {
+    if (!isVisibleElement(el)) continue;
+    if (el.disabled || el.getAttribute('aria-disabled') === 'true') continue;
+
+    const text = [
+      el.textContent,
+      el.value,
+      el.getAttribute('aria-label'),
+      el.getAttribute('title'),
+    ]
+      .filter(Boolean)
+      .join(' ')
+      .replace(/\s+/g, ' ')
+      .trim();
+
+    if (text && ONE_TIME_CODE_LOGIN_PATTERN.test(text)) {
+      return el;
+    }
+  }
+
+  return null;
+}
+
+function findResendVerificationCodeTrigger({ allowDisabled = false } = {}) {
+  const candidates = document.querySelectorAll(
+    'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]'
+  );
+
+  for (const el of candidates) {
+    if (!isVisibleElement(el)) continue;
+    if (!allowDisabled && !isActionEnabled(el)) continue;
+
+    const text = getActionText(el);
+    if (text && RESEND_VERIFICATION_CODE_PATTERN.test(text)) {
+      return el;
+    }
+  }
+
+  return null;
+}
+
+async function prepareLoginCodeFlow(timeout = 15000) {
+  const readyTarget = getVerificationCodeTarget();
+  if (readyTarget) {
+    log('步骤 7:验证码输入框已就绪。');
+    return { ready: true, mode: readyTarget.type };
+  }
+
+  const start = Date.now();
+  let switchClickCount = 0;
+  let lastSwitchAttemptAt = 0;
+  let loggedPasswordPage = false;
+
+  while (Date.now() - start < timeout) {
+    throwIfStopped();
+
+    const target = getVerificationCodeTarget();
+    if (target) {
+      log('步骤 7:验证码页面已就绪。');
+      return { ready: true, mode: target.type };
+    }
+
+    const passwordInput = document.querySelector('input[type="password"]');
+    const switchTrigger = findOneTimeCodeLoginTrigger();
+
+    if (switchTrigger && (switchClickCount === 0 || Date.now() - lastSwitchAttemptAt > 1500)) {
+      switchClickCount += 1;
+      lastSwitchAttemptAt = Date.now();
+      loggedPasswordPage = false;
+      log('步骤 7:检测到密码页,正在切换到一次性验证码登录...');
+      await humanPause(350, 900);
+      simulateClick(switchTrigger);
+      await sleep(1200);
+      continue;
+    }
+
+    if (passwordInput && !loggedPasswordPage) {
+      loggedPasswordPage = true;
+      log('步骤 7:正在等待密码页上的一次性验证码登录入口...');
+    }
+
+    await sleep(200);
+  }
+
+  throw new Error('无法切换到一次性验证码验证页面。URL: ' + location.href);
+}
+
+async function resendVerificationCode(step, timeout = 45000) {
+  if (step === 7) {
+    await prepareLoginCodeFlow();
+  }
+
+  const start = Date.now();
+  let action = null;
+  let loggedWaiting = false;
+
+  while (Date.now() - start < timeout) {
+    throwIfStopped();
+    action = findResendVerificationCodeTrigger({ allowDisabled: true });
+
+    if (action && isActionEnabled(action)) {
+      log(`步骤 ${step}:重新发送验证码按钮已可用。`);
+      await humanPause(350, 900);
+      simulateClick(action);
+      await sleep(1200);
+      return {
+        resent: true,
+        buttonText: getActionText(action),
+      };
+    }
+
+    if (action && !loggedWaiting) {
+      loggedWaiting = true;
+      log(`步骤 ${step}:正在等待重新发送验证码按钮变为可点击...`);
+    }
+
+    await sleep(250);
+  }
+
+  throw new Error('无法点击重新发送验证码按钮。URL: ' + location.href);
+}
+
 // ============================================================
 // Step 2: Click Register
 // ============================================================
 
 async function step2_clickRegister() {
-  log('Step 2: Looking for Register/Sign up button...');
+  log('步骤 2:正在查找注册按钮...');
 
   let registerBtn = null;
   try {
@@ -68,8 +264,8 @@ async function step2_clickRegister() {
       registerBtn = await waitForElement('a[href*="signup"], a[href*="register"]', 5000);
     } catch {
       throw new Error(
-        'Could not find Register/Sign up button. ' +
-        'Check auth page DOM in DevTools. URL: ' + location.href
+        '未找到注册按钮。' +
+        '请在 DevTools 中检查认证页面 DOM。URL: ' + location.href
       );
     }
   }
@@ -77,7 +273,7 @@ async function step2_clickRegister() {
   await humanPause(450, 1200);
   reportComplete(2);
   simulateClick(registerBtn);
-  log('Step 2: Clicked Register button');
+  log('步骤 2:已点击注册按钮');
 }
 
 // ============================================================
@@ -86,9 +282,9 @@ async function step2_clickRegister() {
 
 async function step3_fillEmailPassword(payload) {
   const { email } = payload;
-  if (!email) throw new Error('No email provided. Paste email in Side Panel first.');
+  if (!email) throw new Error('未提供邮箱地址,请先在侧边栏粘贴邮箱。');
 
-  log(`Step 3: Filling email: ${email}`);
+  log(`步骤 3:正在填写邮箱:${email}`);
 
   // Find email input
   let emailInput = null;
@@ -98,40 +294,40 @@ async function step3_fillEmailPassword(payload) {
       10000
     );
   } catch {
-    throw new Error('Could not find email input field on signup page. URL: ' + location.href);
+    throw new Error('在注册页未找到邮箱输入框。URL: ' + location.href);
   }
 
   await humanPause(500, 1400);
   fillInput(emailInput, email);
-  log('Step 3: Email filled');
+  log('步骤 3:邮箱已填写');
 
   // Check if password field is on the same page
   let passwordInput = document.querySelector('input[type="password"]');
 
   if (!passwordInput) {
     // Need to submit email first to get to password page
-    log('Step 3: No password field yet, submitting email first...');
+    log('步骤 3:暂未发现密码输入框,先提交邮箱...');
     const submitBtn = document.querySelector('button[type="submit"]')
       || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null);
 
     if (submitBtn) {
       await humanPause(400, 1100);
       simulateClick(submitBtn);
-      log('Step 3: Submitted email, waiting for password field...');
+      log('步骤 3:邮箱已提交,正在等待密码输入框...');
       await sleep(2000);
     }
 
     try {
       passwordInput = await waitForElement('input[type="password"]', 10000);
     } catch {
-      throw new Error('Could not find password input after submitting email. URL: ' + location.href);
+      throw new Error('提交邮箱后仍未找到密码输入框。URL: ' + location.href);
     }
   }
 
-  if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
+  if (!payload.password) throw new Error('未提供密码,步骤 3 需要可用密码。');
   await humanPause(600, 1500);
   fillInput(passwordInput, payload.password);
-  log('Step 3: Password filled');
+  log('步骤 3:密码已填写');
 
   // Report complete BEFORE submit, because submit causes page navigation
   // which kills the content script connection
@@ -145,7 +341,7 @@ async function step3_fillEmailPassword(payload) {
   if (submitBtn) {
     await humanPause(500, 1300);
     simulateClick(submitBtn);
-    log('Step 3: Form submitted');
+    log('步骤 3:表单已提交');
   }
 }
 
@@ -153,40 +349,177 @@ async function step3_fillEmailPassword(payload) {
 // Fill Verification Code (used by step 4 and step 7)
 // ============================================================
 
+const INVALID_VERIFICATION_CODE_PATTERN = /代码不正确|验证码不正确|验证码错误|code\s+(?:is\s+)?incorrect|invalid\s+code|incorrect\s+code|try\s+again/i;
+const VERIFICATION_PAGE_PATTERN = /检查您的收件箱|输入我们刚刚向|重新发送电子邮件|重新发送验证码|验证码|代码不正确|email\s+verification/i;
+const OAUTH_CONSENT_PAGE_PATTERN = /使用\s*ChatGPT\s*登录到\s*Codex|login\s+to\s+codex|log\s+in\s+to\s+codex|authorize|授权/i;
+const ADD_PHONE_PAGE_PATTERN = /add[\s-]*phone|添加手机号|手机号码|手机号|phone\s+number|telephone/i;
+
+function getVerificationErrorText() {
+  const messages = [];
+  const selectors = [
+    '.react-aria-FieldError',
+    '[slot="errorMessage"]',
+    '[id$="-error"]',
+    '[data-invalid="true"] + *',
+    '[aria-invalid="true"] + *',
+    '[class*="error"]',
+  ];
+
+  for (const selector of selectors) {
+    document.querySelectorAll(selector).forEach((el) => {
+      const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
+      if (text) {
+        messages.push(text);
+      }
+    });
+  }
+
+  const invalidInput = document.querySelector(`${VERIFICATION_CODE_INPUT_SELECTOR}[aria-invalid="true"], ${VERIFICATION_CODE_INPUT_SELECTOR}[data-invalid="true"]`);
+  if (invalidInput) {
+    const wrapper = invalidInput.closest('form, [data-rac], ._root_18qcl_51, div');
+    if (wrapper) {
+      const text = (wrapper.textContent || '').replace(/\s+/g, ' ').trim();
+      if (text) {
+        messages.push(text);
+      }
+    }
+  }
+
+  return messages.find((text) => INVALID_VERIFICATION_CODE_PATTERN.test(text)) || '';
+}
+
+function isStep5Ready() {
+  return Boolean(
+    document.querySelector('input[name="name"], input[autocomplete="name"], input[name="birthday"], input[name="age"], [role="spinbutton"][data-type="year"]')
+  );
+}
+
+function getPageTextSnapshot() {
+  return (document.body?.innerText || document.body?.textContent || '')
+    .replace(/\s+/g, ' ')
+    .trim();
+}
+
+function getPrimaryContinueButton() {
+  const continueBtn = document.querySelector(
+    'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107'
+  );
+  if (continueBtn && isVisibleElement(continueBtn)) {
+    return continueBtn;
+  }
+
+  const buttons = document.querySelectorAll('button, [role="button"]');
+  return Array.from(buttons).find((el) => isVisibleElement(el) && /继续|Continue/i.test(el.textContent || '')) || null;
+}
+
+function isVerificationPageStillVisible() {
+  if (getVerificationCodeTarget()) return true;
+  if (findResendVerificationCodeTrigger({ allowDisabled: true })) return true;
+  if (document.querySelector('form[action*="email-verification" i]')) return true;
+
+  return VERIFICATION_PAGE_PATTERN.test(getPageTextSnapshot());
+}
+
+function isAddPhonePageReady() {
+  const path = `${location.pathname || ''} ${location.href || ''}`;
+  if (/\/add-phone(?:[/?#]|$)/i.test(path)) return true;
+
+  const phoneInput = document.querySelector(
+    'input[type="tel"]:not([maxlength="6"]), input[name*="phone" i], input[id*="phone" i], input[autocomplete="tel"]'
+  );
+  if (phoneInput && isVisibleElement(phoneInput)) {
+    return true;
+  }
+
+  return ADD_PHONE_PAGE_PATTERN.test(getPageTextSnapshot());
+}
+
+function isStep8Ready() {
+  const continueBtn = getPrimaryContinueButton();
+  if (!continueBtn) return false;
+  if (isVerificationPageStillVisible()) return false;
+  if (isAddPhonePageReady()) return false;
+
+  return OAUTH_CONSENT_PAGE_PATTERN.test(getPageTextSnapshot());
+}
+
+async function waitForVerificationSubmitOutcome(step, timeout) {
+  const resolvedTimeout = timeout ?? (step === 7 ? 30000 : 12000);
+  const start = Date.now();
+
+  while (Date.now() - start < resolvedTimeout) {
+    throwIfStopped();
+
+    const errorText = getVerificationErrorText();
+    if (errorText) {
+      return { invalidCode: true, errorText };
+    }
+
+    if (step === 4 && isStep5Ready()) {
+      return { success: true };
+    }
+
+    if (step === 7 && isStep8Ready()) {
+      return { success: true };
+    }
+
+    if (step === 7 && isAddPhonePageReady()) {
+      return { success: true, addPhonePage: true };
+    }
+
+    await sleep(150);
+  }
+
+  if (isVerificationPageStillVisible()) {
+    return {
+      invalidCode: true,
+      errorText: getVerificationErrorText() || '提交后仍停留在验证码页面,准备重新发送验证码。',
+    };
+  }
+
+  return { success: true, assumed: true };
+}
+
 async function fillVerificationCode(step, payload) {
   const { code } = payload;
-  if (!code) throw new Error('No verification code provided.');
+  if (!code) throw new Error('未提供验证码。');
 
-  log(`Step ${step}: Filling verification code: ${code}`);
+  log(`步骤 ${step}:正在填写验证码:${code}`);
+
+  if (step === 7) {
+    await prepareLoginCodeFlow();
+  }
 
   // Find code input — could be a single input or multiple separate inputs
   let codeInput = null;
   try {
-    codeInput = await waitForElement(
-      'input[name="code"], input[name="otp"], input[type="text"][maxlength="6"], input[aria-label*="code"], input[placeholder*="code"], input[placeholder*="Code"], input[inputmode="numeric"]',
-      10000
-    );
+    codeInput = await waitForElement(VERIFICATION_CODE_INPUT_SELECTOR, 10000);
   } catch {
     // Check for multiple single-digit inputs (common pattern)
     const singleInputs = document.querySelectorAll('input[maxlength="1"]');
     if (singleInputs.length >= 6) {
-      log(`Step ${step}: Found single-digit code inputs, filling individually...`);
+      log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`);
       for (let i = 0; i < 6 && i < singleInputs.length; i++) {
         fillInput(singleInputs[i], code[i]);
         await sleep(100);
       }
-      await sleep(1000);
-      reportComplete(step);
-      return;
+      const outcome = await waitForVerificationSubmitOutcome(step);
+      if (outcome.invalidCode) {
+        log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
+      } else if (outcome.addPhonePage) {
+        log(`步骤 ${step}:验证码已通过,并已跳转到手机号页面。`, 'ok');
+      } else {
+        log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
+      }
+      return outcome;
     }
-    throw new Error('Could not find verification code input. URL: ' + location.href);
+    throw new Error('未找到验证码输入框。URL: ' + location.href);
   }
 
   fillInput(codeInput, code);
-  log(`Step ${step}: Code filled`);
+  log(`步骤 ${step}:验证码已填写`);
 
   // Report complete BEFORE submit (page may navigate away)
-  reportComplete(step);
 
   // Submit
   await sleep(500);
@@ -196,8 +529,19 @@ async function fillVerificationCode(step, payload) {
   if (submitBtn) {
     await humanPause(450, 1200);
     simulateClick(submitBtn);
-    log(`Step ${step}: Verification submitted`);
+    log(`步骤 ${step}:验证码已提交`);
+  }
+
+  const outcome = await waitForVerificationSubmitOutcome(step);
+  if (outcome.invalidCode) {
+    log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
+  } else if (outcome.addPhonePage) {
+    log(`步骤 ${step}:验证码已通过,并已跳转到手机号页面。`, 'ok');
+  } else {
+    log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
   }
+
+  return outcome;
 }
 
 // ============================================================
@@ -206,9 +550,9 @@ async function fillVerificationCode(step, payload) {
 
 async function step6_login(payload) {
   const { email, password } = payload;
-  if (!email) throw new Error('No email provided for login.');
+  if (!email) throw new Error('登录时缺少邮箱地址。');
 
-  log(`Step 6: Logging in with ${email}...`);
+  log(`步骤 6:正在使用 ${email} 登录...`);
 
   // Wait for email input on the auth page
   let emailInput = null;
@@ -218,12 +562,12 @@ async function step6_login(payload) {
       15000
     );
   } catch {
-    throw new Error('Could not find email input on login page. URL: ' + location.href);
+    throw new Error('在登录页未找到邮箱输入框。URL: ' + location.href);
   }
 
   await humanPause(500, 1400);
   fillInput(emailInput, email);
-  log('Step 6: Email filled');
+  log('步骤 6:邮箱已填写');
 
   // Submit email
   await sleep(500);
@@ -232,7 +576,7 @@ async function step6_login(payload) {
   if (submitBtn1) {
     await humanPause(400, 1100);
     simulateClick(submitBtn1);
-    log('Step 6: Submitted email');
+    log('步骤 6:邮箱已提交');
   }
 
   await sleep(2000);
@@ -240,7 +584,7 @@ async function step6_login(payload) {
   // Check for password field
   const passwordInput = document.querySelector('input[type="password"]');
   if (passwordInput) {
-    log('Step 6: Password field found, filling password...');
+    log('步骤 6:已找到密码输入框,正在填写密码...');
     await humanPause(550, 1450);
     fillInput(passwordInput, password);
 
@@ -253,13 +597,13 @@ async function step6_login(payload) {
     if (submitBtn2) {
       await humanPause(450, 1200);
       simulateClick(submitBtn2);
-      log('Step 6: Submitted password, may need verification code (step 7)');
+      log('步骤 6:密码已提交,可能还需要验证码(步骤 7)');
     }
     return;
   }
 
   // No password field — OTP flow
-  log('Step 6: No password field. OTP flow or auto-redirect.');
+  log('步骤 6:未发现密码输入框,可能进入验证码流程或自动跳转。');
   reportComplete(6, { needsOTP: true });
 }
 
@@ -271,7 +615,7 @@ async function step6_login(payload) {
 // Background performs the actual click through the debugger Input API.
 
 async function step8_findAndClick() {
-  log('Step 8: Looking for OAuth consent "继续" button...');
+  log('步骤 8:正在查找 OAuth 同意页的“继续”按钮...');
 
   const continueBtn = await findContinueButton();
   await waitForButtonEnabled(continueBtn);
@@ -282,7 +626,7 @@ async function step8_findAndClick() {
   await sleep(250);
 
   const rect = getSerializableRect(continueBtn);
-  log('Step 8: Found "继续" button and prepared debugger click coordinates.');
+  log('步骤 8:已找到“继续”按钮并准备好调试器点击坐标。');
   return {
     rect,
     buttonText: (continueBtn.textContent || '').trim(),
@@ -291,18 +635,20 @@ async function step8_findAndClick() {
 }
 
 async function findContinueButton() {
-  try {
-    return await waitForElement(
-      'button[type="submit"][data-dd-action-name="Continue"], button[type="submit"]._primary_3rdp0_107',
-      10000
-    );
-  } catch {
-    try {
-      return await waitForElementByText('button', /继续|Continue/, 5000);
-    } catch {
-      throw new Error('Could not find "继续" button on OAuth consent page. URL: ' + location.href);
+  const start = Date.now();
+  while (Date.now() - start < 10000) {
+    throwIfStopped();
+    if (isAddPhonePageReady()) {
+      throw new Error('当前页面已进入手机号页面,不是 OAuth 授权同意页。URL: ' + location.href);
+    }
+    const button = getPrimaryContinueButton();
+    if (button && isStep8Ready()) {
+      return button;
     }
+    await sleep(150);
   }
+
+  throw new Error('在 OAuth 同意页未找到“继续”按钮,或页面尚未进入授权同意状态。URL: ' + location.href);
 }
 
 async function waitForButtonEnabled(button, timeout = 8000) {
@@ -312,7 +658,7 @@ async function waitForButtonEnabled(button, timeout = 8000) {
     if (isButtonEnabled(button)) return;
     await sleep(150);
   }
-  throw new Error('"继续" button stayed disabled for too long. URL: ' + location.href);
+  throw new Error('“继续”按钮长时间不可点击。URL: ' + location.href);
 }
 
 function isButtonEnabled(button) {
@@ -324,7 +670,7 @@ function isButtonEnabled(button) {
 function getSerializableRect(el) {
   const rect = el.getBoundingClientRect();
   if (!rect.width || !rect.height) {
-    throw new Error('"继续" button has no clickable size after scrolling. URL: ' + location.href);
+    throw new Error('滚动后“继续”按钮没有可点击尺寸。URL: ' + location.href);
   }
 
   return {
@@ -343,16 +689,16 @@ function getSerializableRect(el) {
 
 async function step5_fillNameBirthday(payload) {
   const { firstName, lastName, age, year, month, day } = payload;
-  if (!firstName || !lastName) throw new Error('No name data provided.');
+  if (!firstName || !lastName) throw new Error('未提供姓名数据。');
 
   const resolvedAge = age ?? (year ? new Date().getFullYear() - Number(year) : null);
   const hasBirthdayData = [year, month, day].every(value => value != null && !Number.isNaN(Number(value)));
   if (!hasBirthdayData && (resolvedAge == null || Number.isNaN(Number(resolvedAge)))) {
-    throw new Error('No birthday or age data provided.');
+    throw new Error('未提供生日或年龄数据。');
   }
 
   const fullName = `${firstName} ${lastName}`;
-  log(`Step 5: Filling name: ${fullName}`);
+  log(`步骤 5:正在填写姓名:${fullName}`);
 
   // Actual DOM structure:
   // - Full name: <input name="name" placeholder="全名" type="text">
@@ -367,11 +713,11 @@ async function step5_fillNameBirthday(payload) {
       10000
     );
   } catch {
-    throw new Error('Could not find name input. URL: ' + location.href);
+    throw new Error('未找到姓名输入框。URL: ' + location.href);
   }
   await humanPause(500, 1300);
   fillInput(nameInput, fullName);
-  log(`Step 5: Name filled: ${fullName}`);
+  log(`步骤 5:姓名已填写:${fullName}`);
 
   let birthdayMode = false;
   let ageInput = null;
@@ -393,7 +739,7 @@ async function step5_fillNameBirthday(payload) {
 
   if (birthdayMode) {
     if (!hasBirthdayData) {
-      throw new Error('Birthday field detected, but no birthday data provided.');
+      throw new Error('检测到生日字段,但未提供生日数据。');
     }
 
     const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
@@ -401,7 +747,7 @@ async function step5_fillNameBirthday(payload) {
     const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
 
     if (yearSpinner && monthSpinner && daySpinner) {
-      log('Step 5: Birthday fields detected, filling birthday...');
+      log('步骤 5:检测到生日字段,正在填写生日...');
 
       async function setSpinButton(el, value) {
         el.focus();
@@ -429,7 +775,7 @@ async function step5_fillNameBirthday(payload) {
       await setSpinButton(monthSpinner, String(month).padStart(2, '0'));
       await humanPause(250, 650);
       await setSpinButton(daySpinner, String(day).padStart(2, '0'));
-      log(`Step 5: Birthday filled: ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
+      log(`步骤 5:生日已填写:${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
     }
 
     const hiddenBirthday = document.querySelector('input[name="birthday"]');
@@ -437,17 +783,17 @@ async function step5_fillNameBirthday(payload) {
       const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
       hiddenBirthday.value = dateStr;
       hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
-      log(`Step 5: Hidden birthday input set: ${dateStr}`);
+      log(`步骤 5:已设置隐藏生日输入框:${dateStr}`);
     }
   } else if (ageInput) {
     if (resolvedAge == null || Number.isNaN(Number(resolvedAge))) {
-      throw new Error('Age field detected, but no age data provided.');
+      throw new Error('检测到年龄字段,但未提供年龄数据。');
     }
     await humanPause(500, 1300);
     fillInput(ageInput, String(resolvedAge));
-    log(`Step 5: Age filled: ${resolvedAge}`);
+    log(`步骤 5:年龄已填写:${resolvedAge}`);
   } else {
-    throw new Error('Could not find birthday or age input. URL: ' + location.href);
+    throw new Error('未找到生日或年龄输入项。URL: ' + location.href);
   }
 
   // Click "完成帐户创建" button
@@ -461,6 +807,6 @@ async function step5_fillNameBirthday(payload) {
   if (completeBtn) {
     await humanPause(500, 1300);
     simulateClick(completeBtn);
-    log('Step 5: Clicked "完成帐户创建"');
+    log('步骤 5:已点击“完成帐户创建”');
   }
 }

+ 26 - 26
content/utils.js

@@ -13,7 +13,7 @@ const SCRIPT_SOURCE = (() => {
 })();
 
 const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
-const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
+const STOP_ERROR_MESSAGE = '流程已被用户停止。';
 let flowStopped = false;
 
 chrome.runtime.onMessage.addListener((message) => {
@@ -50,14 +50,14 @@ function waitForElement(selector, timeout = 10000) {
 
     const existing = document.querySelector(selector);
     if (existing) {
-      console.log(LOG_PREFIX, `Found immediately: ${selector}`);
-      log(`Found element: ${selector}`);
+      console.log(LOG_PREFIX, `立即找到元素: ${selector}`);
+      log(`已找到元素:${selector}`);
       resolve(existing);
       return;
     }
 
-    console.log(LOG_PREFIX, `Waiting for: ${selector} (timeout: ${timeout}ms)`);
-    log(`Waiting for selector: ${selector}...`);
+    console.log(LOG_PREFIX, `等待元素: ${selector}(超时 ${timeout}ms)`);
+    log(`正在等待选择器:${selector}...`);
 
     let settled = false;
     let stopTimer = null;
@@ -78,8 +78,8 @@ function waitForElement(selector, timeout = 10000) {
       const el = document.querySelector(selector);
       if (el) {
         cleanup();
-        console.log(LOG_PREFIX, `Found after wait: ${selector}`);
-        log(`Found element: ${selector}`);
+        console.log(LOG_PREFIX, `等待后找到元素: ${selector}`);
+        log(`已找到元素:${selector}`);
         resolve(el);
       }
     });
@@ -91,7 +91,7 @@ function waitForElement(selector, timeout = 10000) {
 
     const timer = setTimeout(() => {
       cleanup();
-      const msg = `Timeout waiting for ${selector} after ${timeout}ms on ${location.href}`;
+      const msg = `在 ${location.href} 等待 ${selector} 超时,已超过 ${timeout}ms`;
       console.error(LOG_PREFIX, msg);
       reject(new Error(msg));
     }, timeout);
@@ -132,14 +132,14 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
 
     const existing = search();
     if (existing) {
-      console.log(LOG_PREFIX, `Found by text immediately: ${containerSelector} matching ${textPattern}`);
-      log(`Found element by text: ${textPattern}`);
+      console.log(LOG_PREFIX, `立即按文本找到元素: ${containerSelector} 匹配 ${textPattern}`);
+      log(`已按文本找到元素:${textPattern}`);
       resolve(existing);
       return;
     }
 
-    console.log(LOG_PREFIX, `Waiting for text match: ${containerSelector} / ${textPattern}`);
-    log(`Waiting for element with text: ${textPattern}...`);
+    console.log(LOG_PREFIX, `等待文本匹配: ${containerSelector} / ${textPattern}`);
+    log(`正在等待包含文本的元素:${textPattern}...`);
 
     let settled = false;
     let stopTimer = null;
@@ -160,8 +160,8 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
       const el = search();
       if (el) {
         cleanup();
-        console.log(LOG_PREFIX, `Found by text after wait: ${textPattern}`);
-        log(`Found element by text: ${textPattern}`);
+        console.log(LOG_PREFIX, `等待后按文本找到元素: ${textPattern}`);
+        log(`已按文本找到元素:${textPattern}`);
         resolve(el);
       }
     });
@@ -173,7 +173,7 @@ function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
 
     const timer = setTimeout(() => {
       cleanup();
-      const msg = `Timeout waiting for text "${textPattern}" in "${containerSelector}" after ${timeout}ms on ${location.href}`;
+      const msg = `在 ${location.href} 的 ${containerSelector} 中等待文本 "${textPattern}" 超时,已超过 ${timeout}ms`;
       console.error(LOG_PREFIX, msg);
       reject(new Error(msg));
     }, timeout);
@@ -206,8 +206,8 @@ function fillInput(el, value) {
   nativeInputValueSetter.call(el, value);
   el.dispatchEvent(new Event('input', { bubbles: true }));
   el.dispatchEvent(new Event('change', { bubbles: true }));
-  console.log(LOG_PREFIX, `Filled input ${el.name || el.id || el.type} with: ${value}`);
-  log(`Filled input [${el.name || el.id || el.type || 'unknown'}]`);
+  console.log(LOG_PREFIX, `已填写输入框 ${el.name || el.id || el.type}: ${value}`);
+  log(`已填写输入框 [${el.name || el.id || el.type || '未知'}]`);
 }
 
 /**
@@ -219,8 +219,8 @@ function fillSelect(el, value) {
   throwIfStopped();
   el.value = value;
   el.dispatchEvent(new Event('change', { bubbles: true }));
-  console.log(LOG_PREFIX, `Selected value ${value} in ${el.name || el.id}`);
-  log(`Selected [${el.name || el.id || 'unknown'}] = ${value}`);
+  console.log(LOG_PREFIX, `已在 ${el.name || el.id} 中选择值: ${value}`);
+  log(`已选择 [${el.name || el.id || '未知'}] = ${value}`);
 }
 
 /**
@@ -242,7 +242,7 @@ function log(message, level = 'info') {
  * Report that this content script is loaded and ready.
  */
 function reportReady() {
-  console.log(LOG_PREFIX, 'Content script ready');
+  console.log(LOG_PREFIX, '内容脚本已就绪');
   chrome.runtime.sendMessage({
     type: 'CONTENT_SCRIPT_READY',
     source: SCRIPT_SOURCE,
@@ -258,8 +258,8 @@ function reportReady() {
  * @param {Object} data - Step output data
  */
 function reportComplete(step, data = {}) {
-  console.log(LOG_PREFIX, `Step ${step} completed`, data);
-  log(`Step ${step} completed successfully`, 'ok');
+  console.log(LOG_PREFIX, `步骤 ${step} 已完成`, data);
+  log(`步骤 ${step} 已成功完成`, 'ok');
   chrome.runtime.sendMessage({
     type: 'STEP_COMPLETE',
     source: SCRIPT_SOURCE,
@@ -275,8 +275,8 @@ function reportComplete(step, data = {}) {
  * @param {string} errorMessage
  */
 function reportError(step, errorMessage) {
-  console.error(LOG_PREFIX, `Step ${step} failed: ${errorMessage}`);
-  log(`Step ${step} failed: ${errorMessage}`, 'error');
+  console.error(LOG_PREFIX, `步骤 ${step} 失败: ${errorMessage}`);
+  log(`步骤 ${step} 失败:${errorMessage}`, 'error');
   chrome.runtime.sendMessage({
     type: 'STEP_ERROR',
     source: SCRIPT_SOURCE,
@@ -293,8 +293,8 @@ function reportError(step, errorMessage) {
 function simulateClick(el) {
   throwIfStopped();
   el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
-  console.log(LOG_PREFIX, `Clicked: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
-  log(`Clicked [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`);
+  console.log(LOG_PREFIX, `已点击: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
+  log(`已点击 [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`);
 }
 
 /**

+ 188 - 52
content/vps-panel.js

@@ -33,7 +33,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
       sendResponse({ ok: true });
     }).catch(err => {
       if (isStopError(err)) {
-        log(`Step ${message.step}: Stopped by user.`, 'warn');
+        log(`步骤 ${message.step}:已被用户停止。`, 'warn');
         sendResponse({ stopped: true, error: err.message });
         return;
       }
@@ -46,65 +46,199 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
 
 async function handleStep(step, payload) {
   switch (step) {
-    case 1: return await step1_getOAuthLink();
+    case 1: return await step1_getOAuthLink(payload);
     case 9: return await step9_vpsVerify(payload);
     default:
-      throw new Error(`vps-panel.js does not handle step ${step}`);
+      throw new Error(`vps-panel.js 不处理步骤 ${step}`);
   }
 }
 
+function isVisibleElement(el) {
+  if (!el) return false;
+  const style = window.getComputedStyle(el);
+  const rect = el.getBoundingClientRect();
+  return style.display !== 'none'
+    && style.visibility !== 'hidden'
+    && rect.width > 0
+    && rect.height > 0;
+}
+
+function getActionText(el) {
+  return [
+    el?.textContent,
+    el?.value,
+    el?.getAttribute?.('aria-label'),
+    el?.getAttribute?.('title'),
+  ]
+    .filter(Boolean)
+    .join(' ')
+    .replace(/\s+/g, ' ')
+    .trim();
+}
+
+function findManagementKeyInput() {
+  const candidates = document.querySelectorAll(
+    '.LoginPage-module__loginCard___OgP-R input[type="password"], input[placeholder*="管理密钥"], input[aria-label*="管理密钥"]'
+  );
+  return Array.from(candidates).find(isVisibleElement) || null;
+}
+
+function findManagementLoginButton() {
+  const candidates = document.querySelectorAll('.LoginPage-module__loginCard___OgP-R button, .LoginPage-module__loginCard___OgP-R .btn');
+  return Array.from(candidates).find((el) => {
+    if (!isVisibleElement(el)) return false;
+    return /登录|login/i.test(getActionText(el));
+  }) || null;
+}
+
+function findRememberPasswordCheckbox() {
+  const candidates = document.querySelectorAll('.LoginPage-module__loginCard___OgP-R input[type="checkbox"]');
+  return Array.from(candidates).find((el) => {
+    const label = el.closest('label');
+    const text = getActionText(label || el);
+    return /记住密码|remember/i.test(text);
+  }) || null;
+}
+
+function findOAuthNavLink() {
+  const candidates = document.querySelectorAll('a[href*="#/oauth"], a.nav-item, button, [role="link"], [role="button"]');
+  return Array.from(candidates).find((el) => {
+    if (!isVisibleElement(el)) return false;
+    const text = getActionText(el);
+    const href = el.getAttribute('href') || '';
+    return href.includes('#/oauth') || /oauth/i.test(text);
+  }) || null;
+}
+
+function findCodexOAuthHeader() {
+  const candidates = document.querySelectorAll('.card-header, [class*="cardHeader"], .card, [class*="card"]');
+  return Array.from(candidates).find((el) => {
+    if (!isVisibleElement(el)) return false;
+    const text = (el.textContent || '').toLowerCase();
+    return text.includes('codex') && text.includes('oauth');
+  }) || null;
+}
+
+function findOAuthCardLoginButton(header) {
+  const card = header?.closest('.card, [class*="card"]') || header?.parentElement || document;
+  const candidates = card.querySelectorAll('button.btn.btn-primary, button.btn-primary, button.btn');
+  return Array.from(candidates).find((el) => isVisibleElement(el) && /登录|login/i.test(getActionText(el))) || null;
+}
+
+function findAuthUrlElement() {
+  const candidates = document.querySelectorAll('[class*="authUrlValue"], .OAuthPage-module__authUrlValue___axvUJ');
+  return Array.from(candidates).find((el) => isVisibleElement(el) && /^https?:\/\//i.test((el.textContent || '').trim())) || null;
+}
+
+async function ensureOAuthManagementPage(vpsPassword, step = 1, timeout = 45000) {
+  const start = Date.now();
+  let lastLoginAttemptAt = 0;
+  let lastOauthNavAttemptAt = 0;
+
+  while (Date.now() - start < timeout) {
+    throwIfStopped();
+
+    const authUrlEl = findAuthUrlElement();
+    if (authUrlEl) {
+      return { header: findCodexOAuthHeader(), authUrlEl };
+    }
+
+    const oauthHeader = findCodexOAuthHeader();
+    if (oauthHeader) {
+      return { header: oauthHeader, authUrlEl: null };
+    }
+
+    const managementKeyInput = findManagementKeyInput();
+    const managementLoginButton = findManagementLoginButton();
+    if (managementKeyInput && managementLoginButton) {
+      if (!vpsPassword) {
+        throw new Error('VPS 面板需要管理密钥,请先在侧边栏填写 VPS Key(管理密钥)。');
+      }
+
+      if ((managementKeyInput.value || '') !== vpsPassword) {
+        await humanPause(350, 900);
+        fillInput(managementKeyInput, vpsPassword);
+        log(`步骤 ${step}:已填写 VPS 管理密钥。`);
+      }
+
+      const rememberCheckbox = findRememberPasswordCheckbox();
+      if (rememberCheckbox && !rememberCheckbox.checked) {
+        simulateClick(rememberCheckbox);
+        log(`步骤 ${step}:已勾选 VPS 面板“记住密码”。`);
+        await sleep(300);
+      }
+
+      if (Date.now() - lastLoginAttemptAt > 3000) {
+        lastLoginAttemptAt = Date.now();
+        await humanPause(350, 900);
+        simulateClick(managementLoginButton);
+        log(`步骤 ${step}:已提交 VPS 管理登录。`);
+      }
+
+      await sleep(1500);
+      continue;
+    }
+
+    const oauthNavLink = findOAuthNavLink();
+    if (oauthNavLink && Date.now() - lastOauthNavAttemptAt > 2000) {
+      lastOauthNavAttemptAt = Date.now();
+      await humanPause(300, 800);
+      simulateClick(oauthNavLink);
+      log(`步骤 ${step}:已打开“OAuth 登录”导航。`);
+      await sleep(1200);
+      continue;
+    }
+
+    await sleep(250);
+  }
+
+  throw new Error('无法进入 VPS 的 OAuth 管理页面,请检查面板是否正常加载。URL: ' + location.href);
+}
+
 // ============================================================
 // Step 1: Get OAuth Link
 // ============================================================
 
-async function step1_getOAuthLink() {
-  log('Step 1: Waiting for VPS panel to load (auto-login may take a moment)...');
+async function step1_getOAuthLink(payload) {
+  const { vpsPassword } = payload || {};
 
-  // The page may start at #/login and auto-redirect to #/oauth.
-  // Wait for the Codex OAuth card to appear (up to 30s for auto-login + redirect).
-  let loginBtn = null;
-  try {
-    // Wait for any card-header containing "Codex" to appear
-    const header = await waitForElementByText('.card-header', /codex/i, 30000);
-    loginBtn = header.querySelector('button.btn.btn-primary, button.btn');
-    log('Step 1: Found Codex OAuth card');
-  } catch {
-    throw new Error(
-      'Codex OAuth card did not appear after 30s. Page may still be loading or not logged in. ' +
-      'Current URL: ' + location.href
-    );
-  }
+  log('步骤 1:正在等待 VPS 面板加载并进入 OAuth 页面...');
 
-  if (!loginBtn) {
-    throw new Error('Found Codex OAuth card but no login button inside it. URL: ' + location.href);
-  }
+  const { header, authUrlEl: existingAuthUrlEl } = await ensureOAuthManagementPage(vpsPassword, 1);
+  let authUrlEl = existingAuthUrlEl;
 
-  // Check if button is disabled (already clicked / loading)
-  if (loginBtn.disabled) {
-    log('Step 1: Login button is disabled (already loading), waiting for auth URL...');
-  } else {
-    await humanPause(500, 1400);
-    simulateClick(loginBtn);
-    log('Step 1: Clicked login button, waiting for auth URL...');
-  }
+  if (!authUrlEl) {
+    const loginBtn = findOAuthCardLoginButton(header);
+    if (!loginBtn) {
+      throw new Error('已找到 Codex OAuth 卡片,但卡片内没有登录按钮。URL: ' + location.href);
+    }
 
-  // Wait for the auth URL to appear in the specific div
-  let authUrlEl = null;
-  try {
-    authUrlEl = await waitForElement('[class*="authUrlValue"]', 15000);
-  } catch {
-    throw new Error(
-      'Auth URL did not appear after clicking login. ' +
-      'Check if VPS panel is logged in and Codex service is running. URL: ' + location.href
-    );
+    if (loginBtn.disabled) {
+      log('步骤 1:OAuth 登录按钮当前不可用,正在等待授权链接出现...');
+    } else {
+      await humanPause(500, 1400);
+      simulateClick(loginBtn);
+      log('步骤 1:已点击 OAuth 登录按钮,正在等待授权链接...');
+    }
+
+    try {
+      authUrlEl = await waitForElement('[class*="authUrlValue"]', 15000);
+    } catch {
+      throw new Error(
+        '点击 OAuth 登录按钮后未出现授权链接。' +
+        '请检查 VPS 面板服务是否正在运行。URL: ' + location.href
+      );
+    }
+  } else {
+    log('步骤 1:VPS 面板上已显示授权链接。');
   }
 
   const oauthUrl = (authUrlEl.textContent || '').trim();
   if (!oauthUrl || !oauthUrl.startsWith('http')) {
-    throw new Error(`Invalid OAuth URL found: "${oauthUrl.slice(0, 50)}". Expected URL starting with http.`);
+    throw new Error(`拿到的 OAuth 链接无效:\"${oauthUrl.slice(0, 50)}\"。应为 http 开头的 URL。`);
   }
 
-  log(`Step 1: OAuth URL obtained: ${oauthUrl.slice(0, 80)}...`, 'ok');
+  log(`步骤 1:已获取 OAuth 链接:${oauthUrl.slice(0, 80)}...`, 'ok');
   reportComplete(1, { oauthUrl });
 }
 
@@ -113,19 +247,21 @@ async function step1_getOAuthLink() {
 // ============================================================
 
 async function step9_vpsVerify(payload) {
+  await ensureOAuthManagementPage(payload?.vpsPassword, 9);
+
   // Get localhostUrl from payload (passed directly by background) or fallback to state
   let localhostUrl = payload?.localhostUrl;
   if (!localhostUrl) {
-    log('Step 9: localhostUrl not in payload, fetching from state...');
+    log('步骤 9:payload 中没有 localhostUrl,正在从状态中读取...');
     const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
     localhostUrl = state.localhostUrl;
   }
   if (!localhostUrl) {
-    throw new Error('No localhost URL found. Complete step 8 first.');
+    throw new Error('未找到 localhost 回调地址,请先完成步骤 8。');
   }
-  log(`Step 9: Got localhostUrl: ${localhostUrl.slice(0, 60)}...`);
+  log(`步骤 9:已获取 localhostUrl:${localhostUrl.slice(0, 60)}...`);
 
-  log('Step 9: Looking for callback URL input...');
+  log('步骤 9:正在查找回调地址输入框...');
 
   // Find the callback URL input
   // Actual DOM: <input class="input" placeholder="http://localhost:1455/auth/callback?code=...&state=...">
@@ -136,13 +272,13 @@ async function step9_vpsVerify(payload) {
     try {
       urlInput = await waitForElement('input[placeholder*="localhost"]', 5000);
     } catch {
-      throw new Error('Could not find callback URL input on VPS panel. URL: ' + location.href);
+      throw new Error('在 VPS 面板中未找到回调地址输入框。URL: ' + location.href);
     }
   }
 
   await humanPause(600, 1500);
   fillInput(urlInput, localhostUrl);
-  log(`Step 9: Filled callback URL: ${localhostUrl.slice(0, 80)}...`);
+  log(`步骤 9:已填写回调地址:${localhostUrl.slice(0, 80)}...`);
 
   // Find and click "提交回调 URL" button
   let submitBtn = null;
@@ -156,26 +292,26 @@ async function step9_vpsVerify(payload) {
     try {
       submitBtn = await waitForElementByText('button.btn', /提交回调/, 5000);
     } catch {
-      throw new Error('Could not find "提交回调 URL" button. URL: ' + location.href);
+      throw new Error('未找到“提交回调 URL”按钮。URL: ' + location.href);
     }
   }
 
   await humanPause(450, 1200);
   simulateClick(submitBtn);
-  log('Step 9: Clicked "提交回调 URL", waiting for authentication result...');
+  log('步骤 9:已点击“提交回调 URL”,正在等待认证结果...');
 
   // Wait for "认证成功!" status badge to appear
   try {
     await waitForElementByText('.status-badge, [class*="status"]', /认证成功/, 30000);
-    log('Step 9: Authentication successful!', 'ok');
+    log('步骤 9:认证成功!', 'ok');
   } catch {
     // Check if there's an error message instead
     const statusEl = document.querySelector('.status-badge, [class*="status"]');
     const statusText = statusEl ? statusEl.textContent : 'unknown';
     if (/成功|success/i.test(statusText)) {
-      log('Step 9: Authentication successful!', 'ok');
+      log('步骤 9:认证成功!', 'ok');
     } else {
-      log(`Step 9: Status after submit: "${statusText}". May still be processing.`, 'warn');
+      log(`步骤 9:提交后的状态为“${statusText}”,可能仍在处理中。`, 'warn');
     }
   }
 

+ 2 - 2
manifest.json

@@ -1,8 +1,8 @@
 {
   "manifest_version": 3,
-  "name": "Multi-Page Automation",
+  "name": "多页面自动化",
   "version": "1.1.0",
-  "description": "Automates multi-step OAuth registration workflow",
+  "description": "用于自动执行多步骤 OAuth 注册流程",
   "permissions": [
     "sidePanel",
     "tabs",

+ 23 - 0
sidepanel/sidepanel.css

@@ -244,6 +244,10 @@ header {
   gap: 8px;
 }
 
+.data-check-row {
+  align-items: flex-start;
+}
+
 .data-inline {
   display: flex;
   align-items: center;
@@ -323,6 +327,25 @@ header {
 .data-select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
 [data-theme="dark"] .data-select { color-scheme: dark; }
 
+.data-check {
+  display: flex;
+  align-items: flex-start;
+  gap: 8px;
+  flex: 1;
+  min-width: 0;
+  font-size: 12px;
+  line-height: 1.5;
+  color: var(--text-secondary);
+  cursor: pointer;
+}
+
+.data-check input[type="checkbox"] {
+  margin-top: 2px;
+  accent-color: var(--blue);
+  cursor: pointer;
+  flex-shrink: 0;
+}
+
 /* Status Bar */
 .status-bar {
   display: flex;

+ 50 - 39
sidepanel/sidepanel.html

@@ -3,7 +3,7 @@
 <head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <title>Multi-Page Automation</title>
+  <title>多页面自动化面板</title>
   <link rel="preconnect" href="https://fonts.googleapis.com">
   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
   <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
@@ -15,23 +15,23 @@
       <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
         <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
       </svg>
-      <h1>MultiPage</h1>
+      <h1>多页面</h1>
     </div>
     <div class="header-btns">
       <div class="run-group">
-        <input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="Number of runs" />
-        <button id="btn-auto-run" class="btn btn-success" title="Run all steps automatically">
+        <input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="运行次数" />
+        <button id="btn-auto-run" class="btn btn-success" title="自动执行全部步骤">
           <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
-          Auto
+          自动
         </button>
-        <button id="btn-stop" class="btn btn-danger" title="Stop current flow" disabled>Stop</button>
+        <button id="btn-stop" class="btn btn-danger" title="停止当前流程" disabled>停止</button>
       </div>
-      <button id="btn-reset" class="btn btn-ghost" title="Reset all steps">
+      <button id="btn-reset" class="btn btn-ghost" title="重置全部步骤">
         <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
           <polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
         </svg>
       </button>
-      <button id="btn-theme" class="theme-toggle" title="Toggle theme">
+      <button id="btn-theme" class="theme-toggle" title="切换主题">
         <svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
           <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
         </svg>
@@ -49,104 +49,115 @@
         <input type="password" id="input-vps-url" class="data-input" placeholder="http://ip:port/management.html#/oauth" />
       </div>
       <div class="data-row">
-        <span class="data-label">Mail</span>
+        <span class="data-label">管理密钥</span>
+        <input type="password" id="input-vps-password" class="data-input" placeholder="请输入管理密钥" />
+      </div>
+      <div class="data-row">
+        <span class="data-label">邮箱服务</span>
         <select id="select-mail-provider" class="data-select">
-          <option value="163">163 Mail (mail.163.com)</option>
-          <option value="qq">QQ Mail (wx.mail.qq.com)</option>
-          <option value="inbucket">Inbucket (custom host)</option>
+          <option value="163">163 邮箱 (mail.163.com)</option>
+          <option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
+          <option value="inbucket">Inbucket(自定义主机)</option>
         </select>
       </div>
       <div class="data-row" id="row-inbucket-host" style="display:none;">
         <span class="data-label">Inbucket</span>
-        <input type="text" id="input-inbucket-host" class="data-input" placeholder="your-inbucket-host or https://your-inbucket-host" />
+        <input type="text" id="input-inbucket-host" class="data-input" placeholder="填写主机或 https://主机地址" />
       </div>
       <div class="data-row" id="row-inbucket-mailbox" style="display:none;">
-        <span class="data-label">Mailbox</span>
-        <input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="e.g. zju2001" />
+        <span class="data-label">邮箱名</span>
+        <input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="例如 zju2001" />
       </div>
       <div class="data-row">
-        <span class="data-label">Email</span>
+        <span class="data-label">邮箱</span>
         <div class="data-inline">
-          <input type="text" id="input-email" class="data-input" placeholder="Paste DuckDuckGo email" />
-          <button id="btn-fetch-email" class="btn btn-outline btn-sm" type="button">Auto</button>
+          <input type="text" id="input-email" class="data-input" placeholder="粘贴 DuckDuckGo 邮箱" />
+          <button id="btn-fetch-email" class="btn btn-outline btn-sm" type="button">获取</button>
         </div>
       </div>
       <div class="data-row">
-        <span class="data-label">Password</span>
+        <span class="data-label">密码</span>
         <div class="data-inline">
-          <input type="password" id="input-password" class="data-input" placeholder="Leave blank to auto-generate" />
-          <button id="btn-toggle-password" class="btn btn-outline btn-sm" type="button">Show</button>
+          <input type="password" id="input-password" class="data-input" placeholder="留空则自动生成" />
+          <button id="btn-toggle-password" class="btn btn-outline btn-sm" type="button">显示</button>
         </div>
       </div>
+      <div class="data-row data-check-row">
+        <span class="data-label">兜底</span>
+        <label class="data-check" for="input-auto-skip-failures">
+          <input type="checkbox" id="input-auto-skip-failures" />
+          <span>出现错误无法继续时,直接丢弃当前线程并新开一轮,直到补足目标运行次数</span>
+        </label>
+      </div>
       <div class="data-row">
         <span class="data-label">OAuth</span>
-        <span id="display-oauth-url" class="data-value mono truncate">Waiting...</span>
+        <span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
       </div>
       <div class="data-row">
-        <span class="data-label">Callback</span>
-        <span id="display-localhost-url" class="data-value mono truncate">Waiting...</span>
+        <span class="data-label">回调</span>
+        <span id="display-localhost-url" class="data-value mono truncate">等待中...</span>
       </div>
     </div>
     <div id="status-bar" class="status-bar">
       <div class="status-dot"></div>
-      <span id="display-status">Ready</span>
+      <span id="display-status">就绪</span>
     </div>
     <div id="auto-continue-bar" class="auto-continue-bar" style="display:none;">
       <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
-      <span class="auto-hint">Use Auto to fetch Duck email, or paste manually, then continue</span>
-      <button id="btn-auto-continue" class="btn btn-primary btn-sm">Continue</button>
+      <span class="auto-hint">先自动获取 Duck 邮箱,或手动粘贴邮箱后再继续</span>
+      <button id="btn-auto-continue" class="btn btn-primary btn-sm">继续</button>
     </div>
   </section>
 
   <section id="steps-section">
     <div class="steps-header">
-      <span class="section-label">Workflow</span>
+      <span class="section-label">流程</span>
       <span id="steps-progress" class="steps-progress">0 / 9</span>
     </div>
     <div class="steps-list">
       <div class="step-row" data-step="1">
         <div class="step-indicator" data-step="1"><span class="step-num">1</span></div>
-        <button class="step-btn" data-step="1">Get OAuth Link</button>
+        <button class="step-btn" data-step="1">获取 OAuth 链接</button>
         <span class="step-status" data-step="1"></span>
       </div>
       <div class="step-row" data-step="2">
         <div class="step-indicator" data-step="2"><span class="step-num">2</span></div>
-        <button class="step-btn" data-step="2">Open Signup</button>
+        <button class="step-btn" data-step="2">打开注册页</button>
         <span class="step-status" data-step="2"></span>
       </div>
       <div class="step-row" data-step="3">
         <div class="step-indicator" data-step="3"><span class="step-num">3</span></div>
-        <button class="step-btn" data-step="3">Fill Email / Password</button>
+        <button class="step-btn" data-step="3">填写邮箱和密码</button>
         <span class="step-status" data-step="3"></span>
       </div>
       <div class="step-row" data-step="4">
         <div class="step-indicator" data-step="4"><span class="step-num">4</span></div>
-        <button class="step-btn" data-step="4">Get Signup Code</button>
+        <button class="step-btn" data-step="4">获取注册验证码</button>
         <span class="step-status" data-step="4"></span>
       </div>
       <div class="step-row" data-step="5">
         <div class="step-indicator" data-step="5"><span class="step-num">5</span></div>
-        <button class="step-btn" data-step="5">Fill Name / Birthday</button>
+        <button class="step-btn" data-step="5">填写姓名和生日</button>
         <span class="step-status" data-step="5"></span>
       </div>
       <div class="step-row" data-step="6">
         <div class="step-indicator" data-step="6"><span class="step-num">6</span></div>
-        <button class="step-btn" data-step="6">Login via OAuth</button>
+        <button class="step-btn" data-step="6">OAuth 登录</button>
         <span class="step-status" data-step="6"></span>
       </div>
       <div class="step-row" data-step="7">
         <div class="step-indicator" data-step="7"><span class="step-num">7</span></div>
-        <button class="step-btn" data-step="7">Get Login Code</button>
+        <button class="step-btn" data-step="7">获取登录验证码</button>
         <span class="step-status" data-step="7"></span>
       </div>
       <div class="step-row" data-step="8">
         <div class="step-indicator" data-step="8"><span class="step-num">8</span></div>
-        <button class="step-btn" data-step="8">OAuth Auto Confirm</button>
+        <button class="step-btn" data-step="8">自动确认 OAuth</button>
         <span class="step-status" data-step="8"></span>
       </div>
       <div class="step-row" data-step="9">
         <div class="step-indicator" data-step="9"><span class="step-num">9</span></div>
-        <button class="step-btn" data-step="9">VPS Verify</button>
+        <button class="step-btn" data-step="9">VPS 回调验证</button>
         <span class="step-status" data-step="9"></span>
       </div>
     </div>
@@ -154,8 +165,8 @@
 
   <section id="log-section">
     <div class="log-header">
-      <span class="section-label">Console</span>
-      <button id="btn-clear-log" class="btn btn-ghost btn-xs" title="Clear log">Clear</button>
+      <span class="section-label">日志</span>
+      <button id="btn-clear-log" class="btn btn-ghost btn-xs" title="清空日志">清空</button>
     </div>
     <div id="log-area"></div>
   </section>

+ 76 - 35
sidepanel/sidepanel.js

@@ -25,12 +25,14 @@ const btnAutoContinue = document.getElementById('btn-auto-continue');
 const autoContinueBar = document.getElementById('auto-continue-bar');
 const btnClearLog = document.getElementById('btn-clear-log');
 const inputVpsUrl = document.getElementById('input-vps-url');
+const inputVpsPassword = document.getElementById('input-vps-password');
 const selectMailProvider = document.getElementById('select-mail-provider');
 const rowInbucketHost = document.getElementById('row-inbucket-host');
 const inputInbucketHost = document.getElementById('input-inbucket-host');
 const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
 const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
 const inputRunCount = document.getElementById('input-run-count');
+const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
 
 // ============================================================
 // Toast Notifications
@@ -45,6 +47,13 @@ const TOAST_ICONS = {
   info: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
 };
 
+const LOG_LEVEL_LABELS = {
+  info: '信息',
+  ok: '成功',
+  warn: '警告',
+  error: '错误',
+};
+
 function showToast(message, type = 'error', duration = 4000) {
   const toast = document.createElement('div');
   toast.className = `toast toast-${type}`;
@@ -87,6 +96,9 @@ async function restoreState() {
     if (state.vpsUrl) {
       inputVpsUrl.value = state.vpsUrl;
     }
+    if (state.vpsPassword) {
+      inputVpsPassword.value = state.vpsPassword;
+    }
     if (state.mailProvider) {
       selectMailProvider.value = state.mailProvider;
     }
@@ -96,6 +108,7 @@ async function restoreState() {
     if (state.inbucketMailbox) {
       inputInbucketMailbox.value = state.inbucketMailbox;
     }
+    inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures);
 
     if (state.stepStatuses) {
       for (const [step, status] of Object.entries(state.stepStatuses)) {
@@ -194,21 +207,21 @@ function updateStatusDisplay(state) {
 
   const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
   if (running) {
-    displayStatus.textContent = `Step ${running[0]} running...`;
+    displayStatus.textContent = `步骤 ${running[0]} 运行中...`;
     statusBar.classList.add('running');
     return;
   }
 
   const failed = Object.entries(state.stepStatuses).find(([, s]) => s === 'failed');
   if (failed) {
-    displayStatus.textContent = `Step ${failed[0]} failed`;
+    displayStatus.textContent = `步骤 ${failed[0]} 失败`;
     statusBar.classList.add('failed');
     return;
   }
 
   const stopped = Object.entries(state.stepStatuses).find(([, s]) => s === 'stopped');
   if (stopped) {
-    displayStatus.textContent = `Step ${stopped[0]} stopped`;
+    displayStatus.textContent = `步骤 ${stopped[0]} 已停止`;
     statusBar.classList.add('stopped');
     return;
   }
@@ -219,28 +232,28 @@ function updateStatusDisplay(state) {
     .sort((a, b) => b - a)[0];
 
   if (lastCompleted === 9) {
-    displayStatus.textContent = 'All steps completed!';
+    displayStatus.textContent = '全部步骤已完成';
     statusBar.classList.add('completed');
   } else if (lastCompleted) {
-    displayStatus.textContent = `Step ${lastCompleted} done`;
+    displayStatus.textContent = `步骤 ${lastCompleted} 已完成`;
   } else {
-    displayStatus.textContent = 'Ready';
+    displayStatus.textContent = '就绪';
   }
 }
 
 function appendLog(entry) {
-  const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
-  const levelLabel = entry.level.toUpperCase();
+  const time = new Date(entry.timestamp).toLocaleTimeString('zh-CN', { hour12: false });
+  const levelLabel = LOG_LEVEL_LABELS[entry.level] || entry.level;
   const line = document.createElement('div');
   line.className = `log-line log-${entry.level}`;
 
-  const stepMatch = entry.message.match(/Step (\d)/);
-  const stepNum = stepMatch ? stepMatch[1] : null;
+  const stepMatch = entry.message.match(/(?:Step\s+(\d+)|步骤\s*(\d+))/);
+  const stepNum = stepMatch ? (stepMatch[1] || stepMatch[2]) : null;
 
   let html = `<span class="log-time">${time}</span> `;
   html += `<span class="log-level log-level-${entry.level}">${levelLabel}</span> `;
   if (stepNum) {
-    html += `<span class="log-step-tag step-${stepNum}">S${stepNum}</span>`;
+    html += `<span class="log-step-tag step-${stepNum}">${stepNum}</span>`;
   }
   html += `<span class="log-msg">${escapeHtml(entry.message)}</span>`;
 
@@ -256,7 +269,7 @@ function escapeHtml(text) {
 }
 
 async function fetchDuckEmail() {
-  const defaultLabel = 'Auto';
+  const defaultLabel = '获取';
   btnFetchEmail.disabled = true;
   btnFetchEmail.textContent = '...';
 
@@ -271,14 +284,14 @@ async function fetchDuckEmail() {
       throw new Error(response.error);
     }
     if (!response?.email) {
-      throw new Error('Duck email was not returned.');
+      throw new Error('未返回 Duck 邮箱。');
     }
 
     inputEmail.value = response.email;
-    showToast(`Fetched ${response.email}`, 'success', 2500);
+    showToast(`已获取 ${response.email}`, 'success', 2500);
     return response.email;
   } catch (err) {
-    showToast(`Auto fetch failed: ${err.message}`, 'error');
+    showToast(`自动获取失败:${err.message}`, 'error');
     throw err;
   } finally {
     btnFetchEmail.disabled = false;
@@ -287,7 +300,7 @@ async function fetchDuckEmail() {
 }
 
 function syncPasswordToggleLabel() {
-  btnTogglePassword.textContent = inputPassword.type === 'password' ? 'Show' : 'Hide';
+  btnTogglePassword.textContent = inputPassword.type === 'password' ? '显示' : '隐藏';
 }
 
 // ============================================================
@@ -300,7 +313,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
     if (step === 3) {
       const email = inputEmail.value.trim();
       if (!email) {
-        showToast('Please paste email address or use Auto first', 'warn');
+        showToast('请先粘贴邮箱,或先点击获取。', 'warn');
         return;
       }
       await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
@@ -322,7 +335,7 @@ btnTogglePassword.addEventListener('click', () => {
 btnStop.addEventListener('click', async () => {
   btnStop.disabled = true;
   await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} });
-  showToast('Stopping current flow...', 'warn', 2000);
+  showToast('正在停止当前流程...', 'warn', 2000);
 });
 
 // Auto Run
@@ -330,14 +343,21 @@ btnAutoRun.addEventListener('click', async () => {
   const totalRuns = parseInt(inputRunCount.value) || 1;
   btnAutoRun.disabled = true;
   inputRunCount.disabled = true;
-  btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> Running...';
-  await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel', payload: { totalRuns } });
+  btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 运行中...';
+  await chrome.runtime.sendMessage({
+    type: 'AUTO_RUN',
+    source: 'sidepanel',
+    payload: {
+      totalRuns,
+      autoRunSkipFailures: inputAutoSkipFailures.checked,
+    },
+  });
 });
 
 btnAutoContinue.addEventListener('click', async () => {
   const email = inputEmail.value.trim();
   if (!email) {
-    showToast('Please fetch or paste DuckDuckGo email first!', 'warn');
+    showToast('请先获取或粘贴 DuckDuckGo 邮箱。', 'warn');
     return;
   }
   autoContinueBar.style.display = 'none';
@@ -346,21 +366,21 @@ btnAutoContinue.addEventListener('click', async () => {
 
 // Reset
 btnReset.addEventListener('click', async () => {
-  if (confirm('Reset all steps and data?')) {
+  if (confirm('确认重置全部步骤和数据吗?')) {
     await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
-    displayOauthUrl.textContent = 'Waiting...';
+    displayOauthUrl.textContent = '等待中...';
     displayOauthUrl.classList.remove('has-value');
-    displayLocalhostUrl.textContent = 'Waiting...';
+    displayLocalhostUrl.textContent = '等待中...';
     displayLocalhostUrl.classList.remove('has-value');
     inputEmail.value = '';
-    displayStatus.textContent = 'Ready';
+    displayStatus.textContent = '就绪';
     statusBar.className = 'status-bar';
     logArea.innerHTML = '';
     document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
     document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
     btnAutoRun.disabled = false;
     inputRunCount.disabled = false;
-    btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
+    btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> 自动';
     autoContinueBar.style.display = 'none';
     updateStopButtonState(false);
     updateButtonStates();
@@ -388,6 +408,14 @@ inputVpsUrl.addEventListener('change', async () => {
   }
 });
 
+inputVpsPassword.addEventListener('change', async () => {
+  await chrome.runtime.sendMessage({
+    type: 'SAVE_SETTING',
+    source: 'sidepanel',
+    payload: { vpsPassword: inputVpsPassword.value },
+  });
+});
+
 inputPassword.addEventListener('change', async () => {
   await chrome.runtime.sendMessage({
     type: 'SAVE_SETTING',
@@ -420,6 +448,14 @@ inputInbucketHost.addEventListener('change', async () => {
   });
 });
 
+inputAutoSkipFailures.addEventListener('change', async () => {
+  await chrome.runtime.sendMessage({
+    type: 'SAVE_SETTING',
+    source: 'sidepanel',
+    payload: { autoRunSkipFailures: inputAutoSkipFailures.checked },
+  });
+});
+
 // ============================================================
 // Listen for Background broadcasts
 // ============================================================
@@ -455,12 +491,12 @@ chrome.runtime.onMessage.addListener((message) => {
 
     case 'AUTO_RUN_RESET': {
       // Full UI reset for next run
-      displayOauthUrl.textContent = 'Waiting...';
+      displayOauthUrl.textContent = '等待中...';
       displayOauthUrl.classList.remove('has-value');
-      displayLocalhostUrl.textContent = 'Waiting...';
+      displayLocalhostUrl.textContent = '等待中...';
       displayLocalhostUrl.classList.remove('has-value');
       inputEmail.value = '';
-      displayStatus.textContent = 'Ready';
+      displayStatus.textContent = '就绪';
       statusBar.className = 'status-bar';
       logArea.innerHTML = '';
       document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
@@ -489,29 +525,34 @@ chrome.runtime.onMessage.addListener((message) => {
     }
 
     case 'AUTO_RUN_STATUS': {
-      const { phase, currentRun, totalRuns } = message.payload;
-      const runLabel = totalRuns > 1 ? ` (${currentRun}/${totalRuns})` : '';
+      const { phase, currentRun, totalRuns, attemptRun } = message.payload;
+      const attemptLabel = attemptRun ? ` · 尝试${attemptRun}` : '';
+      const runLabel = totalRuns > 1 ? ` (${currentRun}/${totalRuns}${attemptLabel})` : (attemptLabel ? ` (${attemptLabel.slice(3)})` : '');
       switch (phase) {
         case 'waiting_email':
           autoContinueBar.style.display = 'flex';
-          btnAutoRun.innerHTML = `Paused${runLabel}`;
+          btnAutoRun.innerHTML = `已暂停${runLabel}`;
           updateStopButtonState(true);
           break;
         case 'running':
-          btnAutoRun.innerHTML = `Running${runLabel}`;
+          btnAutoRun.innerHTML = `运行中${runLabel}`;
+          updateStopButtonState(true);
+          break;
+        case 'retrying':
+          btnAutoRun.innerHTML = `重试中${runLabel}`;
           updateStopButtonState(true);
           break;
         case 'complete':
           btnAutoRun.disabled = false;
           inputRunCount.disabled = false;
-          btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
+          btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> 自动';
           autoContinueBar.style.display = 'none';
           updateStopButtonState(false);
           break;
         case 'stopped':
           btnAutoRun.disabled = false;
           inputRunCount.disabled = false;
-          btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
+          btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> 自动';
           autoContinueBar.style.display = 'none';
           updateStopButtonState(false);
           break;