sidepanel.js 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678
  1. // sidepanel/sidepanel.js — Side Panel logic
  2. const STATUS_ICONS = {
  3. pending: '',
  4. running: '',
  5. completed: '\u2713', // ✓
  6. failed: '\u2717', // ✗
  7. stopped: '\u25A0', // ■
  8. manual_completed: '跳',
  9. skipped: '跳',
  10. };
  11. const logArea = document.getElementById('log-area');
  12. const displayOauthUrl = document.getElementById('display-oauth-url');
  13. const displayLocalhostUrl = document.getElementById('display-localhost-url');
  14. const displayStatus = document.getElementById('display-status');
  15. const statusBar = document.getElementById('status-bar');
  16. const inputEmail = document.getElementById('input-email');
  17. const inputPassword = document.getElementById('input-password');
  18. const btnToggleVpsUrl = document.getElementById('btn-toggle-vps-url');
  19. const btnFetchEmail = document.getElementById('btn-fetch-email');
  20. const btnTogglePassword = document.getElementById('btn-toggle-password');
  21. const btnSaveSettings = document.getElementById('btn-save-settings');
  22. const btnStop = document.getElementById('btn-stop');
  23. const btnReset = document.getElementById('btn-reset');
  24. const stepsProgress = document.getElementById('steps-progress');
  25. const btnAutoRun = document.getElementById('btn-auto-run');
  26. const btnAutoContinue = document.getElementById('btn-auto-continue');
  27. const autoContinueBar = document.getElementById('auto-continue-bar');
  28. const btnClearLog = document.getElementById('btn-clear-log');
  29. const inputVpsUrl = document.getElementById('input-vps-url');
  30. const inputVpsPassword = document.getElementById('input-vps-password');
  31. const selectMailProvider = document.getElementById('select-mail-provider');
  32. const hotmailSection = document.getElementById('hotmail-section');
  33. const inputHotmailEmail = document.getElementById('input-hotmail-email');
  34. const inputHotmailClientId = document.getElementById('input-hotmail-client-id');
  35. const inputHotmailPassword = document.getElementById('input-hotmail-password');
  36. const inputHotmailRefreshToken = document.getElementById('input-hotmail-refresh-token');
  37. const inputHotmailImport = document.getElementById('input-hotmail-import');
  38. const btnAddHotmailAccount = document.getElementById('btn-add-hotmail-account');
  39. const btnImportHotmailAccounts = document.getElementById('btn-import-hotmail-accounts');
  40. const btnClearUsedHotmailAccounts = document.getElementById('btn-clear-used-hotmail-accounts');
  41. const btnDeleteAllHotmailAccounts = document.getElementById('btn-delete-all-hotmail-accounts');
  42. const btnToggleHotmailList = document.getElementById('btn-toggle-hotmail-list');
  43. const hotmailListShell = document.getElementById('hotmail-list-shell');
  44. const hotmailAccountsList = document.getElementById('hotmail-accounts-list');
  45. const rowInbucketHost = document.getElementById('row-inbucket-host');
  46. const inputInbucketHost = document.getElementById('input-inbucket-host');
  47. const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
  48. const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
  49. const inputRunCount = document.getElementById('input-run-count');
  50. const inputAutoSkipFailures = document.getElementById('input-auto-skip-failures');
  51. const autoStartModal = document.getElementById('auto-start-modal');
  52. const autoStartTitle = autoStartModal?.querySelector('.modal-title');
  53. const autoStartMessage = document.getElementById('auto-start-message');
  54. const btnAutoStartClose = document.getElementById('btn-auto-start-close');
  55. const btnAutoStartCancel = document.getElementById('btn-auto-start-cancel');
  56. const btnAutoStartRestart = document.getElementById('btn-auto-start-restart');
  57. const btnAutoStartContinue = document.getElementById('btn-auto-start-continue');
  58. const autoHintText = document.querySelector('.auto-hint');
  59. const STEP_DEFAULT_STATUSES = {
  60. 1: 'pending',
  61. 2: 'pending',
  62. 3: 'pending',
  63. 4: 'pending',
  64. 5: 'pending',
  65. 6: 'pending',
  66. 7: 'pending',
  67. 8: 'pending',
  68. 9: 'pending',
  69. };
  70. const SKIPPABLE_STEPS = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]);
  71. let latestState = null;
  72. let currentAutoRun = {
  73. autoRunning: false,
  74. phase: 'idle',
  75. currentRun: 0,
  76. totalRuns: 1,
  77. attemptRun: 0,
  78. };
  79. let settingsDirty = false;
  80. let settingsSaveInFlight = false;
  81. let settingsAutoSaveTimer = null;
  82. let modalChoiceResolver = null;
  83. let currentModalActions = [];
  84. let hotmailActionInFlight = false;
  85. let hotmailListExpanded = false;
  86. const EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
  87. const EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
  88. const COPY_ICON = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
  89. const parseHotmailImportText = window.HotmailUtils?.parseHotmailImportText;
  90. const shouldClearHotmailCurrentSelection = window.HotmailUtils?.shouldClearHotmailCurrentSelection;
  91. const upsertHotmailAccountInList = window.HotmailUtils?.upsertHotmailAccountInList;
  92. const filterHotmailAccountsByUsage = window.HotmailUtils?.filterHotmailAccountsByUsage;
  93. const getHotmailBulkActionLabel = window.HotmailUtils?.getHotmailBulkActionLabel;
  94. const getHotmailListToggleLabel = window.HotmailUtils?.getHotmailListToggleLabel;
  95. const HOTMAIL_LIST_EXPANDED_STORAGE_KEY = 'multipage-hotmail-list-expanded';
  96. // ============================================================
  97. // Toast Notifications
  98. // ============================================================
  99. const toastContainer = document.getElementById('toast-container');
  100. const TOAST_ICONS = {
  101. error: '<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="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
  102. warn: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
  103. success: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
  104. 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>',
  105. };
  106. const LOG_LEVEL_LABELS = {
  107. info: '信息',
  108. ok: '成功',
  109. warn: '警告',
  110. error: '错误',
  111. };
  112. function showToast(message, type = 'error', duration = 4000) {
  113. const toast = document.createElement('div');
  114. toast.className = `toast toast-${type}`;
  115. toast.innerHTML = `${TOAST_ICONS[type] || ''}<span class="toast-msg">${escapeHtml(message)}</span><button class="toast-close">&times;</button>`;
  116. toast.querySelector('.toast-close').addEventListener('click', () => dismissToast(toast));
  117. toastContainer.appendChild(toast);
  118. if (duration > 0) {
  119. setTimeout(() => dismissToast(toast), duration);
  120. }
  121. }
  122. function dismissToast(toast) {
  123. if (!toast.parentNode) return;
  124. toast.classList.add('toast-exit');
  125. toast.addEventListener('animationend', () => toast.remove());
  126. }
  127. function resetActionModalButtons() {
  128. const buttons = [btnAutoStartCancel, btnAutoStartRestart, btnAutoStartContinue];
  129. buttons.forEach((button) => {
  130. if (!button) return;
  131. button.hidden = true;
  132. button.disabled = false;
  133. button.onclick = null;
  134. });
  135. currentModalActions = [];
  136. }
  137. function configureActionModalButton(button, action) {
  138. if (!button) return;
  139. if (!action) {
  140. button.hidden = true;
  141. button.onclick = null;
  142. return;
  143. }
  144. button.hidden = false;
  145. button.disabled = false;
  146. button.textContent = action.label;
  147. button.className = `btn ${action.variant || 'btn-outline'} btn-sm`;
  148. button.onclick = () => resolveModalChoice(action.id);
  149. }
  150. function resolveModalChoice(choice) {
  151. if (modalChoiceResolver) {
  152. modalChoiceResolver(choice);
  153. modalChoiceResolver = null;
  154. }
  155. resetActionModalButtons();
  156. if (autoStartModal) {
  157. autoStartModal.hidden = true;
  158. }
  159. }
  160. function openActionModal({ title, message, actions }) {
  161. if (!autoStartModal) {
  162. return Promise.resolve(null);
  163. }
  164. if (modalChoiceResolver) {
  165. resolveModalChoice(null);
  166. }
  167. autoStartTitle.textContent = title;
  168. autoStartMessage.textContent = message;
  169. currentModalActions = actions || [];
  170. configureActionModalButton(btnAutoStartCancel, currentModalActions[0]);
  171. configureActionModalButton(btnAutoStartRestart, currentModalActions[1]);
  172. configureActionModalButton(btnAutoStartContinue, currentModalActions[2]);
  173. autoStartModal.hidden = false;
  174. return new Promise((resolve) => {
  175. modalChoiceResolver = resolve;
  176. });
  177. }
  178. function openAutoStartChoiceDialog(startStep) {
  179. return openActionModal({
  180. title: '启动自动',
  181. message: `检测到当前已有流程进度。继续当前会从步骤 ${startStep} 开始自动执行,重新开始会清空当前流程进度并从步骤 1 新开一轮。`,
  182. actions: [
  183. { id: null, label: '取消', variant: 'btn-ghost' },
  184. { id: 'restart', label: '重新开始', variant: 'btn-outline' },
  185. { id: 'continue', label: '继续当前', variant: 'btn-primary' },
  186. ],
  187. });
  188. }
  189. async function openConfirmModal({ title, message, confirmLabel = '确认', confirmVariant = 'btn-primary' }) {
  190. const choice = await openActionModal({
  191. title,
  192. message,
  193. actions: [
  194. { id: null, label: '取消', variant: 'btn-ghost' },
  195. { id: 'confirm', label: confirmLabel, variant: confirmVariant },
  196. ],
  197. });
  198. return choice === 'confirm';
  199. }
  200. function isDoneStatus(status) {
  201. return status === 'completed' || status === 'manual_completed' || status === 'skipped';
  202. }
  203. function getStepStatuses(state = latestState) {
  204. return { ...STEP_DEFAULT_STATUSES, ...(state?.stepStatuses || {}) };
  205. }
  206. function getFirstUnfinishedStep(state = latestState) {
  207. const statuses = getStepStatuses(state);
  208. for (let step = 1; step <= 9; step++) {
  209. if (!isDoneStatus(statuses[step])) {
  210. return step;
  211. }
  212. }
  213. return null;
  214. }
  215. function hasSavedProgress(state = latestState) {
  216. const statuses = getStepStatuses(state);
  217. return Object.values(statuses).some((status) => status !== 'pending');
  218. }
  219. function shouldOfferAutoModeChoice(state = latestState) {
  220. return hasSavedProgress(state) && getFirstUnfinishedStep(state) !== null;
  221. }
  222. function syncLatestState(nextState) {
  223. const mergedStepStatuses = nextState?.stepStatuses
  224. ? { ...STEP_DEFAULT_STATUSES, ...(latestState?.stepStatuses || {}), ...nextState.stepStatuses }
  225. : getStepStatuses(latestState);
  226. latestState = {
  227. ...(latestState || {}),
  228. ...(nextState || {}),
  229. stepStatuses: mergedStepStatuses,
  230. };
  231. }
  232. function syncAutoRunState(source = {}) {
  233. const phase = source.autoRunPhase ?? source.phase ?? currentAutoRun.phase;
  234. const autoRunning = source.autoRunning !== undefined
  235. ? Boolean(source.autoRunning)
  236. : (source.autoRunPhase !== undefined || source.phase !== undefined
  237. ? ['running', 'waiting_email', 'retrying'].includes(phase)
  238. : currentAutoRun.autoRunning);
  239. currentAutoRun = {
  240. autoRunning,
  241. phase,
  242. currentRun: source.autoRunCurrentRun ?? source.currentRun ?? currentAutoRun.currentRun,
  243. totalRuns: source.autoRunTotalRuns ?? source.totalRuns ?? currentAutoRun.totalRuns,
  244. attemptRun: source.autoRunAttemptRun ?? source.attemptRun ?? currentAutoRun.attemptRun,
  245. };
  246. }
  247. function isAutoRunLockedPhase() {
  248. return currentAutoRun.phase === 'running' || currentAutoRun.phase === 'retrying';
  249. }
  250. function isAutoRunPausedPhase() {
  251. return currentAutoRun.phase === 'waiting_email';
  252. }
  253. function getAutoRunLabel(payload = currentAutoRun) {
  254. const attemptLabel = payload.attemptRun ? ` · 尝试${payload.attemptRun}` : '';
  255. if ((payload.totalRuns || 1) > 1) {
  256. return ` (${payload.currentRun}/${payload.totalRuns}${attemptLabel})`;
  257. }
  258. return attemptLabel ? ` (${attemptLabel.slice(3)})` : '';
  259. }
  260. function setDefaultAutoRunButton() {
  261. btnAutoRun.disabled = false;
  262. inputRunCount.disabled = false;
  263. 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> 自动';
  264. }
  265. function collectSettingsPayload() {
  266. return {
  267. vpsUrl: inputVpsUrl.value.trim(),
  268. vpsPassword: inputVpsPassword.value,
  269. customPassword: inputPassword.value,
  270. mailProvider: selectMailProvider.value,
  271. inbucketHost: inputInbucketHost.value.trim(),
  272. inbucketMailbox: inputInbucketMailbox.value.trim(),
  273. autoRunSkipFailures: inputAutoSkipFailures.checked,
  274. };
  275. }
  276. function markSettingsDirty(isDirty = true) {
  277. settingsDirty = isDirty;
  278. updateSaveButtonState();
  279. }
  280. function updateSaveButtonState() {
  281. btnSaveSettings.disabled = settingsSaveInFlight || !settingsDirty;
  282. btnSaveSettings.textContent = settingsSaveInFlight ? '保存中' : '保存';
  283. }
  284. function scheduleSettingsAutoSave() {
  285. clearTimeout(settingsAutoSaveTimer);
  286. settingsAutoSaveTimer = setTimeout(() => {
  287. saveSettings({ silent: true }).catch(() => {});
  288. }, 500);
  289. }
  290. async function saveSettings(options = {}) {
  291. const { silent = false } = options;
  292. clearTimeout(settingsAutoSaveTimer);
  293. if (!settingsDirty && !settingsSaveInFlight && silent) {
  294. return;
  295. }
  296. const payload = collectSettingsPayload();
  297. settingsSaveInFlight = true;
  298. updateSaveButtonState();
  299. try {
  300. const response = await chrome.runtime.sendMessage({
  301. type: 'SAVE_SETTING',
  302. source: 'sidepanel',
  303. payload,
  304. });
  305. if (response?.error) {
  306. throw new Error(response.error);
  307. }
  308. syncLatestState(payload);
  309. markSettingsDirty(false);
  310. updateMailProviderUI();
  311. updateButtonStates();
  312. if (!silent) {
  313. showToast('配置已保存', 'success', 1800);
  314. }
  315. } catch (err) {
  316. markSettingsDirty(true);
  317. if (!silent) {
  318. showToast(`保存失败:${err.message}`, 'error');
  319. }
  320. throw err;
  321. } finally {
  322. settingsSaveInFlight = false;
  323. updateSaveButtonState();
  324. }
  325. }
  326. function applyAutoRunStatus(payload = currentAutoRun) {
  327. syncAutoRunState(payload);
  328. const runLabel = getAutoRunLabel(currentAutoRun);
  329. const locked = isAutoRunLockedPhase();
  330. const paused = isAutoRunPausedPhase();
  331. inputRunCount.disabled = currentAutoRun.autoRunning;
  332. btnAutoRun.disabled = currentAutoRun.autoRunning;
  333. btnFetchEmail.disabled = locked;
  334. inputEmail.disabled = locked;
  335. switch (currentAutoRun.phase) {
  336. case 'waiting_email':
  337. autoContinueBar.style.display = 'flex';
  338. btnAutoRun.innerHTML = `已暂停${runLabel}`;
  339. break;
  340. case 'running':
  341. autoContinueBar.style.display = 'none';
  342. btnAutoRun.innerHTML = `运行中${runLabel}`;
  343. break;
  344. case 'retrying':
  345. autoContinueBar.style.display = 'none';
  346. btnAutoRun.innerHTML = `重试中${runLabel}`;
  347. break;
  348. default:
  349. autoContinueBar.style.display = 'none';
  350. setDefaultAutoRunButton();
  351. inputEmail.disabled = false;
  352. if (!locked) {
  353. btnFetchEmail.disabled = false;
  354. }
  355. break;
  356. }
  357. updateStopButtonState(paused || locked || Object.values(getStepStatuses()).some(status => status === 'running'));
  358. }
  359. function initializeManualStepActions() {
  360. document.querySelectorAll('.step-row').forEach((row) => {
  361. const step = Number(row.dataset.step);
  362. const statusEl = row.querySelector('.step-status');
  363. if (!statusEl) return;
  364. const actions = document.createElement('div');
  365. actions.className = 'step-actions';
  366. const manualBtn = document.createElement('button');
  367. manualBtn.type = 'button';
  368. manualBtn.className = 'step-manual-btn';
  369. manualBtn.dataset.step = String(step);
  370. manualBtn.title = '跳过此步';
  371. manualBtn.setAttribute('aria-label', `跳过步骤 ${step}`);
  372. manualBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>';
  373. manualBtn.addEventListener('click', async (event) => {
  374. event.stopPropagation();
  375. try {
  376. await handleSkipStep(step);
  377. } catch (err) {
  378. showToast(err.message, 'error');
  379. }
  380. });
  381. statusEl.parentNode.replaceChild(actions, statusEl);
  382. actions.appendChild(manualBtn);
  383. actions.appendChild(statusEl);
  384. });
  385. }
  386. // ============================================================
  387. // State Restore on load
  388. // ============================================================
  389. async function restoreState() {
  390. try {
  391. const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
  392. syncLatestState(state);
  393. syncAutoRunState(state);
  394. if (state.oauthUrl) {
  395. displayOauthUrl.textContent = state.oauthUrl;
  396. displayOauthUrl.classList.add('has-value');
  397. }
  398. if (state.localhostUrl) {
  399. displayLocalhostUrl.textContent = state.localhostUrl;
  400. displayLocalhostUrl.classList.add('has-value');
  401. }
  402. if (state.email) {
  403. inputEmail.value = state.email;
  404. }
  405. syncPasswordField(state);
  406. if (state.vpsUrl) {
  407. inputVpsUrl.value = state.vpsUrl;
  408. }
  409. if (state.vpsPassword) {
  410. inputVpsPassword.value = state.vpsPassword;
  411. }
  412. if (state.mailProvider) {
  413. selectMailProvider.value = state.mailProvider;
  414. }
  415. if (state.inbucketHost) {
  416. inputInbucketHost.value = state.inbucketHost;
  417. }
  418. if (state.inbucketMailbox) {
  419. inputInbucketMailbox.value = state.inbucketMailbox;
  420. }
  421. inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures);
  422. if (state.stepStatuses) {
  423. for (const [step, status] of Object.entries(state.stepStatuses)) {
  424. updateStepUI(Number(step), status);
  425. }
  426. }
  427. if (state.logs) {
  428. for (const entry of state.logs) {
  429. appendLog(entry);
  430. }
  431. }
  432. applyAutoRunStatus(state);
  433. markSettingsDirty(false);
  434. updateStatusDisplay(latestState);
  435. updateProgressCounter();
  436. updateMailProviderUI();
  437. updateButtonStates();
  438. } catch (err) {
  439. console.error('Failed to restore state:', err);
  440. }
  441. }
  442. function syncPasswordField(state) {
  443. inputPassword.value = state.customPassword || state.password || '';
  444. }
  445. function getHotmailAccounts(state = latestState) {
  446. return Array.isArray(state?.hotmailAccounts) ? state.hotmailAccounts : [];
  447. }
  448. function getCurrentHotmailAccount(state = latestState) {
  449. const currentId = state?.currentHotmailAccountId;
  450. return getHotmailAccounts(state).find((account) => account.id === currentId) || null;
  451. }
  452. function getHotmailAccountsByUsage(mode = 'all', state = latestState) {
  453. const accounts = getHotmailAccounts(state);
  454. if (typeof filterHotmailAccountsByUsage === 'function') {
  455. return filterHotmailAccountsByUsage(accounts, mode);
  456. }
  457. if (mode === 'used') {
  458. return accounts.filter((account) => Boolean(account?.used));
  459. }
  460. return accounts.slice();
  461. }
  462. function getHotmailBulkActionText(mode, count) {
  463. if (typeof getHotmailBulkActionLabel === 'function') {
  464. return getHotmailBulkActionLabel(mode, count);
  465. }
  466. const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
  467. const prefix = mode === 'used' ? '清空已用' : '全部删除';
  468. const suffix = normalizedCount > 0 ? `(${normalizedCount})` : '';
  469. return `${prefix}${suffix}`;
  470. }
  471. function getHotmailListToggleText(expanded, count) {
  472. if (typeof getHotmailListToggleLabel === 'function') {
  473. return getHotmailListToggleLabel(expanded, count);
  474. }
  475. const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0;
  476. const suffix = normalizedCount > 0 ? `(${normalizedCount})` : '';
  477. return `${expanded ? '收起列表' : '展开列表'}${suffix}`;
  478. }
  479. function updateHotmailListViewport() {
  480. const count = getHotmailAccounts().length;
  481. const usedCount = getHotmailAccountsByUsage('used').length;
  482. if (btnClearUsedHotmailAccounts) {
  483. btnClearUsedHotmailAccounts.textContent = getHotmailBulkActionText('used', usedCount);
  484. btnClearUsedHotmailAccounts.disabled = usedCount === 0;
  485. }
  486. if (btnDeleteAllHotmailAccounts) {
  487. btnDeleteAllHotmailAccounts.textContent = getHotmailBulkActionText('all', count);
  488. btnDeleteAllHotmailAccounts.disabled = count === 0;
  489. }
  490. if (btnToggleHotmailList) {
  491. btnToggleHotmailList.textContent = getHotmailListToggleText(hotmailListExpanded, count);
  492. btnToggleHotmailList.setAttribute('aria-expanded', String(hotmailListExpanded));
  493. btnToggleHotmailList.disabled = count === 0;
  494. }
  495. if (hotmailListShell) {
  496. hotmailListShell.classList.toggle('is-expanded', hotmailListExpanded);
  497. hotmailListShell.classList.toggle('is-collapsed', !hotmailListExpanded);
  498. }
  499. }
  500. function setHotmailListExpanded(expanded, options = {}) {
  501. const { persist = true } = options;
  502. hotmailListExpanded = Boolean(expanded);
  503. updateHotmailListViewport();
  504. if (persist) {
  505. localStorage.setItem(HOTMAIL_LIST_EXPANDED_STORAGE_KEY, hotmailListExpanded ? '1' : '0');
  506. }
  507. }
  508. function initHotmailListExpandedState() {
  509. const saved = localStorage.getItem(HOTMAIL_LIST_EXPANDED_STORAGE_KEY);
  510. setHotmailListExpanded(saved === '1', { persist: false });
  511. }
  512. function shouldClearCurrentHotmailSelectionLocally(account) {
  513. if (typeof shouldClearHotmailCurrentSelection === 'function') {
  514. return shouldClearHotmailCurrentSelection(account);
  515. }
  516. return Boolean(account) && account.used === true;
  517. }
  518. function upsertHotmailAccountListLocally(accounts, nextAccount) {
  519. if (typeof upsertHotmailAccountInList === 'function') {
  520. return upsertHotmailAccountInList(accounts, nextAccount);
  521. }
  522. const list = Array.isArray(accounts) ? accounts.slice() : [];
  523. if (!nextAccount?.id) return list;
  524. const existingIndex = list.findIndex((account) => account?.id === nextAccount.id);
  525. if (existingIndex === -1) {
  526. list.push(nextAccount);
  527. return list;
  528. }
  529. list[existingIndex] = nextAccount;
  530. return list;
  531. }
  532. function refreshHotmailSelectionUI() {
  533. renderHotmailAccounts();
  534. if (selectMailProvider.value === 'hotmail-api') {
  535. const currentAccount = getCurrentHotmailAccount();
  536. inputEmail.value = currentAccount?.email || latestState?.email || '';
  537. }
  538. }
  539. function applyHotmailAccountMutation(account, options = {}) {
  540. if (!account?.id) return;
  541. const { preserveCurrentSelection = false } = options;
  542. const nextState = {
  543. hotmailAccounts: upsertHotmailAccountListLocally(getHotmailAccounts(), account),
  544. };
  545. if (!preserveCurrentSelection
  546. && latestState?.currentHotmailAccountId === account.id
  547. && shouldClearCurrentHotmailSelectionLocally(account)) {
  548. nextState.currentHotmailAccountId = null;
  549. if (selectMailProvider.value === 'hotmail-api') {
  550. nextState.email = null;
  551. }
  552. }
  553. syncLatestState(nextState);
  554. refreshHotmailSelectionUI();
  555. }
  556. function formatDateTime(timestamp) {
  557. const value = Number(timestamp);
  558. if (!Number.isFinite(value) || value <= 0) {
  559. return '未使用';
  560. }
  561. return new Date(value).toLocaleString('zh-CN', { hour12: false });
  562. }
  563. function getHotmailAvailabilityLabel(account) {
  564. if (account.used) return '已用';
  565. return '可分配';
  566. }
  567. function getHotmailStatusLabel(account) {
  568. if (account.used) return '已用';
  569. switch (account.status) {
  570. case 'authorized':
  571. return '可用';
  572. case 'error':
  573. return '异常';
  574. default:
  575. return '待校验';
  576. }
  577. }
  578. function getHotmailStatusClass(account) {
  579. if (account.used) return 'status-used';
  580. return `status-${account.status || 'pending'}`;
  581. }
  582. function clearHotmailForm() {
  583. inputHotmailEmail.value = '';
  584. inputHotmailClientId.value = '';
  585. inputHotmailPassword.value = '';
  586. inputHotmailRefreshToken.value = '';
  587. }
  588. function renderHotmailAccounts() {
  589. if (!hotmailAccountsList) return;
  590. const accounts = getHotmailAccounts();
  591. const currentId = latestState?.currentHotmailAccountId || '';
  592. if (!accounts.length) {
  593. hotmailAccountsList.innerHTML = '<div class="hotmail-empty">还没有 Hotmail 账号,先添加一条再校验。</div>';
  594. updateHotmailListViewport();
  595. return;
  596. }
  597. hotmailAccountsList.innerHTML = accounts.map((account) => `
  598. <div class="hotmail-account-item${account.id === currentId ? ' is-current' : ''}">
  599. <div class="hotmail-account-top">
  600. <div class="hotmail-account-title-row">
  601. <div class="hotmail-account-email">${escapeHtml(account.email || '(未命名账号)')}</div>
  602. <button
  603. class="hotmail-copy-btn"
  604. type="button"
  605. data-account-action="copy-email"
  606. data-account-id="${escapeHtml(account.id)}"
  607. title="复制邮箱"
  608. aria-label="复制邮箱 ${escapeHtml(account.email || '')}"
  609. >${COPY_ICON}</button>
  610. </div>
  611. <span class="hotmail-status-chip ${escapeHtml(getHotmailStatusClass(account))}">${escapeHtml(getHotmailStatusLabel(account))}</span>
  612. </div>
  613. <div class="hotmail-account-meta">
  614. <span>客户端 ID:${escapeHtml(account.clientId ? `${account.clientId.slice(0, 10)}...` : '未填写')}</span>
  615. <span>刷新令牌:${account.refreshToken ? '已保存' : '未保存'}</span>
  616. <span>分配状态: ${escapeHtml(getHotmailAvailabilityLabel(account))}</span>
  617. <span>上次校验: ${escapeHtml(formatDateTime(account.lastAuthAt))}</span>
  618. <span>上次使用: ${escapeHtml(formatDateTime(account.lastUsedAt))}</span>
  619. </div>
  620. ${account.lastError ? `<div class="hotmail-account-error">${escapeHtml(account.lastError)}</div>` : ''}
  621. <div class="hotmail-account-actions">
  622. <button class="btn btn-outline btn-sm" type="button" data-account-action="select" data-account-id="${escapeHtml(account.id)}">使用此账号</button>
  623. <button class="btn btn-outline btn-sm" type="button" data-account-action="toggle-used" data-account-id="${escapeHtml(account.id)}">${account.used ? '标记未用' : '标记已用'}</button>
  624. <button class="btn btn-primary btn-sm" type="button" data-account-action="verify" data-account-id="${escapeHtml(account.id)}">校验</button>
  625. <button class="btn btn-outline btn-sm" type="button" data-account-action="test" data-account-id="${escapeHtml(account.id)}">复制最新验证码</button>
  626. <button class="btn btn-ghost btn-sm" type="button" data-account-action="delete" data-account-id="${escapeHtml(account.id)}">删除</button>
  627. </div>
  628. </div>
  629. `).join('');
  630. updateHotmailListViewport();
  631. }
  632. function updateMailProviderUI() {
  633. const useInbucket = selectMailProvider.value === 'inbucket';
  634. const useHotmail = selectMailProvider.value === 'hotmail-api';
  635. rowInbucketHost.style.display = useInbucket ? '' : 'none';
  636. rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
  637. if (hotmailSection) {
  638. hotmailSection.style.display = useHotmail ? '' : 'none';
  639. }
  640. btnFetchEmail.hidden = useHotmail;
  641. inputEmail.readOnly = useHotmail;
  642. inputEmail.placeholder = useHotmail ? '由 Hotmail 账号池自动分配' : '粘贴 DuckDuckGo 邮箱';
  643. if (autoHintText) {
  644. autoHintText.textContent = useHotmail
  645. ? '请先校验并选择一个 Hotmail 账号'
  646. : '先自动获取 Duck 邮箱,或手动粘贴邮箱后再继续';
  647. }
  648. if (useHotmail) {
  649. const currentAccount = getCurrentHotmailAccount();
  650. inputEmail.value = currentAccount?.email || latestState?.email || '';
  651. }
  652. renderHotmailAccounts();
  653. }
  654. // ============================================================
  655. // UI Updates
  656. // ============================================================
  657. function updateStepUI(step, status) {
  658. const statusEl = document.querySelector(`.step-status[data-step="${step}"]`);
  659. const row = document.querySelector(`.step-row[data-step="${step}"]`);
  660. syncLatestState({
  661. stepStatuses: {
  662. ...getStepStatuses(),
  663. [step]: status,
  664. },
  665. });
  666. if (statusEl) statusEl.textContent = STATUS_ICONS[status] || '';
  667. if (row) {
  668. row.className = `step-row ${status}`;
  669. }
  670. updateButtonStates();
  671. updateProgressCounter();
  672. }
  673. function updateProgressCounter() {
  674. const completed = Object.values(getStepStatuses()).filter(isDoneStatus).length;
  675. stepsProgress.textContent = `${completed} / 9`;
  676. }
  677. function updateButtonStates() {
  678. const statuses = getStepStatuses();
  679. const anyRunning = Object.values(statuses).some(s => s === 'running');
  680. const autoLocked = isAutoRunLockedPhase();
  681. for (let step = 1; step <= 9; step++) {
  682. const btn = document.querySelector(`.step-btn[data-step="${step}"]`);
  683. if (!btn) continue;
  684. if (anyRunning || autoLocked) {
  685. btn.disabled = true;
  686. } else if (step === 1) {
  687. btn.disabled = false;
  688. } else {
  689. const prevStatus = statuses[step - 1];
  690. const currentStatus = statuses[step];
  691. btn.disabled = !(isDoneStatus(prevStatus) || currentStatus === 'failed' || isDoneStatus(currentStatus) || currentStatus === 'stopped');
  692. }
  693. }
  694. document.querySelectorAll('.step-manual-btn').forEach((btn) => {
  695. const step = Number(btn.dataset.step);
  696. const currentStatus = statuses[step];
  697. const prevStatus = statuses[step - 1];
  698. if (!SKIPPABLE_STEPS.has(step) || anyRunning || autoLocked || currentStatus === 'running' || isDoneStatus(currentStatus)) {
  699. btn.style.display = 'none';
  700. btn.disabled = true;
  701. btn.title = '当前不可跳过';
  702. return;
  703. }
  704. if (step > 1 && !isDoneStatus(prevStatus)) {
  705. btn.style.display = 'none';
  706. btn.disabled = true;
  707. btn.title = `请先完成步骤 ${step - 1}`;
  708. return;
  709. }
  710. btn.style.display = '';
  711. btn.disabled = false;
  712. btn.title = `跳过步骤 ${step}`;
  713. });
  714. updateStopButtonState(anyRunning || isAutoRunPausedPhase() || autoLocked);
  715. }
  716. function updateStopButtonState(active) {
  717. btnStop.disabled = !active;
  718. }
  719. function updateStatusDisplay(state) {
  720. if (!state || !state.stepStatuses) return;
  721. statusBar.className = 'status-bar';
  722. if (isAutoRunPausedPhase()) {
  723. displayStatus.textContent = `自动已暂停${getAutoRunLabel()},等待邮箱后继续`;
  724. statusBar.classList.add('paused');
  725. return;
  726. }
  727. const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
  728. if (running) {
  729. displayStatus.textContent = `步骤 ${running[0]} 运行中...`;
  730. statusBar.classList.add('running');
  731. return;
  732. }
  733. if (isAutoRunLockedPhase()) {
  734. displayStatus.textContent = `${currentAutoRun.phase === 'retrying' ? '自动重试中' : '自动运行中'}${getAutoRunLabel()}`;
  735. statusBar.classList.add('running');
  736. return;
  737. }
  738. const failed = Object.entries(state.stepStatuses).find(([, s]) => s === 'failed');
  739. if (failed) {
  740. displayStatus.textContent = `步骤 ${failed[0]} 失败`;
  741. statusBar.classList.add('failed');
  742. return;
  743. }
  744. const stopped = Object.entries(state.stepStatuses).find(([, s]) => s === 'stopped');
  745. if (stopped) {
  746. displayStatus.textContent = `步骤 ${stopped[0]} 已停止`;
  747. statusBar.classList.add('stopped');
  748. return;
  749. }
  750. const lastCompleted = Object.entries(state.stepStatuses)
  751. .filter(([, s]) => isDoneStatus(s))
  752. .map(([k]) => Number(k))
  753. .sort((a, b) => b - a)[0];
  754. if (lastCompleted === 9) {
  755. displayStatus.textContent = (state.stepStatuses[9] === 'manual_completed' || state.stepStatuses[9] === 'skipped') ? '全部步骤已跳过/完成' : '全部步骤已完成';
  756. statusBar.classList.add('completed');
  757. } else if (lastCompleted) {
  758. displayStatus.textContent = (state.stepStatuses[lastCompleted] === 'manual_completed' || state.stepStatuses[lastCompleted] === 'skipped')
  759. ? `步骤 ${lastCompleted} 已跳过`
  760. : `步骤 ${lastCompleted} 已完成`;
  761. } else {
  762. displayStatus.textContent = '就绪';
  763. }
  764. }
  765. function appendLog(entry) {
  766. const time = new Date(entry.timestamp).toLocaleTimeString('zh-CN', { hour12: false });
  767. const levelLabel = LOG_LEVEL_LABELS[entry.level] || entry.level;
  768. const line = document.createElement('div');
  769. line.className = `log-line log-${entry.level}`;
  770. const stepMatch = entry.message.match(/(?:Step\s+(\d+)|步骤\s*(\d+))/);
  771. const stepNum = stepMatch ? (stepMatch[1] || stepMatch[2]) : null;
  772. let html = `<span class="log-time">${time}</span> `;
  773. html += `<span class="log-level log-level-${entry.level}">${levelLabel}</span> `;
  774. if (stepNum) {
  775. html += `<span class="log-step-tag step-${stepNum}">步${stepNum}</span>`;
  776. }
  777. html += `<span class="log-msg">${escapeHtml(entry.message)}</span>`;
  778. line.innerHTML = html;
  779. logArea.appendChild(line);
  780. logArea.scrollTop = logArea.scrollHeight;
  781. }
  782. function escapeHtml(text) {
  783. const div = document.createElement('div');
  784. div.textContent = text;
  785. return div.innerHTML;
  786. }
  787. async function fetchDuckEmail(options = {}) {
  788. const { showFailureToast = true } = options;
  789. const defaultLabel = '获取';
  790. btnFetchEmail.disabled = true;
  791. btnFetchEmail.textContent = '...';
  792. try {
  793. const response = await chrome.runtime.sendMessage({
  794. type: 'FETCH_DUCK_EMAIL',
  795. source: 'sidepanel',
  796. payload: { generateNew: true },
  797. });
  798. if (response?.error) {
  799. throw new Error(response.error);
  800. }
  801. if (!response?.email) {
  802. throw new Error('未返回 Duck 邮箱。');
  803. }
  804. inputEmail.value = response.email;
  805. showToast(`已获取 ${response.email}`, 'success', 2500);
  806. return response.email;
  807. } catch (err) {
  808. if (showFailureToast) {
  809. showToast(`自动获取失败:${err.message}`, 'error');
  810. }
  811. throw err;
  812. } finally {
  813. btnFetchEmail.disabled = false;
  814. btnFetchEmail.textContent = defaultLabel;
  815. }
  816. }
  817. function syncToggleButtonLabel(button, input, labels) {
  818. if (!button || !input) return;
  819. const isHidden = input.type === 'password';
  820. button.innerHTML = isHidden ? EYE_OPEN_ICON : EYE_CLOSED_ICON;
  821. button.setAttribute('aria-label', isHidden ? labels.show : labels.hide);
  822. button.title = isHidden ? labels.show : labels.hide;
  823. }
  824. async function copyTextToClipboard(text) {
  825. const value = String(text || '').trim();
  826. if (!value) {
  827. throw new Error('没有可复制的内容。');
  828. }
  829. if (!navigator.clipboard?.writeText) {
  830. throw new Error('当前环境不支持剪贴板复制。');
  831. }
  832. await navigator.clipboard.writeText(value);
  833. }
  834. async function deleteHotmailAccountsByMode(mode) {
  835. const isUsedMode = mode === 'used';
  836. const targetAccounts = getHotmailAccountsByUsage(isUsedMode ? 'used' : 'all');
  837. if (!targetAccounts.length) {
  838. showToast(isUsedMode ? '没有已用账号可清空。' : '没有可删除的 Hotmail 账号。', 'warn');
  839. return;
  840. }
  841. const confirmed = await openConfirmModal({
  842. title: isUsedMode ? '清空已用账号' : '全部删除账号',
  843. message: isUsedMode
  844. ? `确认删除当前 ${targetAccounts.length} 个已用 Hotmail 账号吗?`
  845. : `确认删除全部 ${targetAccounts.length} 个 Hotmail 账号吗?`,
  846. confirmLabel: isUsedMode ? '确认清空已用' : '确认全部删除',
  847. confirmVariant: isUsedMode ? 'btn-outline' : 'btn-danger',
  848. });
  849. if (!confirmed) {
  850. return;
  851. }
  852. const response = await chrome.runtime.sendMessage({
  853. type: 'DELETE_HOTMAIL_ACCOUNTS',
  854. source: 'sidepanel',
  855. payload: { mode: isUsedMode ? 'used' : 'all' },
  856. });
  857. if (response?.error) {
  858. throw new Error(response.error);
  859. }
  860. const targetIds = new Set(targetAccounts.map((account) => account.id));
  861. const nextAccounts = isUsedMode
  862. ? getHotmailAccounts().filter((account) => !targetIds.has(account.id))
  863. : [];
  864. const nextState = { hotmailAccounts: nextAccounts };
  865. if (latestState?.currentHotmailAccountId && targetIds.has(latestState.currentHotmailAccountId)) {
  866. nextState.currentHotmailAccountId = null;
  867. if (selectMailProvider.value === 'hotmail-api') {
  868. nextState.email = null;
  869. }
  870. }
  871. syncLatestState(nextState);
  872. refreshHotmailSelectionUI();
  873. showToast(
  874. isUsedMode
  875. ? `已清空 ${response.deletedCount || 0} 个已用 Hotmail 账号`
  876. : `已删除全部 ${response.deletedCount || 0} 个 Hotmail 账号`,
  877. 'success',
  878. 2200
  879. );
  880. }
  881. function syncPasswordToggleLabel() {
  882. syncToggleButtonLabel(btnTogglePassword, inputPassword, {
  883. show: '显示密码',
  884. hide: '隐藏密码',
  885. });
  886. }
  887. function syncVpsUrlToggleLabel() {
  888. syncToggleButtonLabel(btnToggleVpsUrl, inputVpsUrl, {
  889. show: '显示 CPA 地址',
  890. hide: '隐藏 CPA 地址',
  891. });
  892. }
  893. async function maybeTakeoverAutoRun(actionLabel) {
  894. if (!isAutoRunPausedPhase()) {
  895. return true;
  896. }
  897. const confirmed = await openConfirmModal({
  898. title: '接管自动',
  899. message: `当前自动流程已暂停。若继续${actionLabel},将停止自动流程并切换为手动控制。是否继续?`,
  900. confirmLabel: '确认接管',
  901. confirmVariant: 'btn-primary',
  902. });
  903. if (!confirmed) {
  904. return false;
  905. }
  906. await chrome.runtime.sendMessage({ type: 'TAKEOVER_AUTO_RUN', source: 'sidepanel', payload: {} });
  907. return true;
  908. }
  909. async function handleSkipStep(step) {
  910. if (!(await maybeTakeoverAutoRun(`跳过步骤 ${step}`))) {
  911. return;
  912. }
  913. const confirmed = await openConfirmModal({
  914. title: '跳过步骤',
  915. message: `这不会真正执行步骤 ${step},只会直接跳过该步骤并放行后续步骤。是否继续?`,
  916. confirmLabel: `跳过步骤 ${step}`,
  917. confirmVariant: 'btn-primary',
  918. });
  919. if (!confirmed) {
  920. return;
  921. }
  922. const response = await chrome.runtime.sendMessage({
  923. type: 'SKIP_STEP',
  924. source: 'sidepanel',
  925. payload: { step },
  926. });
  927. if (response?.error) {
  928. throw new Error(response.error);
  929. }
  930. showToast(`步骤 ${step} 已跳过`, 'success', 2200);
  931. }
  932. // ============================================================
  933. // Button Handlers
  934. // ============================================================
  935. document.querySelectorAll('.step-btn').forEach(btn => {
  936. btn.addEventListener('click', async () => {
  937. try {
  938. const step = Number(btn.dataset.step);
  939. if (!(await maybeTakeoverAutoRun(`执行步骤 ${step}`))) {
  940. return;
  941. }
  942. if (step === 3) {
  943. if (inputPassword.value !== (latestState?.customPassword || '')) {
  944. await chrome.runtime.sendMessage({
  945. type: 'SAVE_SETTING',
  946. source: 'sidepanel',
  947. payload: { customPassword: inputPassword.value },
  948. });
  949. syncLatestState({ customPassword: inputPassword.value });
  950. }
  951. if (selectMailProvider.value === 'hotmail-api') {
  952. const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
  953. if (response?.error) {
  954. throw new Error(response.error);
  955. }
  956. } else {
  957. let email = inputEmail.value.trim();
  958. if (!email) {
  959. try {
  960. email = await fetchDuckEmail({ showFailureToast: false });
  961. } catch (err) {
  962. showToast(`自动获取失败:${err.message},请手动粘贴邮箱后重试。`, 'warn');
  963. return;
  964. }
  965. }
  966. const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
  967. if (response?.error) {
  968. throw new Error(response.error);
  969. }
  970. }
  971. } else {
  972. const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
  973. if (response?.error) {
  974. throw new Error(response.error);
  975. }
  976. }
  977. } catch (err) {
  978. showToast(err.message, 'error');
  979. }
  980. });
  981. });
  982. btnFetchEmail.addEventListener('click', async () => {
  983. if (selectMailProvider.value === 'hotmail-api') {
  984. return;
  985. }
  986. await fetchDuckEmail().catch(() => {});
  987. });
  988. btnToggleHotmailList?.addEventListener('click', () => {
  989. setHotmailListExpanded(!hotmailListExpanded);
  990. });
  991. btnClearUsedHotmailAccounts?.addEventListener('click', async () => {
  992. if (hotmailActionInFlight) return;
  993. hotmailActionInFlight = true;
  994. btnClearUsedHotmailAccounts.disabled = true;
  995. try {
  996. await deleteHotmailAccountsByMode('used');
  997. } catch (err) {
  998. showToast(err.message, 'error');
  999. } finally {
  1000. hotmailActionInFlight = false;
  1001. updateHotmailListViewport();
  1002. }
  1003. });
  1004. btnDeleteAllHotmailAccounts?.addEventListener('click', async () => {
  1005. if (hotmailActionInFlight) return;
  1006. hotmailActionInFlight = true;
  1007. btnDeleteAllHotmailAccounts.disabled = true;
  1008. try {
  1009. await deleteHotmailAccountsByMode('all');
  1010. } catch (err) {
  1011. showToast(err.message, 'error');
  1012. } finally {
  1013. hotmailActionInFlight = false;
  1014. updateHotmailListViewport();
  1015. }
  1016. });
  1017. btnAddHotmailAccount?.addEventListener('click', async () => {
  1018. if (hotmailActionInFlight) return;
  1019. const email = inputHotmailEmail.value.trim();
  1020. const clientId = inputHotmailClientId.value.trim();
  1021. const refreshToken = inputHotmailRefreshToken.value.trim();
  1022. if (!email) {
  1023. showToast('请先填写 Hotmail 邮箱。', 'warn');
  1024. return;
  1025. }
  1026. if (!clientId) {
  1027. showToast('请先填写微软应用客户端 ID。', 'warn');
  1028. return;
  1029. }
  1030. if (!refreshToken) {
  1031. showToast('请先填写刷新令牌(refresh token)。', 'warn');
  1032. return;
  1033. }
  1034. hotmailActionInFlight = true;
  1035. btnAddHotmailAccount.disabled = true;
  1036. try {
  1037. const response = await chrome.runtime.sendMessage({
  1038. type: 'UPSERT_HOTMAIL_ACCOUNT',
  1039. source: 'sidepanel',
  1040. payload: {
  1041. email,
  1042. clientId,
  1043. password: inputHotmailPassword.value,
  1044. refreshToken,
  1045. },
  1046. });
  1047. if (response?.error) {
  1048. throw new Error(response.error);
  1049. }
  1050. showToast(`已保存 Hotmail 账号 ${email}`, 'success', 1800);
  1051. clearHotmailForm();
  1052. } catch (err) {
  1053. showToast(`保存 Hotmail 账号失败:${err.message}`, 'error');
  1054. } finally {
  1055. hotmailActionInFlight = false;
  1056. btnAddHotmailAccount.disabled = false;
  1057. }
  1058. });
  1059. btnImportHotmailAccounts?.addEventListener('click', async () => {
  1060. if (hotmailActionInFlight) return;
  1061. if (typeof parseHotmailImportText !== 'function') {
  1062. showToast('导入解析器未加载,请刷新扩展后重试。', 'error');
  1063. return;
  1064. }
  1065. const rawText = inputHotmailImport.value.trim();
  1066. if (!rawText) {
  1067. showToast('请先粘贴账号导入内容。', 'warn');
  1068. return;
  1069. }
  1070. const parsedAccounts = parseHotmailImportText(rawText);
  1071. if (!parsedAccounts.length) {
  1072. showToast('没有解析到有效账号,请检查格式是否为 账号----密码----ID----Token。', 'error');
  1073. return;
  1074. }
  1075. hotmailActionInFlight = true;
  1076. btnImportHotmailAccounts.disabled = true;
  1077. try {
  1078. for (const account of parsedAccounts) {
  1079. const response = await chrome.runtime.sendMessage({
  1080. type: 'UPSERT_HOTMAIL_ACCOUNT',
  1081. source: 'sidepanel',
  1082. payload: account,
  1083. });
  1084. if (response?.error) {
  1085. throw new Error(response.error);
  1086. }
  1087. }
  1088. inputHotmailImport.value = '';
  1089. showToast(`已导入 ${parsedAccounts.length} 条 Hotmail 账号`, 'success', 2200);
  1090. } catch (err) {
  1091. showToast(`批量导入失败:${err.message}`, 'error');
  1092. } finally {
  1093. hotmailActionInFlight = false;
  1094. btnImportHotmailAccounts.disabled = false;
  1095. }
  1096. });
  1097. hotmailAccountsList?.addEventListener('click', async (event) => {
  1098. const actionButton = event.target.closest('[data-account-action]');
  1099. if (!actionButton || hotmailActionInFlight) {
  1100. return;
  1101. }
  1102. const accountId = actionButton.dataset.accountId;
  1103. const action = actionButton.dataset.accountAction;
  1104. if (!accountId || !action) {
  1105. return;
  1106. }
  1107. const targetAccount = getHotmailAccounts().find((account) => account.id === accountId) || null;
  1108. hotmailActionInFlight = true;
  1109. actionButton.disabled = true;
  1110. try {
  1111. if (action === 'copy-email') {
  1112. if (!targetAccount?.email) throw new Error('未找到可复制的邮箱地址。');
  1113. await copyTextToClipboard(targetAccount.email);
  1114. showToast(`已复制 ${targetAccount.email}`, 'success', 1800);
  1115. } else if (action === 'select') {
  1116. const response = await chrome.runtime.sendMessage({
  1117. type: 'SELECT_HOTMAIL_ACCOUNT',
  1118. source: 'sidepanel',
  1119. payload: { accountId },
  1120. });
  1121. if (response?.error) throw new Error(response.error);
  1122. syncLatestState({ currentHotmailAccountId: response.account.id });
  1123. applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
  1124. showToast(`已切换当前 Hotmail 账号为 ${response.account.email}`, 'success', 1800);
  1125. } else if (action === 'toggle-used') {
  1126. if (!targetAccount) throw new Error('未找到目标 Hotmail 账号。');
  1127. const response = await chrome.runtime.sendMessage({
  1128. type: 'PATCH_HOTMAIL_ACCOUNT',
  1129. source: 'sidepanel',
  1130. payload: {
  1131. accountId,
  1132. updates: { used: !targetAccount.used },
  1133. },
  1134. });
  1135. if (response?.error) throw new Error(response.error);
  1136. applyHotmailAccountMutation(response.account);
  1137. showToast(`账号 ${response.account.email} 已${response.account.used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
  1138. } else if (action === 'verify') {
  1139. const response = await chrome.runtime.sendMessage({
  1140. type: 'VERIFY_HOTMAIL_ACCOUNT',
  1141. source: 'sidepanel',
  1142. payload: { accountId },
  1143. });
  1144. if (response?.error) throw new Error(response.error);
  1145. applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
  1146. showToast(`账号 ${response.account.email} 校验通过`, 'success', 2200);
  1147. } else if (action === 'test') {
  1148. const response = await chrome.runtime.sendMessage({
  1149. type: 'TEST_HOTMAIL_ACCOUNT',
  1150. source: 'sidepanel',
  1151. payload: { accountId },
  1152. });
  1153. if (response?.error) throw new Error(response.error);
  1154. applyHotmailAccountMutation(response.account, { preserveCurrentSelection: true });
  1155. if (response.latestCode) {
  1156. await copyTextToClipboard(response.latestCode);
  1157. const mailbox = response.latestMailbox ? `(${response.latestMailbox})` : '';
  1158. showToast(`已复制最新验证码 ${response.latestCode}${mailbox}`, 'success', 2600);
  1159. } else if (response.latestSubject) {
  1160. const mailbox = response.latestMailbox ? `(${response.latestMailbox})` : '';
  1161. showToast(`最新邮件${mailbox}没有验证码:${response.latestSubject}`, 'warn', 3200);
  1162. } else {
  1163. showToast('当前没有可读取的最新邮件。', 'warn', 2600);
  1164. }
  1165. } else if (action === 'delete') {
  1166. const confirmed = await openConfirmModal({
  1167. title: '删除账号',
  1168. message: '确认删除这个 Hotmail 账号吗?对应 token 也会一起移除。',
  1169. confirmLabel: '确认删除',
  1170. confirmVariant: 'btn-danger',
  1171. });
  1172. if (!confirmed) {
  1173. return;
  1174. }
  1175. const response = await chrome.runtime.sendMessage({
  1176. type: 'DELETE_HOTMAIL_ACCOUNT',
  1177. source: 'sidepanel',
  1178. payload: { accountId },
  1179. });
  1180. if (response?.error) throw new Error(response.error);
  1181. showToast('Hotmail 账号已删除', 'success', 1800);
  1182. }
  1183. } catch (err) {
  1184. showToast(err.message, 'error');
  1185. } finally {
  1186. hotmailActionInFlight = false;
  1187. actionButton.disabled = false;
  1188. }
  1189. });
  1190. btnTogglePassword.addEventListener('click', () => {
  1191. inputPassword.type = inputPassword.type === 'password' ? 'text' : 'password';
  1192. syncPasswordToggleLabel();
  1193. });
  1194. btnToggleVpsUrl.addEventListener('click', () => {
  1195. inputVpsUrl.type = inputVpsUrl.type === 'password' ? 'text' : 'password';
  1196. syncVpsUrlToggleLabel();
  1197. });
  1198. btnSaveSettings.addEventListener('click', async () => {
  1199. if (!settingsDirty) {
  1200. showToast('配置已是最新', 'info', 1400);
  1201. return;
  1202. }
  1203. await saveSettings({ silent: false }).catch(() => {});
  1204. });
  1205. btnStop.addEventListener('click', async () => {
  1206. btnStop.disabled = true;
  1207. await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} });
  1208. showToast('正在停止当前流程...', 'warn', 2000);
  1209. });
  1210. autoStartModal?.addEventListener('click', (event) => {
  1211. if (event.target === autoStartModal) {
  1212. resolveModalChoice(null);
  1213. }
  1214. });
  1215. btnAutoStartClose?.addEventListener('click', () => resolveModalChoice(null));
  1216. // Auto Run
  1217. btnAutoRun.addEventListener('click', async () => {
  1218. try {
  1219. const totalRuns = parseInt(inputRunCount.value) || 1;
  1220. let mode = 'restart';
  1221. if (shouldOfferAutoModeChoice()) {
  1222. const startStep = getFirstUnfinishedStep();
  1223. const choice = await openAutoStartChoiceDialog(startStep);
  1224. if (!choice) {
  1225. return;
  1226. }
  1227. mode = choice;
  1228. }
  1229. btnAutoRun.disabled = true;
  1230. inputRunCount.disabled = true;
  1231. 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> 运行中...';
  1232. const response = await chrome.runtime.sendMessage({
  1233. type: 'AUTO_RUN',
  1234. source: 'sidepanel',
  1235. payload: {
  1236. totalRuns,
  1237. autoRunSkipFailures: inputAutoSkipFailures.checked,
  1238. mode,
  1239. },
  1240. });
  1241. if (response?.error) {
  1242. throw new Error(response.error);
  1243. }
  1244. } catch (err) {
  1245. setDefaultAutoRunButton();
  1246. inputRunCount.disabled = false;
  1247. showToast(err.message, 'error');
  1248. }
  1249. });
  1250. btnAutoContinue.addEventListener('click', async () => {
  1251. const email = inputEmail.value.trim();
  1252. if (!email) {
  1253. showToast('请先获取或粘贴 DuckDuckGo 邮箱。', 'warn');
  1254. return;
  1255. }
  1256. autoContinueBar.style.display = 'none';
  1257. await chrome.runtime.sendMessage({ type: 'RESUME_AUTO_RUN', source: 'sidepanel', payload: { email } });
  1258. });
  1259. // Reset
  1260. btnReset.addEventListener('click', async () => {
  1261. const confirmed = await openConfirmModal({
  1262. title: '重置流程',
  1263. message: '确认重置全部步骤和数据吗?',
  1264. confirmLabel: '确认重置',
  1265. confirmVariant: 'btn-danger',
  1266. });
  1267. if (!confirmed) {
  1268. return;
  1269. }
  1270. await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
  1271. syncLatestState({ stepStatuses: STEP_DEFAULT_STATUSES, currentHotmailAccountId: null, email: null });
  1272. syncAutoRunState({ autoRunning: false, autoRunPhase: 'idle', autoRunCurrentRun: 0, autoRunTotalRuns: 1, autoRunAttemptRun: 0 });
  1273. displayOauthUrl.textContent = '等待中...';
  1274. displayOauthUrl.classList.remove('has-value');
  1275. displayLocalhostUrl.textContent = '等待中...';
  1276. displayLocalhostUrl.classList.remove('has-value');
  1277. inputEmail.value = '';
  1278. displayStatus.textContent = '就绪';
  1279. statusBar.className = 'status-bar';
  1280. logArea.innerHTML = '';
  1281. document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
  1282. document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
  1283. setDefaultAutoRunButton();
  1284. applyAutoRunStatus(currentAutoRun);
  1285. markSettingsDirty(false);
  1286. updateStopButtonState(false);
  1287. updateButtonStates();
  1288. updateProgressCounter();
  1289. renderHotmailAccounts();
  1290. });
  1291. // Clear log
  1292. btnClearLog.addEventListener('click', () => {
  1293. logArea.innerHTML = '';
  1294. });
  1295. // Save settings on change
  1296. inputEmail.addEventListener('change', async () => {
  1297. if (selectMailProvider.value === 'hotmail-api') {
  1298. return;
  1299. }
  1300. const email = inputEmail.value.trim();
  1301. if (email) {
  1302. await chrome.runtime.sendMessage({ type: 'SAVE_EMAIL', source: 'sidepanel', payload: { email } });
  1303. }
  1304. });
  1305. inputEmail.addEventListener('input', updateButtonStates);
  1306. inputVpsUrl.addEventListener('input', () => {
  1307. markSettingsDirty(true);
  1308. scheduleSettingsAutoSave();
  1309. });
  1310. inputVpsUrl.addEventListener('blur', () => {
  1311. saveSettings({ silent: true }).catch(() => {});
  1312. });
  1313. inputVpsPassword.addEventListener('input', () => {
  1314. markSettingsDirty(true);
  1315. scheduleSettingsAutoSave();
  1316. });
  1317. inputVpsPassword.addEventListener('blur', () => {
  1318. saveSettings({ silent: true }).catch(() => {});
  1319. });
  1320. inputPassword.addEventListener('input', () => {
  1321. markSettingsDirty(true);
  1322. updateButtonStates();
  1323. scheduleSettingsAutoSave();
  1324. });
  1325. inputPassword.addEventListener('blur', () => {
  1326. saveSettings({ silent: true }).catch(() => {});
  1327. });
  1328. selectMailProvider.addEventListener('change', () => {
  1329. updateMailProviderUI();
  1330. markSettingsDirty(true);
  1331. saveSettings({ silent: true }).catch(() => {});
  1332. });
  1333. inputInbucketMailbox.addEventListener('input', () => {
  1334. markSettingsDirty(true);
  1335. scheduleSettingsAutoSave();
  1336. });
  1337. inputInbucketMailbox.addEventListener('blur', () => {
  1338. saveSettings({ silent: true }).catch(() => {});
  1339. });
  1340. inputInbucketHost.addEventListener('input', () => {
  1341. markSettingsDirty(true);
  1342. scheduleSettingsAutoSave();
  1343. });
  1344. inputInbucketHost.addEventListener('blur', () => {
  1345. saveSettings({ silent: true }).catch(() => {});
  1346. });
  1347. inputAutoSkipFailures.addEventListener('change', () => {
  1348. markSettingsDirty(true);
  1349. saveSettings({ silent: true }).catch(() => {});
  1350. });
  1351. // ============================================================
  1352. // Listen for Background broadcasts
  1353. // ============================================================
  1354. chrome.runtime.onMessage.addListener((message) => {
  1355. switch (message.type) {
  1356. case 'LOG_ENTRY':
  1357. appendLog(message.payload);
  1358. if (message.payload.level === 'error') {
  1359. showToast(message.payload.message, 'error');
  1360. }
  1361. break;
  1362. case 'STEP_STATUS_CHANGED': {
  1363. const { step, status } = message.payload;
  1364. updateStepUI(step, status);
  1365. chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => {
  1366. syncLatestState(state);
  1367. syncAutoRunState(state);
  1368. updateStatusDisplay(latestState);
  1369. updateButtonStates();
  1370. if (status === 'completed' || status === 'manual_completed' || status === 'skipped') {
  1371. syncPasswordField(state);
  1372. if (state.oauthUrl) {
  1373. displayOauthUrl.textContent = state.oauthUrl;
  1374. displayOauthUrl.classList.add('has-value');
  1375. }
  1376. if (state.localhostUrl) {
  1377. displayLocalhostUrl.textContent = state.localhostUrl;
  1378. displayLocalhostUrl.classList.add('has-value');
  1379. }
  1380. }
  1381. }
  1382. ).catch(() => {});
  1383. break;
  1384. }
  1385. case 'AUTO_RUN_RESET': {
  1386. // Full UI reset for next run
  1387. syncLatestState({
  1388. oauthUrl: null,
  1389. localhostUrl: null,
  1390. email: null,
  1391. password: null,
  1392. stepStatuses: STEP_DEFAULT_STATUSES,
  1393. logs: [],
  1394. });
  1395. displayOauthUrl.textContent = '等待中...';
  1396. displayOauthUrl.classList.remove('has-value');
  1397. displayLocalhostUrl.textContent = '等待中...';
  1398. displayLocalhostUrl.classList.remove('has-value');
  1399. inputEmail.value = '';
  1400. displayStatus.textContent = '就绪';
  1401. statusBar.className = 'status-bar';
  1402. logArea.innerHTML = '';
  1403. document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
  1404. document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
  1405. applyAutoRunStatus(currentAutoRun);
  1406. updateProgressCounter();
  1407. updateButtonStates();
  1408. renderHotmailAccounts();
  1409. break;
  1410. }
  1411. case 'DATA_UPDATED': {
  1412. syncLatestState(message.payload);
  1413. if (message.payload.email !== undefined) {
  1414. inputEmail.value = message.payload.email || '';
  1415. }
  1416. if (message.payload.password !== undefined) {
  1417. inputPassword.value = message.payload.password || '';
  1418. }
  1419. if (message.payload.oauthUrl !== undefined) {
  1420. displayOauthUrl.textContent = message.payload.oauthUrl || '等待中...';
  1421. displayOauthUrl.classList.toggle('has-value', Boolean(message.payload.oauthUrl));
  1422. }
  1423. if (message.payload.localhostUrl !== undefined) {
  1424. displayLocalhostUrl.textContent = message.payload.localhostUrl || '等待中...';
  1425. displayLocalhostUrl.classList.toggle('has-value', Boolean(message.payload.localhostUrl));
  1426. }
  1427. if (message.payload.currentHotmailAccountId !== undefined || message.payload.hotmailAccounts !== undefined) {
  1428. renderHotmailAccounts();
  1429. if (selectMailProvider.value === 'hotmail-api') {
  1430. const currentAccount = getCurrentHotmailAccount();
  1431. inputEmail.value = currentAccount?.email || latestState?.email || '';
  1432. }
  1433. }
  1434. break;
  1435. }
  1436. case 'AUTO_RUN_STATUS': {
  1437. syncLatestState({
  1438. autoRunning: ['running', 'waiting_email', 'retrying'].includes(message.payload.phase),
  1439. autoRunPhase: message.payload.phase,
  1440. autoRunCurrentRun: message.payload.currentRun,
  1441. autoRunTotalRuns: message.payload.totalRuns,
  1442. autoRunAttemptRun: message.payload.attemptRun,
  1443. });
  1444. applyAutoRunStatus(message.payload);
  1445. updateStatusDisplay(latestState);
  1446. updateButtonStates();
  1447. break;
  1448. }
  1449. }
  1450. });
  1451. // ============================================================
  1452. // Theme Toggle
  1453. // ============================================================
  1454. const btnTheme = document.getElementById('btn-theme');
  1455. function setTheme(theme) {
  1456. document.documentElement.setAttribute('data-theme', theme);
  1457. localStorage.setItem('multipage-theme', theme);
  1458. }
  1459. function initTheme() {
  1460. const saved = localStorage.getItem('multipage-theme');
  1461. if (saved) {
  1462. setTheme(saved);
  1463. } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  1464. setTheme('dark');
  1465. }
  1466. }
  1467. btnTheme.addEventListener('click', () => {
  1468. const current = document.documentElement.getAttribute('data-theme');
  1469. setTheme(current === 'dark' ? 'light' : 'dark');
  1470. });
  1471. // ============================================================
  1472. // Init
  1473. // ============================================================
  1474. initializeManualStepActions();
  1475. initTheme();
  1476. initHotmailListExpandedState();
  1477. updateSaveButtonState();
  1478. restoreState().then(() => {
  1479. syncPasswordToggleLabel();
  1480. syncVpsUrlToggleLabel();
  1481. updateButtonStates();
  1482. updateStatusDisplay(latestState);
  1483. });