background.js 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774
  1. // background.js — Service Worker: orchestration, state, tab management, message routing
  2. importScripts('data/names.js');
  3. const LOG_PREFIX = '[MultiPage:bg]';
  4. const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill';
  5. const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
  6. const HUMAN_STEP_DELAY_MIN = 700;
  7. const HUMAN_STEP_DELAY_MAX = 2200;
  8. initializeSessionStorageAccess();
  9. let automationWindowId = null;
  10. async function ensureAutomationWindowId() {
  11. if (automationWindowId != null) {
  12. try {
  13. await chrome.windows.get(automationWindowId);
  14. return automationWindowId;
  15. } catch {
  16. automationWindowId = null;
  17. }
  18. }
  19. const registry = await getTabRegistry();
  20. for (const entry of Object.values(registry)) {
  21. if (entry && entry.tabId) {
  22. try {
  23. const tab = await chrome.tabs.get(entry.tabId);
  24. automationWindowId = tab.windowId;
  25. return automationWindowId;
  26. } catch {}
  27. }
  28. }
  29. const win = await chrome.windows.getLastFocused();
  30. automationWindowId = win.id;
  31. return automationWindowId;
  32. }
  33. // ============================================================
  34. // State Management (chrome.storage.session + local for persistence)
  35. // ============================================================
  36. // Keys that should survive extension reload (saved to chrome.storage.local)
  37. const PERSISTENT_KEYS = [
  38. 'vpsUrl', 'customPassword', 'mailProvider',
  39. 'tempApiUrl',
  40. 'inbucketHost', 'inbucketMailbox', 'accounts',
  41. 'seenCodes', 'seenInbucketMailIds',
  42. 'duckToken',
  43. ];
  44. const DEFAULT_STATE = {
  45. currentStep: 0,
  46. stepStatuses: {
  47. 1: 'pending', 2: 'pending', 3: 'pending', 4: 'pending', 5: 'pending',
  48. 6: 'pending', 7: 'pending', 8: 'pending', 9: 'pending',
  49. },
  50. oauthUrl: null,
  51. email: null,
  52. password: null,
  53. accounts: [], // { email, password, createdAt }
  54. lastEmailTimestamp: null,
  55. localhostUrl: null,
  56. flowStartTime: null,
  57. tabRegistry: {},
  58. logs: [],
  59. vpsUrl: '',
  60. customPassword: '',
  61. mailProvider: 'temp-api', // 'temp-api', 'qq', '163', or 'inbucket'
  62. tempApiUrl: '',
  63. inbucketHost: '',
  64. inbucketMailbox: '',
  65. };
  66. async function getState() {
  67. const state = await chrome.storage.session.get(null);
  68. return { ...DEFAULT_STATE, ...state };
  69. }
  70. async function initializeSessionStorageAccess() {
  71. try {
  72. if (chrome.storage?.session?.setAccessLevel) {
  73. await chrome.storage.session.setAccessLevel({
  74. accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS',
  75. });
  76. console.log(LOG_PREFIX, 'Enabled storage.session for content scripts');
  77. }
  78. } catch (err) {
  79. console.warn(LOG_PREFIX, 'Failed to enable storage.session for content scripts:', err?.message || err);
  80. }
  81. // Restore persistent settings from chrome.storage.local into session
  82. try {
  83. const saved = await chrome.storage.local.get(PERSISTENT_KEYS);
  84. const toRestore = {};
  85. for (const key of PERSISTENT_KEYS) {
  86. if (saved[key] !== undefined) toRestore[key] = saved[key];
  87. }
  88. if (Object.keys(toRestore).length > 0) {
  89. await chrome.storage.session.set(toRestore);
  90. console.log(LOG_PREFIX, 'Restored persistent settings from local storage:', Object.keys(toRestore).join(', '));
  91. }
  92. } catch (err) {
  93. console.warn(LOG_PREFIX, 'Failed to restore persistent settings:', err?.message || err);
  94. }
  95. }
  96. async function setState(updates) {
  97. console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
  98. await chrome.storage.session.set(updates);
  99. // Persist relevant keys to chrome.storage.local so they survive reload
  100. const toPersist = {};
  101. for (const key of PERSISTENT_KEYS) {
  102. if (key in updates) toPersist[key] = updates[key];
  103. }
  104. if (Object.keys(toPersist).length > 0) {
  105. await chrome.storage.local.set(toPersist);
  106. }
  107. }
  108. function broadcastDataUpdate(payload) {
  109. chrome.runtime.sendMessage({
  110. type: 'DATA_UPDATED',
  111. payload,
  112. }).catch(() => {});
  113. }
  114. async function setEmailState(email) {
  115. await setState({ email });
  116. broadcastDataUpdate({ email });
  117. }
  118. async function setPasswordState(password) {
  119. await setState({ password });
  120. broadcastDataUpdate({ password });
  121. }
  122. async function resetState() {
  123. console.log(LOG_PREFIX, 'Resetting all state');
  124. // Preserve settings and persistent data across resets
  125. const prev = await chrome.storage.session.get([
  126. 'seenCodes',
  127. 'seenInbucketMailIds',
  128. 'accounts',
  129. 'tabRegistry',
  130. 'vpsUrl',
  131. 'customPassword',
  132. 'mailProvider',
  133. 'inbucketHost',
  134. 'inbucketMailbox',
  135. ]);
  136. // Also check chrome.storage.local in case session was empty (after reload)
  137. const local = await chrome.storage.local.get(PERSISTENT_KEYS);
  138. await chrome.storage.session.clear();
  139. await chrome.storage.session.set({
  140. ...DEFAULT_STATE,
  141. seenCodes: prev.seenCodes || local.seenCodes || [],
  142. seenInbucketMailIds: prev.seenInbucketMailIds || local.seenInbucketMailIds || [],
  143. accounts: prev.accounts || local.accounts || [],
  144. tabRegistry: prev.tabRegistry || {},
  145. vpsUrl: prev.vpsUrl || local.vpsUrl || '',
  146. customPassword: prev.customPassword || local.customPassword || '',
  147. mailProvider: prev.mailProvider || local.mailProvider || '163',
  148. inbucketHost: prev.inbucketHost || local.inbucketHost || '',
  149. inbucketMailbox: prev.inbucketMailbox || local.inbucketMailbox || '',
  150. });
  151. }
  152. /**
  153. * Generate a random password: 14 chars, mix of uppercase, lowercase, digits, symbols.
  154. */
  155. function generatePassword() {
  156. const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
  157. const lower = 'abcdefghjkmnpqrstuvwxyz';
  158. const digits = '23456789';
  159. const symbols = '!@#$%&*?';
  160. const all = upper + lower + digits + symbols;
  161. // Ensure at least one of each type
  162. let pw = '';
  163. pw += upper[Math.floor(Math.random() * upper.length)];
  164. pw += lower[Math.floor(Math.random() * lower.length)];
  165. pw += digits[Math.floor(Math.random() * digits.length)];
  166. pw += symbols[Math.floor(Math.random() * symbols.length)];
  167. // Fill remaining 10 chars
  168. for (let i = 0; i < 10; i++) {
  169. pw += all[Math.floor(Math.random() * all.length)];
  170. }
  171. // Shuffle
  172. return pw.split('').sort(() => Math.random() - 0.5).join('');
  173. }
  174. // ============================================================
  175. // Tab Registry
  176. // ============================================================
  177. async function getTabRegistry() {
  178. const state = await getState();
  179. return state.tabRegistry || {};
  180. }
  181. async function registerTab(source, tabId) {
  182. const registry = await getTabRegistry();
  183. registry[source] = { tabId, ready: true };
  184. await setState({ tabRegistry: registry });
  185. console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
  186. }
  187. async function isTabAlive(source) {
  188. const registry = await getTabRegistry();
  189. const entry = registry[source];
  190. if (!entry) return false;
  191. try {
  192. await chrome.tabs.get(entry.tabId);
  193. return true;
  194. } catch {
  195. // Tab no longer exists — clean up registry
  196. registry[source] = null;
  197. await setState({ tabRegistry: registry });
  198. return false;
  199. }
  200. }
  201. async function getTabId(source) {
  202. const registry = await getTabRegistry();
  203. return registry[source]?.tabId || null;
  204. }
  205. // ============================================================
  206. // Command Queue (for content scripts not yet ready)
  207. // ============================================================
  208. const pendingCommands = new Map(); // source -> { message, resolve, reject, timer }
  209. function queueCommand(source, message, timeout = 15000) {
  210. return new Promise((resolve, reject) => {
  211. const timer = setTimeout(() => {
  212. pendingCommands.delete(source);
  213. const err = `Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`;
  214. console.error(LOG_PREFIX, err);
  215. reject(new Error(err));
  216. }, timeout);
  217. pendingCommands.set(source, { message, resolve, reject, timer });
  218. console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
  219. });
  220. }
  221. function flushCommand(source, tabId) {
  222. const pending = pendingCommands.get(source);
  223. if (pending) {
  224. clearTimeout(pending.timer);
  225. pendingCommands.delete(source);
  226. chrome.tabs.sendMessage(tabId, pending.message).then(pending.resolve).catch(pending.reject);
  227. console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
  228. }
  229. }
  230. function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) {
  231. for (const [source, pending] of pendingCommands.entries()) {
  232. clearTimeout(pending.timer);
  233. pending.reject(new Error(reason));
  234. pendingCommands.delete(source);
  235. console.log(LOG_PREFIX, `Cancelled queued command for ${source}`);
  236. }
  237. }
  238. // ============================================================
  239. // Reuse or create tab
  240. // ============================================================
  241. async function reuseOrCreateTab(source, url, options = {}) {
  242. const alive = await isTabAlive(source);
  243. if (alive) {
  244. const tabId = await getTabId(source);
  245. const currentTab = await chrome.tabs.get(tabId);
  246. const sameUrl = currentTab.url === url;
  247. const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
  248. const registry = await getTabRegistry();
  249. if (sameUrl) {
  250. await chrome.tabs.update(tabId, { active: true });
  251. console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}) on same URL`);
  252. if (shouldReloadOnReuse) {
  253. if (registry[source]) registry[source].ready = false;
  254. await setState({ tabRegistry: registry });
  255. await chrome.tabs.reload(tabId);
  256. await new Promise((resolve) => {
  257. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  258. const listener = (tid, info) => {
  259. if (tid === tabId && info.status === 'complete') {
  260. chrome.tabs.onUpdated.removeListener(listener);
  261. clearTimeout(timer);
  262. resolve();
  263. }
  264. };
  265. chrome.tabs.onUpdated.addListener(listener);
  266. });
  267. }
  268. // For dynamically injected pages like the VPS panel, re-inject immediately.
  269. if (options.inject) {
  270. if (registry[source]) registry[source].ready = false;
  271. await setState({ tabRegistry: registry });
  272. if (options.injectSource) {
  273. await chrome.scripting.executeScript({
  274. target: { tabId },
  275. func: (injectedSource) => {
  276. window.__MULTIPAGE_SOURCE = injectedSource;
  277. },
  278. args: [options.injectSource],
  279. });
  280. }
  281. await chrome.scripting.executeScript({
  282. target: { tabId },
  283. files: options.inject,
  284. });
  285. await new Promise(r => setTimeout(r, 500));
  286. }
  287. return tabId;
  288. }
  289. // Mark as not ready BEFORE navigating — so READY signal from new page is captured correctly
  290. if (registry[source]) registry[source].ready = false;
  291. await setState({ tabRegistry: registry });
  292. // Navigate existing tab to new URL
  293. await chrome.tabs.update(tabId, { url, active: true });
  294. console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}), navigated to ${url.slice(0, 60)}`);
  295. // Wait for page load complete (with 30s timeout)
  296. await new Promise((resolve) => {
  297. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  298. const listener = (tid, info) => {
  299. if (tid === tabId && info.status === 'complete') {
  300. chrome.tabs.onUpdated.removeListener(listener);
  301. clearTimeout(timer);
  302. resolve();
  303. }
  304. };
  305. chrome.tabs.onUpdated.addListener(listener);
  306. });
  307. // If dynamic injection needed (VPS panel), re-inject after navigation
  308. if (options.inject) {
  309. if (options.injectSource) {
  310. await chrome.scripting.executeScript({
  311. target: { tabId },
  312. func: (injectedSource) => {
  313. window.__MULTIPAGE_SOURCE = injectedSource;
  314. },
  315. args: [options.injectSource],
  316. });
  317. }
  318. await chrome.scripting.executeScript({
  319. target: { tabId },
  320. files: options.inject,
  321. });
  322. }
  323. // Wait a bit for content script to inject and send READY
  324. await new Promise(r => setTimeout(r, 500));
  325. return tabId;
  326. }
  327. // Create new tab in the automation window
  328. const wid = await ensureAutomationWindowId();
  329. const tab = await chrome.tabs.create({ url, active: true, windowId: wid });
  330. console.log(LOG_PREFIX, `Created new tab ${source} (${tab.id})`);
  331. // If dynamic injection needed (VPS panel), inject scripts after load
  332. if (options.inject) {
  333. await new Promise((resolve) => {
  334. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  335. const listener = (tabId, info) => {
  336. if (tabId === tab.id && info.status === 'complete') {
  337. chrome.tabs.onUpdated.removeListener(listener);
  338. clearTimeout(timer);
  339. resolve();
  340. }
  341. };
  342. chrome.tabs.onUpdated.addListener(listener);
  343. });
  344. if (options.injectSource) {
  345. await chrome.scripting.executeScript({
  346. target: { tabId: tab.id },
  347. func: (injectedSource) => {
  348. window.__MULTIPAGE_SOURCE = injectedSource;
  349. },
  350. args: [options.injectSource],
  351. });
  352. }
  353. await chrome.scripting.executeScript({
  354. target: { tabId: tab.id },
  355. files: options.inject,
  356. });
  357. }
  358. return tab.id;
  359. }
  360. // ============================================================
  361. // Send command to content script (with readiness check)
  362. // ============================================================
  363. async function sendToContentScript(source, message) {
  364. const registry = await getTabRegistry();
  365. const entry = registry[source];
  366. if (!entry || !entry.ready) {
  367. console.log(LOG_PREFIX, `${source} not ready, queuing command`);
  368. return queueCommand(source, message);
  369. }
  370. // Verify tab is still alive
  371. const alive = await isTabAlive(source);
  372. if (!alive) {
  373. // Tab was closed — queue the command, it will be sent when tab is reopened
  374. console.log(LOG_PREFIX, `${source} tab was closed, queuing command`);
  375. return queueCommand(source, message);
  376. }
  377. console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type);
  378. try {
  379. return await chrome.tabs.sendMessage(entry.tabId, message);
  380. } catch (err) {
  381. // Handle bfcache / port closed errors by reloading the tab and retrying
  382. if (err?.message?.includes('back/forward cache') || err?.message?.includes('message channel is closed')) {
  383. console.log(LOG_PREFIX, `${source} hit bfcache error, reloading tab and retrying...`);
  384. try {
  385. await chrome.tabs.reload(entry.tabId);
  386. // Wait for page to reload and content script to re-inject
  387. await new Promise((resolve) => {
  388. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 15000);
  389. const listener = (tid, info) => {
  390. if (tid === entry.tabId && info.status === 'complete') {
  391. chrome.tabs.onUpdated.removeListener(listener);
  392. clearTimeout(timer);
  393. resolve();
  394. }
  395. };
  396. chrome.tabs.onUpdated.addListener(listener);
  397. });
  398. // Mark as not ready and wait for READY signal
  399. const reg = await getTabRegistry();
  400. if (reg[source]) reg[source].ready = false;
  401. await setState({ tabRegistry: reg });
  402. // Queue the command — it'll be sent once the content script signals READY
  403. return queueCommand(source, message);
  404. } catch (retryErr) {
  405. throw new Error(`${source} bfcache recovery failed: ${retryErr.message}`);
  406. }
  407. }
  408. throw err;
  409. }
  410. }
  411. // ============================================================
  412. // Logging
  413. // ============================================================
  414. async function addLog(message, level = 'info') {
  415. const state = await getState();
  416. const logs = state.logs || [];
  417. const entry = { message, level, timestamp: Date.now() };
  418. logs.push(entry);
  419. // Keep last 500 logs
  420. if (logs.length > 500) logs.splice(0, logs.length - 500);
  421. await setState({ logs });
  422. // Broadcast to side panel
  423. chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {});
  424. }
  425. // ============================================================
  426. // Step Status Management
  427. // ============================================================
  428. async function setStepStatus(step, status) {
  429. const state = await getState();
  430. const statuses = { ...state.stepStatuses };
  431. statuses[step] = status;
  432. await setState({ stepStatuses: statuses, currentStep: step });
  433. // Broadcast to side panel
  434. chrome.runtime.sendMessage({
  435. type: 'STEP_STATUS_CHANGED',
  436. payload: { step, status },
  437. }).catch(() => {});
  438. }
  439. function isStopError(error) {
  440. const message = typeof error === 'string' ? error : error?.message;
  441. return message === STOP_ERROR_MESSAGE;
  442. }
  443. function clearStopRequest() {
  444. stopRequested = false;
  445. }
  446. function throwIfStopped() {
  447. if (stopRequested) {
  448. throw new Error(STOP_ERROR_MESSAGE);
  449. }
  450. }
  451. async function sleepWithStop(ms) {
  452. const start = Date.now();
  453. while (Date.now() - start < ms) {
  454. throwIfStopped();
  455. await new Promise(r => setTimeout(r, Math.min(100, ms - (Date.now() - start))));
  456. }
  457. }
  458. async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY_MAX) {
  459. const duration = Math.floor(Math.random() * (max - min + 1)) + min;
  460. await sleepWithStop(duration);
  461. }
  462. // clickWithDebugger removed — Step 8 now uses content script simulateClick directly
  463. async function broadcastStopToContentScripts() {
  464. const registry = await getTabRegistry();
  465. for (const entry of Object.values(registry)) {
  466. if (!entry?.tabId) continue;
  467. try {
  468. await chrome.tabs.sendMessage(entry.tabId, {
  469. type: 'STOP_FLOW',
  470. source: 'background',
  471. payload: {},
  472. });
  473. } catch {}
  474. }
  475. }
  476. let stopRequested = false;
  477. // ============================================================
  478. // Message Handler (central router)
  479. // ============================================================
  480. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  481. console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message);
  482. handleMessage(message, sender).then(response => {
  483. sendResponse(response);
  484. }).catch(err => {
  485. console.error(LOG_PREFIX, 'Handler error:', err);
  486. sendResponse({ error: err.message });
  487. });
  488. return true; // async response
  489. });
  490. async function handleMessage(message, sender) {
  491. switch (message.type) {
  492. case 'CONTENT_SCRIPT_READY': {
  493. const tabId = sender.tab?.id;
  494. if (tabId && message.source) {
  495. await registerTab(message.source, tabId);
  496. flushCommand(message.source, tabId);
  497. await addLog(`Content script ready: ${message.source} (tab ${tabId})`);
  498. }
  499. return { ok: true };
  500. }
  501. case 'LOG': {
  502. const { message: msg, level } = message.payload;
  503. await addLog(`[${message.source}] ${msg}`, level);
  504. return { ok: true };
  505. }
  506. case 'STEP_COMPLETE': {
  507. if (stopRequested) {
  508. await setStepStatus(message.step, 'stopped');
  509. notifyStepError(message.step, STOP_ERROR_MESSAGE);
  510. return { ok: true };
  511. }
  512. await setStepStatus(message.step, 'completed');
  513. await addLog(`Step ${message.step} completed`, 'ok');
  514. await handleStepData(message.step, message.payload);
  515. notifyStepComplete(message.step, message.payload);
  516. return { ok: true };
  517. }
  518. case 'STEP_ERROR': {
  519. if (isStopError(message.error)) {
  520. await setStepStatus(message.step, 'stopped');
  521. await addLog(`Step ${message.step} stopped by user`, 'warn');
  522. notifyStepError(message.step, message.error);
  523. } else {
  524. await setStepStatus(message.step, 'failed');
  525. await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
  526. notifyStepError(message.step, message.error);
  527. }
  528. return { ok: true };
  529. }
  530. case 'GET_STATE': {
  531. return await getState();
  532. }
  533. case 'RESET': {
  534. clearStopRequest();
  535. await resetState();
  536. await addLog('Flow reset', 'info');
  537. return { ok: true };
  538. }
  539. case 'EXECUTE_STEP': {
  540. clearStopRequest();
  541. const step = message.payload.step;
  542. // Save email if provided (from side panel step 3)
  543. if (message.payload.email) {
  544. await setEmailState(message.payload.email);
  545. }
  546. await executeStep(step);
  547. return { ok: true };
  548. }
  549. case 'AUTO_RUN': {
  550. clearStopRequest();
  551. const totalRuns = message.payload?.totalRuns || 1;
  552. autoRunLoop(totalRuns); // fire-and-forget
  553. return { ok: true };
  554. }
  555. case 'RESUME_AUTO_RUN': {
  556. clearStopRequest();
  557. if (message.payload.email) {
  558. await setEmailState(message.payload.email);
  559. }
  560. resumeAutoRun(); // fire-and-forget
  561. return { ok: true };
  562. }
  563. case 'SAVE_SETTING': {
  564. const updates = {};
  565. if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
  566. if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
  567. if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
  568. if (message.payload.tempApiUrl !== undefined) updates.tempApiUrl = message.payload.tempApiUrl;
  569. if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
  570. if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
  571. await setState(updates);
  572. return { ok: true };
  573. }
  574. // Side panel data updates
  575. case 'SAVE_EMAIL': {
  576. await setEmailState(message.payload.email);
  577. return { ok: true, email: message.payload.email };
  578. }
  579. case 'FETCH_DUCK_EMAIL': {
  580. clearStopRequest();
  581. const email = await fetchDuckEmail(message.payload || {});
  582. return { ok: true, email };
  583. }
  584. case 'STOP_FLOW': {
  585. await requestStop();
  586. return { ok: true };
  587. }
  588. default:
  589. console.warn(LOG_PREFIX, `Unknown message type: ${message.type}`);
  590. return { error: `Unknown message type: ${message.type}` };
  591. }
  592. }
  593. // ============================================================
  594. // Step Data Handlers
  595. // ============================================================
  596. async function handleStepData(step, payload) {
  597. switch (step) {
  598. case 1:
  599. if (payload.oauthUrl) {
  600. await setState({ oauthUrl: payload.oauthUrl });
  601. broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
  602. }
  603. break;
  604. case 3:
  605. if (payload.email) await setEmailState(payload.email);
  606. break;
  607. case 4:
  608. if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp });
  609. break;
  610. case 8:
  611. if (payload.localhostUrl) {
  612. await setState({ localhostUrl: payload.localhostUrl });
  613. broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
  614. }
  615. break;
  616. }
  617. }
  618. // ============================================================
  619. // Step Completion Waiting
  620. // ============================================================
  621. // Map of step -> { resolve, reject } for waiting on step completion
  622. const stepWaiters = new Map();
  623. let resumeWaiter = null;
  624. function waitForStepComplete(step, timeoutMs = 120000) {
  625. return new Promise((resolve, reject) => {
  626. throwIfStopped();
  627. const timer = setTimeout(() => {
  628. stepWaiters.delete(step);
  629. reject(new Error(`Step ${step} timed out after ${timeoutMs / 1000}s`));
  630. }, timeoutMs);
  631. stepWaiters.set(step, {
  632. resolve: (data) => { clearTimeout(timer); stepWaiters.delete(step); resolve(data); },
  633. reject: (err) => { clearTimeout(timer); stepWaiters.delete(step); reject(err); },
  634. });
  635. });
  636. }
  637. function notifyStepComplete(step, payload) {
  638. const waiter = stepWaiters.get(step);
  639. if (waiter) waiter.resolve(payload);
  640. }
  641. function notifyStepError(step, error) {
  642. const waiter = stepWaiters.get(step);
  643. if (waiter) waiter.reject(new Error(error));
  644. }
  645. async function markRunningStepsStopped() {
  646. const state = await getState();
  647. const runningSteps = Object.entries(state.stepStatuses || {})
  648. .filter(([, status]) => status === 'running')
  649. .map(([step]) => Number(step));
  650. for (const step of runningSteps) {
  651. await setStepStatus(step, 'stopped');
  652. }
  653. }
  654. async function requestStop() {
  655. if (stopRequested) return;
  656. stopRequested = true;
  657. cancelPendingCommands();
  658. if (webNavListener) {
  659. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  660. webNavListener = null;
  661. }
  662. await addLog('Stop requested. Cancelling current operations...', 'warn');
  663. await broadcastStopToContentScripts();
  664. for (const waiter of stepWaiters.values()) {
  665. waiter.reject(new Error(STOP_ERROR_MESSAGE));
  666. }
  667. stepWaiters.clear();
  668. if (resumeWaiter) {
  669. resumeWaiter.reject(new Error(STOP_ERROR_MESSAGE));
  670. resumeWaiter = null;
  671. }
  672. await markRunningStepsStopped();
  673. autoRunActive = false;
  674. await setState({ autoRunning: false });
  675. chrome.runtime.sendMessage({
  676. type: 'AUTO_RUN_STATUS',
  677. payload: { phase: 'stopped', currentRun: autoRunCurrentRun, totalRuns: autoRunTotalRuns },
  678. }).catch(() => {});
  679. }
  680. // ============================================================
  681. // Step Execution
  682. // ============================================================
  683. async function executeStep(step) {
  684. console.log(LOG_PREFIX, `Executing step ${step}`);
  685. throwIfStopped();
  686. await setStepStatus(step, 'running');
  687. await addLog(`Step ${step} started`);
  688. await humanStepDelay();
  689. const state = await getState();
  690. // Set flow start time on first step
  691. if (step === 1 && !state.flowStartTime) {
  692. await setState({ flowStartTime: Date.now() });
  693. }
  694. try {
  695. switch (step) {
  696. case 1: await executeStep1(state); break;
  697. case 2: await executeStep2(state); break;
  698. case 3: await executeStep3(state); break;
  699. case 4: await executeStep4(state); break;
  700. case 5: await executeStep5(state); break;
  701. case 6: await executeStep6(state); break;
  702. case 7: await executeStep7(state); break;
  703. case 8: await executeStep8(state); break;
  704. case 9: await executeStep9(state); break;
  705. default:
  706. throw new Error(`Unknown step: ${step}`);
  707. }
  708. } catch (err) {
  709. if (isStopError(err)) {
  710. await setStepStatus(step, 'stopped');
  711. await addLog(`Step ${step} stopped by user`, 'warn');
  712. throw err;
  713. }
  714. await setStepStatus(step, 'failed');
  715. await addLog(`Step ${step} failed: ${err.message}`, 'error');
  716. throw err;
  717. }
  718. }
  719. /**
  720. * Execute a step and wait for it to complete before returning.
  721. * @param {number} step
  722. * @param {number} delayAfter - ms to wait after completion (for page transitions)
  723. */
  724. async function executeStepAndWait(step, delayAfter = 2000) {
  725. throwIfStopped();
  726. const promise = waitForStepComplete(step, 120000);
  727. await executeStep(step);
  728. await promise;
  729. // Extra delay for page transitions / DOM updates
  730. if (delayAfter > 0) {
  731. await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200));
  732. }
  733. }
  734. async function fetchDuckEmail(options = {}) {
  735. throwIfStopped();
  736. // Try API-based generation first (no tab needed)
  737. const state = await getState();
  738. let token = state.duckToken;
  739. if (token) {
  740. try {
  741. const email = await generateDuckEmailViaApi(token);
  742. await setEmailState(email);
  743. await addLog(`Duck Mail: Generated ${email} (via API, no tab)`, 'ok');
  744. return email;
  745. } catch (err) {
  746. await addLog(`Duck Mail: API generation failed: ${err.message}, falling back to page...`, 'warn');
  747. // Token may be expired, clear it so we re-extract
  748. token = null;
  749. await setState({ duckToken: null });
  750. }
  751. }
  752. // Fallback: open page, extract token for future use, then generate
  753. await addLog('Duck Mail: Opening page to extract token...');
  754. await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL);
  755. // Try to extract and save the access token
  756. try {
  757. const tokenResult = await sendToContentScript('duck-mail', {
  758. type: 'EXTRACT_DUCK_TOKEN',
  759. source: 'background',
  760. payload: {},
  761. });
  762. if (tokenResult?.token) {
  763. token = tokenResult.token;
  764. await setState({ duckToken: token });
  765. await addLog(`Duck Mail: Token saved for ${tokenResult.username}, future runs won't need page`, 'ok');
  766. // Now use API to generate
  767. const email = await generateDuckEmailViaApi(token);
  768. await setEmailState(email);
  769. await addLog(`Duck Mail: Generated ${email} (via API)`, 'ok');
  770. return email;
  771. }
  772. } catch (err) {
  773. await addLog(`Duck Mail: Token extraction failed: ${err.message}, using page method`, 'warn');
  774. }
  775. // Final fallback: use the old page-based method
  776. const result = await sendToContentScript('duck-mail', {
  777. type: 'FETCH_DUCK_EMAIL',
  778. source: 'background',
  779. payload: { generateNew: true },
  780. });
  781. if (result?.error) throw new Error(result.error);
  782. if (!result?.email) throw new Error('Duck email not returned.');
  783. await setEmailState(result.email);
  784. await addLog(`Duck Mail: ${result.generated ? 'Generated' : 'Loaded'} ${result.email} (via page)`, 'ok');
  785. return result.email;
  786. }
  787. async function generateDuckEmailViaApi(token) {
  788. const resp = await fetch('https://quack.duckduckgo.com/api/email/addresses', {
  789. method: 'POST',
  790. headers: {
  791. 'Authorization': `Bearer ${token}`,
  792. 'Content-Type': 'application/json',
  793. },
  794. });
  795. if (!resp.ok) {
  796. throw new Error(`DuckDuckGo API returned ${resp.status}`);
  797. }
  798. const data = await resp.json();
  799. // Response: { address: "random-words" }
  800. const address = data?.address;
  801. if (!address) throw new Error('No address in API response');
  802. return `${address}@duck.com`;
  803. }
  804. // ============================================================
  805. // Auto Run Flow
  806. // ============================================================
  807. let autoRunActive = false;
  808. let autoRunCurrentRun = 0;
  809. let autoRunTotalRuns = 1;
  810. // Outer loop: runs the full flow N times
  811. async function autoRunLoop(totalRuns) {
  812. if (autoRunActive) {
  813. await addLog('Auto run already in progress', 'warn');
  814. return;
  815. }
  816. clearStopRequest();
  817. autoRunActive = true;
  818. autoRunTotalRuns = totalRuns;
  819. let successCount = 0;
  820. let failCount = 0;
  821. await setState({ autoRunning: true });
  822. for (let run = 1; run <= totalRuns; run++) {
  823. autoRunCurrentRun = run;
  824. // Reset everything at the start of each run (keep VPS/mail settings)
  825. const prevState = await getState();
  826. const keepSettings = {
  827. vpsUrl: prevState.vpsUrl,
  828. mailProvider: prevState.mailProvider,
  829. tempApiUrl: prevState.tempApiUrl,
  830. inbucketHost: prevState.inbucketHost,
  831. inbucketMailbox: prevState.inbucketMailbox,
  832. duckToken: prevState.duckToken,
  833. autoRunning: true,
  834. };
  835. await resetState();
  836. await setState(keepSettings);
  837. // Tell side panel to reset all UI
  838. chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {});
  839. await sleepWithStop(500);
  840. await addLog(`=== Auto Run ${run}/${totalRuns} — Phase 1: Get OAuth link & open signup ===`, 'info');
  841. const status = (phase) => ({ type: 'AUTO_RUN_STATUS', payload: { phase, currentRun: run, totalRuns } });
  842. try {
  843. throwIfStopped();
  844. chrome.runtime.sendMessage(status('running')).catch(() => {});
  845. await executeStepAndWait(1, 2000);
  846. await executeStepAndWait(2, 2000);
  847. let emailReady = false;
  848. try {
  849. const duckEmail = await fetchDuckEmail({ generateNew: true });
  850. await addLog(`=== Run ${run}/${totalRuns} — Duck email ready: ${duckEmail} ===`, 'ok');
  851. emailReady = true;
  852. } catch (err) {
  853. await addLog(`Duck Mail auto-fetch failed: ${err.message}`, 'warn');
  854. }
  855. if (!emailReady) {
  856. await addLog(`=== Run ${run}/${totalRuns} PAUSED: Fetch Duck email or paste manually, then continue ===`, 'warn');
  857. chrome.runtime.sendMessage(status('waiting_email')).catch(() => {});
  858. // Wait for RESUME_AUTO_RUN — sets a promise that resumeAutoRun resolves
  859. await waitForResume();
  860. const resumedState = await getState();
  861. if (!resumedState.email) {
  862. await addLog('Cannot resume: no email address.', 'error');
  863. break;
  864. }
  865. }
  866. await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info');
  867. chrome.runtime.sendMessage(status('running')).catch(() => {});
  868. const signupTabId = await getTabId('signup-page');
  869. if (signupTabId) {
  870. await chrome.tabs.update(signupTabId, { active: true });
  871. }
  872. await executeStepAndWait(3, 3000);
  873. await executeStepAndWait(4, 2000);
  874. await executeStepAndWait(5, 3000);
  875. await executeStepAndWait(6, 3000);
  876. await executeStepAndWait(7, 2000);
  877. await executeStepAndWait(8, 2000);
  878. await executeStepAndWait(9, 1000);
  879. successCount++;
  880. await addLog(`=== Run ${run}/${totalRuns} COMPLETE! (${successCount} succeeded, ${failCount} failed) ===`, 'ok');
  881. } catch (err) {
  882. if (isStopError(err)) {
  883. await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn');
  884. chrome.runtime.sendMessage(status('stopped')).catch(() => {});
  885. break; // Only stop on explicit user stop
  886. } else {
  887. failCount++;
  888. await addLog(`Run ${run}/${totalRuns} failed: ${err.message} (${successCount} succeeded, ${failCount} failed)`, 'error');
  889. await addLog(`Skipping to next run...`, 'warn');
  890. continue; // Continue to next run on error
  891. }
  892. }
  893. }
  894. const attempted = autoRunCurrentRun;
  895. if (stopRequested) {
  896. await addLog(`=== Stopped after ${Math.max(0, attempted - 1)}/${autoRunTotalRuns} runs — ${successCount} succeeded, ${failCount} failed ===`, 'warn');
  897. chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: attempted, totalRuns: autoRunTotalRuns, successCount, failCount } }).catch(() => {});
  898. } else {
  899. await addLog(`=== All ${autoRunTotalRuns} runs finished — ${successCount} succeeded, ${failCount} failed ===`, successCount > 0 ? 'ok' : 'warn');
  900. chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: attempted, totalRuns: autoRunTotalRuns, successCount, failCount } }).catch(() => {});
  901. }
  902. autoRunActive = false;
  903. await setState({ autoRunning: false });
  904. clearStopRequest();
  905. }
  906. function waitForResume() {
  907. return new Promise((resolve, reject) => {
  908. throwIfStopped();
  909. resumeWaiter = { resolve, reject };
  910. });
  911. }
  912. async function resumeAutoRun() {
  913. throwIfStopped();
  914. const state = await getState();
  915. if (!state.email) {
  916. await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error');
  917. return;
  918. }
  919. if (resumeWaiter) {
  920. resumeWaiter.resolve();
  921. resumeWaiter = null;
  922. }
  923. }
  924. // ============================================================
  925. // Step 1: Get OAuth Link (via vps-panel.js)
  926. // ============================================================
  927. async function executeStep1(state) {
  928. if (!state.vpsUrl) {
  929. throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.');
  930. }
  931. await addLog(`Step 1: Opening VPS panel...`);
  932. await reuseOrCreateTab('vps-panel', state.vpsUrl, {
  933. inject: ['content/utils.js', 'content/vps-panel.js'],
  934. reloadIfSameUrl: true,
  935. });
  936. await sendToContentScript('vps-panel', {
  937. type: 'EXECUTE_STEP',
  938. step: 1,
  939. source: 'background',
  940. payload: {},
  941. });
  942. }
  943. // ============================================================
  944. // Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register)
  945. // ============================================================
  946. async function executeStep2(state) {
  947. if (!state.oauthUrl) {
  948. throw new Error('No OAuth URL. Complete step 1 first.');
  949. }
  950. await addLog(`Step 2: Opening auth URL...`);
  951. await reuseOrCreateTab('signup-page', state.oauthUrl);
  952. await sendToContentScript('signup-page', {
  953. type: 'EXECUTE_STEP',
  954. step: 2,
  955. source: 'background',
  956. payload: {},
  957. });
  958. }
  959. // ============================================================
  960. // Step 3: Fill Email & Password (via signup-page.js)
  961. // ============================================================
  962. async function executeStep3(state) {
  963. if (!state.email) {
  964. throw new Error('No email address. Paste email in Side Panel first.');
  965. }
  966. const password = state.customPassword || generatePassword();
  967. await setPasswordState(password);
  968. // Save account record
  969. const accounts = state.accounts || [];
  970. accounts.push({ email: state.email, password, createdAt: new Date().toISOString() });
  971. await setState({ accounts });
  972. await addLog(
  973. `Step 3: Filling email ${state.email}, password ${state.customPassword ? 'customized' : 'generated'} (${password.length} chars)`
  974. );
  975. await sendToContentScript('signup-page', {
  976. type: 'EXECUTE_STEP',
  977. step: 3,
  978. source: 'background',
  979. payload: { email: state.email, password },
  980. });
  981. }
  982. // ============================================================
  983. // Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js)
  984. // ============================================================
  985. async function getMailConfig(state) {
  986. const provider = state.mailProvider || 'temp-api';
  987. if (provider === 'temp-api') {
  988. let rawUrl = (state.tempApiUrl || '').trim();
  989. // Fallback: read directly from chrome.storage.local in case session is stale
  990. if (!rawUrl) {
  991. const local = await chrome.storage.local.get('tempApiUrl');
  992. rawUrl = (local.tempApiUrl || '').trim();
  993. }
  994. if (!rawUrl) {
  995. return { error: 'Temp Email API URL is empty. Please paste the full URL with JWT in Side Panel.' };
  996. }
  997. // Extract base origin and JWT from the URL (e.g. https://host/?jwt=xxx)
  998. let apiOrigin, jwt;
  999. try {
  1000. const parsed = new URL(rawUrl);
  1001. apiOrigin = parsed.origin;
  1002. jwt = parsed.searchParams.get('jwt');
  1003. } catch {
  1004. return { error: 'Temp Email API URL is invalid.' };
  1005. }
  1006. if (!jwt) {
  1007. return { error: 'Temp Email API URL has no jwt parameter.' };
  1008. }
  1009. return {
  1010. source: 'temp-api',
  1011. apiOrigin,
  1012. jwt,
  1013. label: 'Temp Email API',
  1014. useFetch: true, // signal that this provider uses fetch, not a tab
  1015. };
  1016. }
  1017. if (provider === '163') {
  1018. 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' };
  1019. }
  1020. if (provider === 'inbucket') {
  1021. const host = normalizeInbucketOrigin(state.inbucketHost);
  1022. const mailbox = (state.inbucketMailbox || '').trim();
  1023. if (!host) {
  1024. return { error: 'Inbucket host is empty or invalid.' };
  1025. }
  1026. if (!mailbox) {
  1027. return { error: 'Inbucket mailbox name is empty.' };
  1028. }
  1029. return {
  1030. source: 'inbucket-mail',
  1031. url: `${host}/m/${encodeURIComponent(mailbox)}/`,
  1032. label: `Inbucket Mailbox (${mailbox})`,
  1033. navigateOnReuse: true,
  1034. inject: ['content/utils.js', 'content/inbucket-mail.js'],
  1035. injectSource: 'inbucket-mail',
  1036. };
  1037. }
  1038. return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
  1039. }
  1040. function normalizeInbucketOrigin(rawValue) {
  1041. const value = (rawValue || '').trim();
  1042. if (!value) return '';
  1043. const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
  1044. try {
  1045. const parsed = new URL(candidate);
  1046. return parsed.origin;
  1047. } catch {
  1048. return '';
  1049. }
  1050. }
  1051. // ============================================================
  1052. // Temp Email API: fetch-based polling (no tab needed)
  1053. // ============================================================
  1054. function extractVerificationCodeFromText(text) {
  1055. // Pattern 1: Chinese format "代码为 370794" or "验证码...370794"
  1056. const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
  1057. if (matchCn) return matchCn[1];
  1058. // Pattern 2: English format "code is 370794" or "code: 370794"
  1059. const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
  1060. if (matchEn) return matchEn[1] || matchEn[2];
  1061. // Pattern 3: standalone 6-digit number
  1062. const match6 = text.match(/\b(\d{6})\b/);
  1063. if (match6) return match6[1];
  1064. return null;
  1065. }
  1066. function decodeMimeWord(encoded) {
  1067. // Decode =?charset?encoding?text?= (RFC 2047)
  1068. return encoded.replace(/=\?([^?]+)\?(B|Q)\?([^?]*)\?=/gi, (_, charset, enc, text) => {
  1069. if (enc.toUpperCase() === 'B') {
  1070. // Base64
  1071. try { return atob(text); } catch { return text; }
  1072. }
  1073. // Quoted-Printable
  1074. return text.replace(/_/g, ' ').replace(/=([0-9A-Fa-f]{2})/g, (__, hex) =>
  1075. String.fromCharCode(parseInt(hex, 16))
  1076. );
  1077. });
  1078. }
  1079. function decodeUtf8Bytes(str) {
  1080. // If the decoded string contains raw UTF-8 bytes, decode them
  1081. try { return decodeURIComponent(escape(str)); } catch { return str; }
  1082. }
  1083. function extractSubjectFromRaw(raw) {
  1084. // Extract Subject header (may span multiple lines with folding)
  1085. const match = raw.match(/^Subject:\s*([\s\S]*?)(?=\r?\n[^\s]|\r?\n\r?\n)/mi);
  1086. if (!match) return '';
  1087. // Unfold continuation lines and decode MIME words
  1088. const unfolded = match[1].replace(/\r?\n\s+/g, ' ').trim();
  1089. return decodeUtf8Bytes(decodeMimeWord(unfolded));
  1090. }
  1091. function extractFromHeader(raw) {
  1092. const match = raw.match(/^From:\s*([\s\S]*?)(?=\r?\n[^\s]|\r?\n\r?\n)/mi);
  1093. if (!match) return '';
  1094. return match[1].replace(/\r?\n\s+/g, ' ').trim().toLowerCase();
  1095. }
  1096. function extractPlainTextFromRaw(raw) {
  1097. // Try to extract the text/plain part from a multipart email
  1098. const plainMatch = raw.match(/Content-Type:\s*text\/plain[\s\S]*?\r?\n\r?\n([\s\S]*?)(?:\r?\n--|\r?\n$)/i);
  1099. if (plainMatch) {
  1100. let text = plainMatch[1];
  1101. // Decode quoted-printable
  1102. text = text.replace(/=\r?\n/g, '');
  1103. text = text.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
  1104. try { return decodeURIComponent(escape(text)); } catch { return text; }
  1105. }
  1106. return raw;
  1107. }
  1108. async function pollTempEmailApi(mail, step, payload) {
  1109. const { senderFilters, subjectFilters, maxAttempts, intervalMs, usedMailIds, usedCodes } = payload;
  1110. const usedIdSet = new Set((usedMailIds || []).map(String));
  1111. const usedCodeSet = new Set(usedCodes || []);
  1112. const headers = { 'Authorization': `Bearer ${mail.jwt}` };
  1113. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  1114. throwIfStopped();
  1115. await addLog(`Step ${step}: Polling Temp Email API... attempt ${attempt}/${maxAttempts}`);
  1116. try {
  1117. const resp = await fetch(`${mail.apiOrigin}/api/mails?limit=10&offset=0`, { headers });
  1118. if (!resp.ok) throw new Error(`API returned ${resp.status}`);
  1119. const data = await resp.json();
  1120. const mails = data.results || [];
  1121. for (const m of mails) {
  1122. if (usedIdSet.has(String(m.id))) continue;
  1123. const raw = m.raw || '';
  1124. const source = (m.source || '').toLowerCase();
  1125. const fromHeader = extractFromHeader(raw);
  1126. const subject = extractSubjectFromRaw(raw).toLowerCase();
  1127. const plainText = extractPlainTextFromRaw(raw);
  1128. // Match against both envelope sender and From header
  1129. const senderMatch = senderFilters.some(f => {
  1130. const fl = f.toLowerCase();
  1131. return source.includes(fl) || fromHeader.includes(fl);
  1132. });
  1133. const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()));
  1134. if (senderMatch || subjectMatch) {
  1135. const code = extractVerificationCodeFromText(subject + ' ' + plainText);
  1136. if (code) {
  1137. if (usedCodeSet.has(code)) {
  1138. console.log(LOG_PREFIX, `Step ${step}: Skipping already-used code ${code} (id: ${m.id})`);
  1139. continue;
  1140. }
  1141. await addLog(`Step ${step}: Code found from API: ${code} (subject: ${subject.slice(0, 40)})`, 'ok');
  1142. return { ok: true, code, emailTimestamp: Date.now(), mailId: String(m.id) };
  1143. }
  1144. }
  1145. }
  1146. } catch (err) {
  1147. await addLog(`Step ${step}: API fetch error: ${err.message}`, 'warn');
  1148. }
  1149. if (attempt < maxAttempts) {
  1150. await sleepWithStop(intervalMs);
  1151. }
  1152. }
  1153. throw new Error(
  1154. `No matching email found via Temp Email API after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
  1155. 'Email may be delayed.'
  1156. );
  1157. }
  1158. async function clickResendOnSignupPage(step) {
  1159. const signupTabId = await getTabId('signup-page');
  1160. if (!signupTabId) return;
  1161. await chrome.tabs.update(signupTabId, { active: true });
  1162. await sleepWithStop(500);
  1163. try {
  1164. await sendToContentScript('signup-page', {
  1165. type: 'CLICK_RESEND_EMAIL',
  1166. step,
  1167. source: 'background',
  1168. });
  1169. } catch (err) {
  1170. await addLog(`Step ${step}: Resend click skipped: ${err.message}`, 'warn');
  1171. }
  1172. }
  1173. async function executeStep4(state) {
  1174. // Click "重新发送电子邮件" on the signup page before polling
  1175. await clickResendOnSignupPage(4);
  1176. // Re-read state to ensure tempApiUrl is fresh (may have been saved after initial getState)
  1177. const freshState = await getState();
  1178. const mail = await getMailConfig(freshState);
  1179. if (mail.error) throw new Error(mail.error);
  1180. const pollPayload = {
  1181. filterAfterTimestamp: state.flowStartTime || 0,
  1182. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
  1183. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
  1184. targetEmail: state.email,
  1185. maxAttempts: 20,
  1186. intervalMs: 3000,
  1187. usedMailIds: state.usedMailIds || [],
  1188. usedCodes: state.usedCodes || [],
  1189. };
  1190. let result;
  1191. if (mail.useFetch) {
  1192. // Temp Email API: fetch directly, no tab needed
  1193. await addLog(`Step 4: Polling ${mail.label}...`);
  1194. result = await pollTempEmailApi(mail, 4, pollPayload);
  1195. } else {
  1196. await addLog(`Step 4: Opening ${mail.label}...`);
  1197. // For mail tabs, only create if not alive — don't navigate (preserves login session)
  1198. const alive = await isTabAlive(mail.source);
  1199. if (alive) {
  1200. if (mail.navigateOnReuse) {
  1201. await reuseOrCreateTab(mail.source, mail.url, {
  1202. inject: mail.inject,
  1203. injectSource: mail.injectSource,
  1204. });
  1205. } else {
  1206. const tabId = await getTabId(mail.source);
  1207. await chrome.tabs.update(tabId, { active: true });
  1208. }
  1209. } else {
  1210. await reuseOrCreateTab(mail.source, mail.url, {
  1211. inject: mail.inject,
  1212. injectSource: mail.injectSource,
  1213. });
  1214. }
  1215. result = await sendToContentScript(mail.source, {
  1216. type: 'POLL_EMAIL',
  1217. step: 4,
  1218. source: 'background',
  1219. payload: pollPayload,
  1220. });
  1221. }
  1222. if (result && result.error) {
  1223. throw new Error(result.error);
  1224. }
  1225. if (result && result.code) {
  1226. // Track used mail IDs and codes to avoid reusing stale ones
  1227. const usedMailIds = [...(state.usedMailIds || [])];
  1228. const usedCodes = [...(state.usedCodes || [])];
  1229. if (result.mailId) usedMailIds.push(result.mailId);
  1230. usedCodes.push(result.code);
  1231. await setState({ lastEmailTimestamp: result.emailTimestamp, usedMailIds, usedCodes });
  1232. await addLog(`Step 4: Got verification code: ${result.code}`);
  1233. // Switch to signup tab and fill code
  1234. const signupTabId = await getTabId('signup-page');
  1235. if (signupTabId) {
  1236. await chrome.tabs.update(signupTabId, { active: true });
  1237. await sendToContentScript('signup-page', {
  1238. type: 'FILL_CODE',
  1239. step: 4,
  1240. source: 'background',
  1241. payload: { code: result.code },
  1242. });
  1243. } else {
  1244. throw new Error('Signup page tab was closed. Cannot fill verification code.');
  1245. }
  1246. }
  1247. }
  1248. // ============================================================
  1249. // Step 5: Fill Name & Birthday (via signup-page.js)
  1250. // ============================================================
  1251. async function executeStep5(state) {
  1252. const { firstName, lastName } = generateRandomName();
  1253. const { year, month, day } = generateRandomBirthday();
  1254. // Save birthday data to state for potential reuse on about-you page
  1255. await setState({ birthYear: year, birthMonth: month, birthDay: day, fullName: `${firstName} ${lastName}` });
  1256. await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
  1257. await sendToContentScript('signup-page', {
  1258. type: 'EXECUTE_STEP',
  1259. step: 5,
  1260. source: 'background',
  1261. payload: { firstName, lastName, year, month, day },
  1262. });
  1263. }
  1264. // ============================================================
  1265. // Step 6: Login ChatGPT (Background opens tab, chatgpt.js handles login)
  1266. // ============================================================
  1267. async function executeStep6(state) {
  1268. if (!state.oauthUrl) {
  1269. throw new Error('No OAuth URL. Complete step 1 first.');
  1270. }
  1271. if (!state.email) {
  1272. throw new Error('No email. Complete step 3 first.');
  1273. }
  1274. await addLog(`Step 6: Opening OAuth URL for login...`);
  1275. // Reuse the signup-page tab — navigate it to the OAuth URL
  1276. await reuseOrCreateTab('signup-page', state.oauthUrl);
  1277. // signup-page.js will inject (same auth.openai.com domain) and handle login
  1278. await sendToContentScript('signup-page', {
  1279. type: 'EXECUTE_STEP',
  1280. step: 6,
  1281. source: 'background',
  1282. payload: { email: state.email, password: state.password },
  1283. });
  1284. }
  1285. // ============================================================
  1286. // Step 7: Get Login Verification Code (qq-mail.js polls, then fills in chatgpt.js)
  1287. // ============================================================
  1288. async function executeStep7(state) {
  1289. // Check if page landed on add-phone — means phone verification required, skip this run
  1290. const signupTabIdCheck = await getTabId('signup-page');
  1291. if (signupTabIdCheck) {
  1292. try {
  1293. const tab = await chrome.tabs.get(signupTabIdCheck);
  1294. if (tab.url && (tab.url.includes('add-phone') || tab.url.includes('/phone'))) {
  1295. throw new Error('Phone verification required — skipping this account.');
  1296. }
  1297. } catch (err) {
  1298. if (err.message.includes('Phone verification')) throw err;
  1299. }
  1300. }
  1301. // Check if the page is on /about-you (birthday not filled during signup)
  1302. // and handle it before proceeding with verification code polling
  1303. try {
  1304. const aboutYouResult = await sendToContentScript('signup-page', {
  1305. type: 'HANDLE_ABOUT_YOU',
  1306. step: 7,
  1307. source: 'background',
  1308. payload: {
  1309. year: state.birthYear,
  1310. month: state.birthMonth,
  1311. day: state.birthDay,
  1312. fullName: state.fullName,
  1313. },
  1314. });
  1315. if (aboutYouResult && aboutYouResult.handled) {
  1316. await addLog('Step 7: Handled about-you page, waiting for next page...', 'info');
  1317. await sleep(3000);
  1318. }
  1319. } catch (e) {
  1320. // Not on about-you page, continue normally
  1321. }
  1322. // Click "重新发送电子邮件" on the auth page before polling
  1323. await clickResendOnSignupPage(7);
  1324. const mail = await getMailConfig(state);
  1325. if (mail.error) throw new Error(mail.error);
  1326. // Re-read state to get latest usedMailIds/usedCodes
  1327. const freshState = await getState();
  1328. const pollPayload = {
  1329. filterAfterTimestamp: freshState.lastEmailTimestamp || freshState.flowStartTime || 0,
  1330. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
  1331. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
  1332. targetEmail: freshState.email,
  1333. maxAttempts: 20,
  1334. intervalMs: 3000,
  1335. usedMailIds: freshState.usedMailIds || [],
  1336. usedCodes: freshState.usedCodes || [],
  1337. };
  1338. let result;
  1339. if (mail.useFetch) {
  1340. await addLog(`Step 7: Polling ${mail.label}...`);
  1341. result = await pollTempEmailApi(mail, 7, pollPayload);
  1342. } else {
  1343. await addLog(`Step 7: Opening ${mail.label}...`);
  1344. const alive = await isTabAlive(mail.source);
  1345. if (alive) {
  1346. if (mail.navigateOnReuse) {
  1347. await reuseOrCreateTab(mail.source, mail.url, {
  1348. inject: mail.inject,
  1349. injectSource: mail.injectSource,
  1350. });
  1351. } else {
  1352. const tabId = await getTabId(mail.source);
  1353. await chrome.tabs.update(tabId, { active: true });
  1354. }
  1355. } else {
  1356. await reuseOrCreateTab(mail.source, mail.url, {
  1357. inject: mail.inject,
  1358. injectSource: mail.injectSource,
  1359. });
  1360. }
  1361. result = await sendToContentScript(mail.source, {
  1362. type: 'POLL_EMAIL',
  1363. step: 7,
  1364. source: 'background',
  1365. payload: pollPayload,
  1366. });
  1367. }
  1368. if (result && result.error) {
  1369. throw new Error(result.error);
  1370. }
  1371. if (result && result.code) {
  1372. // Track used mail IDs and codes
  1373. const usedMailIds = [...(freshState.usedMailIds || [])];
  1374. const usedCodes = [...(freshState.usedCodes || [])];
  1375. if (result.mailId) usedMailIds.push(result.mailId);
  1376. usedCodes.push(result.code);
  1377. await setState({ usedMailIds, usedCodes });
  1378. await addLog(`Step 7: Got login verification code: ${result.code}`);
  1379. // Switch to signup/auth tab and fill code
  1380. const signupTabId = await getTabId('signup-page');
  1381. if (signupTabId) {
  1382. await chrome.tabs.update(signupTabId, { active: true });
  1383. await sendToContentScript('signup-page', {
  1384. type: 'FILL_CODE',
  1385. step: 7,
  1386. source: 'background',
  1387. payload: { code: result.code },
  1388. });
  1389. } else {
  1390. throw new Error('Auth page tab was closed. Cannot fill verification code.');
  1391. }
  1392. }
  1393. }
  1394. // ============================================================
  1395. // Step 8: Complete OAuth (auto click + localhost listener)
  1396. // ============================================================
  1397. let webNavListener = null;
  1398. async function executeStep8(state) {
  1399. if (!state.oauthUrl) {
  1400. throw new Error('No OAuth URL. Complete step 1 first.');
  1401. }
  1402. // Check if the signup tab already redirected to localhost before listener setup
  1403. const signupTabIdEarly = await getTabId('signup-page');
  1404. if (signupTabIdEarly) {
  1405. try {
  1406. const tab = await chrome.tabs.get(signupTabIdEarly);
  1407. if (tab.url && (tab.url.startsWith('http://localhost') || tab.url.startsWith('http://127.0.0.1'))) {
  1408. await addLog(`Step 8: Localhost redirect already captured: ${tab.url}`, 'ok');
  1409. await setState({ localhostUrl: tab.url });
  1410. broadcastDataUpdate({ localhostUrl: tab.url });
  1411. return;
  1412. }
  1413. } catch {}
  1414. }
  1415. await addLog('Step 8: Setting up localhost redirect listener...');
  1416. // Register webNavigation listener (scoped to this step)
  1417. return new Promise((resolve, reject) => {
  1418. let resolved = false;
  1419. const isLocalhostUrl = (url) =>
  1420. url && (url.startsWith('http://localhost') || url.startsWith('http://127.0.0.1'));
  1421. const cleanupListeners = () => {
  1422. if (webNavListener) {
  1423. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  1424. chrome.webNavigation.onCommitted.removeListener(webNavListener);
  1425. chrome.webNavigation.onErrorOccurred.removeListener(webNavListener);
  1426. webNavListener = null;
  1427. }
  1428. };
  1429. const captureLocalhostUrl = (url) => {
  1430. if (resolved) return;
  1431. resolved = true;
  1432. cleanupListeners();
  1433. clearTimeout(timeout);
  1434. setState({ localhostUrl: url }).then(() => {
  1435. addLog(`Step 8: Captured localhost URL: ${url}`, 'ok');
  1436. setStepStatus(8, 'completed');
  1437. notifyStepComplete(8, { localhostUrl: url });
  1438. broadcastDataUpdate({ localhostUrl: url });
  1439. resolve();
  1440. });
  1441. };
  1442. const timeout = setTimeout(() => {
  1443. cleanupListeners();
  1444. reject(new Error('Localhost redirect not captured after 120s. Step 8 click may have been blocked.'));
  1445. }, 120000);
  1446. webNavListener = (details) => {
  1447. if (details.frameId === 0 && isLocalhostUrl(details.url)) {
  1448. console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`);
  1449. captureLocalhostUrl(details.url);
  1450. }
  1451. };
  1452. chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
  1453. chrome.webNavigation.onCommitted.addListener(webNavListener);
  1454. chrome.webNavigation.onErrorOccurred.addListener(webNavListener);
  1455. // After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
  1456. // with a "继续" button. Content script clicks it directly (no debugger needed).
  1457. (async () => {
  1458. try {
  1459. let signupTabId = await getTabId('signup-page');
  1460. if (signupTabId) {
  1461. await chrome.tabs.update(signupTabId, { active: true });
  1462. await addLog('Step 8: Switched to auth page...');
  1463. } else {
  1464. signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
  1465. await addLog('Step 8: Auth tab reopened...');
  1466. }
  1467. const clickResult = await sendToContentScript('signup-page', {
  1468. type: 'STEP8_FIND_AND_CLICK',
  1469. source: 'background',
  1470. payload: {},
  1471. });
  1472. if (clickResult?.error) {
  1473. throw new Error(clickResult.error);
  1474. }
  1475. await addLog('Step 8: "继续" button clicked, waiting for redirect...');
  1476. // Poll tab URL in case webNavigation listeners missed the redirect
  1477. for (let i = 0; i < 30 && !resolved; i++) {
  1478. await new Promise(r => setTimeout(r, 1000));
  1479. try {
  1480. const tab = await chrome.tabs.get(signupTabId);
  1481. if (isLocalhostUrl(tab.url)) {
  1482. captureLocalhostUrl(tab.url);
  1483. break;
  1484. }
  1485. } catch { break; }
  1486. }
  1487. } catch (err) {
  1488. clearTimeout(timeout);
  1489. cleanupListeners();
  1490. reject(err);
  1491. }
  1492. })();
  1493. });
  1494. }
  1495. // ============================================================
  1496. // Step 9: VPS Verify (via vps-panel.js)
  1497. // ============================================================
  1498. async function executeStep9(state) {
  1499. if (!state.localhostUrl) {
  1500. throw new Error('No localhost URL. Complete step 8 first.');
  1501. }
  1502. if (!state.vpsUrl) {
  1503. throw new Error('VPS URL not set. Please enter VPS URL in the side panel.');
  1504. }
  1505. await addLog('Step 9: Opening VPS panel...');
  1506. let tabId = await getTabId('vps-panel');
  1507. const alive = tabId && await isTabAlive('vps-panel');
  1508. if (!alive) {
  1509. // Create new tab in the automation window
  1510. const wid = await ensureAutomationWindowId();
  1511. const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true, windowId: wid });
  1512. tabId = tab.id;
  1513. await new Promise(resolve => {
  1514. const listener = (tid, info) => {
  1515. if (tid === tabId && info.status === 'complete') {
  1516. chrome.tabs.onUpdated.removeListener(listener);
  1517. resolve();
  1518. }
  1519. };
  1520. chrome.tabs.onUpdated.addListener(listener);
  1521. });
  1522. } else {
  1523. await chrome.tabs.update(tabId, { active: true });
  1524. }
  1525. // Inject scripts directly and wait for them to be ready
  1526. await chrome.scripting.executeScript({
  1527. target: { tabId },
  1528. files: ['content/utils.js', 'content/vps-panel.js'],
  1529. });
  1530. await new Promise(r => setTimeout(r, 1000));
  1531. // Send command directly — bypass queue/ready mechanism
  1532. await addLog(`Step 9: Filling callback URL...`);
  1533. await chrome.tabs.sendMessage(tabId, {
  1534. type: 'EXECUTE_STEP',
  1535. step: 9,
  1536. source: 'background',
  1537. payload: { localhostUrl: state.localhostUrl },
  1538. });
  1539. }
  1540. // ============================================================
  1541. // Open Side Panel on extension icon click
  1542. // ============================================================
  1543. chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });