background.js 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760
  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, successCount } });
  842. try {
  843. throwIfStopped();
  844. chrome.runtime.sendMessage(status('running')).catch(() => {});
  845. await executeStepAndWait(1, 2000);
  846. await executeStepAndWait(2, 2000);
  847. const duckEmail = await fetchDuckEmail({ generateNew: true });
  848. await addLog(`=== Run ${run}/${totalRuns} — Duck email ready: ${duckEmail} ===`, 'ok');
  849. await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info');
  850. chrome.runtime.sendMessage(status('running')).catch(() => {});
  851. const signupTabId = await getTabId('signup-page');
  852. if (signupTabId) {
  853. await chrome.tabs.update(signupTabId, { active: true });
  854. }
  855. await executeStepAndWait(3, 3000);
  856. await executeStepAndWait(4, 2000);
  857. await executeStepAndWait(5, 3000);
  858. await executeStepAndWait(6, 3000);
  859. await executeStepAndWait(7, 2000);
  860. // After login+verification, check if page landed on add-phone (only expected after step 5, not here)
  861. const phoneCheckTabId = await getTabId('signup-page');
  862. if (phoneCheckTabId) {
  863. try {
  864. const phoneTab = await chrome.tabs.get(phoneCheckTabId);
  865. if (phoneTab.url && (phoneTab.url.includes('add-phone') || phoneTab.url.includes('/phone'))) {
  866. throw new Error('PHONE_VERIFY_REQUIRED');
  867. }
  868. } catch (e) {
  869. if (e.message === 'PHONE_VERIFY_REQUIRED') throw e;
  870. }
  871. }
  872. await executeStepAndWait(8, 2000);
  873. await executeStepAndWait(9, 1000);
  874. successCount++;
  875. await addLog(`=== Run ${run}/${totalRuns} COMPLETE! (${successCount} succeeded, ${failCount} failed) ===`, 'ok');
  876. } catch (err) {
  877. if (isStopError(err)) {
  878. await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn');
  879. chrome.runtime.sendMessage(status('stopped')).catch(() => {});
  880. break; // Only stop on explicit user stop
  881. } else {
  882. failCount++;
  883. if (err.message === 'PHONE_VERIFY_REQUIRED') {
  884. await addLog(`Run ${run}/${totalRuns} failed: Phone verification required — sleeping 30s before next run (${successCount} succeeded, ${failCount} failed)`, 'error');
  885. await sleepWithStop(30000);
  886. } else {
  887. await addLog(`Run ${run}/${totalRuns} failed: ${err.message} (${successCount} succeeded, ${failCount} failed)`, 'error');
  888. }
  889. await addLog(`Skipping to next run...`, 'warn');
  890. continue;
  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 the page is on /about-you (birthday not filled during signup)
  1290. // and handle it before proceeding with verification code polling
  1291. try {
  1292. const aboutYouResult = await sendToContentScript('signup-page', {
  1293. type: 'HANDLE_ABOUT_YOU',
  1294. step: 7,
  1295. source: 'background',
  1296. payload: {
  1297. year: state.birthYear,
  1298. month: state.birthMonth,
  1299. day: state.birthDay,
  1300. fullName: state.fullName,
  1301. },
  1302. });
  1303. if (aboutYouResult && aboutYouResult.handled) {
  1304. await addLog('Step 7: Handled about-you page, waiting for next page...', 'info');
  1305. await sleep(3000);
  1306. }
  1307. } catch (e) {
  1308. // Not on about-you page, continue normally
  1309. }
  1310. // Click "重新发送电子邮件" on the auth page before polling
  1311. await clickResendOnSignupPage(7);
  1312. const mail = await getMailConfig(state);
  1313. if (mail.error) throw new Error(mail.error);
  1314. // Re-read state to get latest usedMailIds/usedCodes
  1315. const freshState = await getState();
  1316. const pollPayload = {
  1317. filterAfterTimestamp: freshState.lastEmailTimestamp || freshState.flowStartTime || 0,
  1318. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
  1319. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
  1320. targetEmail: freshState.email,
  1321. maxAttempts: 20,
  1322. intervalMs: 3000,
  1323. usedMailIds: freshState.usedMailIds || [],
  1324. usedCodes: freshState.usedCodes || [],
  1325. };
  1326. let result;
  1327. if (mail.useFetch) {
  1328. await addLog(`Step 7: Polling ${mail.label}...`);
  1329. result = await pollTempEmailApi(mail, 7, pollPayload);
  1330. } else {
  1331. await addLog(`Step 7: Opening ${mail.label}...`);
  1332. const alive = await isTabAlive(mail.source);
  1333. if (alive) {
  1334. if (mail.navigateOnReuse) {
  1335. await reuseOrCreateTab(mail.source, mail.url, {
  1336. inject: mail.inject,
  1337. injectSource: mail.injectSource,
  1338. });
  1339. } else {
  1340. const tabId = await getTabId(mail.source);
  1341. await chrome.tabs.update(tabId, { active: true });
  1342. }
  1343. } else {
  1344. await reuseOrCreateTab(mail.source, mail.url, {
  1345. inject: mail.inject,
  1346. injectSource: mail.injectSource,
  1347. });
  1348. }
  1349. result = await sendToContentScript(mail.source, {
  1350. type: 'POLL_EMAIL',
  1351. step: 7,
  1352. source: 'background',
  1353. payload: pollPayload,
  1354. });
  1355. }
  1356. if (result && result.error) {
  1357. throw new Error(result.error);
  1358. }
  1359. if (result && result.code) {
  1360. // Track used mail IDs and codes
  1361. const usedMailIds = [...(freshState.usedMailIds || [])];
  1362. const usedCodes = [...(freshState.usedCodes || [])];
  1363. if (result.mailId) usedMailIds.push(result.mailId);
  1364. usedCodes.push(result.code);
  1365. await setState({ usedMailIds, usedCodes });
  1366. await addLog(`Step 7: Got login verification code: ${result.code}`);
  1367. // Switch to signup/auth tab and fill code
  1368. const signupTabId = await getTabId('signup-page');
  1369. if (signupTabId) {
  1370. await chrome.tabs.update(signupTabId, { active: true });
  1371. await sendToContentScript('signup-page', {
  1372. type: 'FILL_CODE',
  1373. step: 7,
  1374. source: 'background',
  1375. payload: { code: result.code },
  1376. });
  1377. } else {
  1378. throw new Error('Auth page tab was closed. Cannot fill verification code.');
  1379. }
  1380. }
  1381. }
  1382. // ============================================================
  1383. // Step 8: Complete OAuth (auto click + localhost listener)
  1384. // ============================================================
  1385. let webNavListener = null;
  1386. async function executeStep8(state) {
  1387. if (!state.oauthUrl) {
  1388. throw new Error('No OAuth URL. Complete step 1 first.');
  1389. }
  1390. // Check if the signup tab already redirected to localhost before listener setup
  1391. const signupTabIdEarly = await getTabId('signup-page');
  1392. if (signupTabIdEarly) {
  1393. try {
  1394. const tab = await chrome.tabs.get(signupTabIdEarly);
  1395. if (tab.url && (tab.url.startsWith('http://localhost') || tab.url.startsWith('http://127.0.0.1'))) {
  1396. await addLog(`Step 8: Localhost redirect already captured: ${tab.url}`, 'ok');
  1397. await setState({ localhostUrl: tab.url });
  1398. broadcastDataUpdate({ localhostUrl: tab.url });
  1399. return;
  1400. }
  1401. } catch {}
  1402. }
  1403. await addLog('Step 8: Setting up localhost redirect listener...');
  1404. // Register webNavigation listener (scoped to this step)
  1405. return new Promise((resolve, reject) => {
  1406. let resolved = false;
  1407. const isLocalhostUrl = (url) =>
  1408. url && (url.startsWith('http://localhost') || url.startsWith('http://127.0.0.1'));
  1409. const cleanupListeners = () => {
  1410. if (webNavListener) {
  1411. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  1412. chrome.webNavigation.onCommitted.removeListener(webNavListener);
  1413. chrome.webNavigation.onErrorOccurred.removeListener(webNavListener);
  1414. webNavListener = null;
  1415. }
  1416. };
  1417. const captureLocalhostUrl = (url) => {
  1418. if (resolved) return;
  1419. resolved = true;
  1420. cleanupListeners();
  1421. clearTimeout(timeout);
  1422. setState({ localhostUrl: url }).then(() => {
  1423. addLog(`Step 8: Captured localhost URL: ${url}`, 'ok');
  1424. setStepStatus(8, 'completed');
  1425. notifyStepComplete(8, { localhostUrl: url });
  1426. broadcastDataUpdate({ localhostUrl: url });
  1427. resolve();
  1428. });
  1429. };
  1430. const timeout = setTimeout(() => {
  1431. cleanupListeners();
  1432. reject(new Error('Localhost redirect not captured after 120s. Step 8 click may have been blocked.'));
  1433. }, 120000);
  1434. webNavListener = (details) => {
  1435. if (details.frameId === 0 && isLocalhostUrl(details.url)) {
  1436. console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`);
  1437. captureLocalhostUrl(details.url);
  1438. }
  1439. };
  1440. chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
  1441. chrome.webNavigation.onCommitted.addListener(webNavListener);
  1442. chrome.webNavigation.onErrorOccurred.addListener(webNavListener);
  1443. // After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
  1444. // with a "继续" button. Content script clicks it directly (no debugger needed).
  1445. (async () => {
  1446. try {
  1447. let signupTabId = await getTabId('signup-page');
  1448. if (signupTabId) {
  1449. await chrome.tabs.update(signupTabId, { active: true });
  1450. await addLog('Step 8: Switched to auth page...');
  1451. } else {
  1452. signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
  1453. await addLog('Step 8: Auth tab reopened...');
  1454. }
  1455. const clickResult = await sendToContentScript('signup-page', {
  1456. type: 'STEP8_FIND_AND_CLICK',
  1457. source: 'background',
  1458. payload: {},
  1459. });
  1460. if (clickResult?.error) {
  1461. throw new Error(clickResult.error);
  1462. }
  1463. await addLog('Step 8: "继续" button clicked, waiting for redirect...');
  1464. // Poll tab URL in case webNavigation listeners missed the redirect
  1465. for (let i = 0; i < 30 && !resolved; i++) {
  1466. await new Promise(r => setTimeout(r, 1000));
  1467. try {
  1468. const tab = await chrome.tabs.get(signupTabId);
  1469. if (isLocalhostUrl(tab.url)) {
  1470. captureLocalhostUrl(tab.url);
  1471. break;
  1472. }
  1473. } catch { break; }
  1474. }
  1475. } catch (err) {
  1476. clearTimeout(timeout);
  1477. cleanupListeners();
  1478. reject(err);
  1479. }
  1480. })();
  1481. });
  1482. }
  1483. // ============================================================
  1484. // Step 9: VPS Verify (via vps-panel.js)
  1485. // ============================================================
  1486. async function executeStep9(state) {
  1487. if (!state.localhostUrl) {
  1488. throw new Error('No localhost URL. Complete step 8 first.');
  1489. }
  1490. if (!state.vpsUrl) {
  1491. throw new Error('VPS URL not set. Please enter VPS URL in the side panel.');
  1492. }
  1493. await addLog('Step 9: Opening VPS panel...');
  1494. let tabId = await getTabId('vps-panel');
  1495. const alive = tabId && await isTabAlive('vps-panel');
  1496. if (!alive) {
  1497. // Create new tab in the automation window
  1498. const wid = await ensureAutomationWindowId();
  1499. const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true, windowId: wid });
  1500. tabId = tab.id;
  1501. await new Promise(resolve => {
  1502. const listener = (tid, info) => {
  1503. if (tid === tabId && info.status === 'complete') {
  1504. chrome.tabs.onUpdated.removeListener(listener);
  1505. resolve();
  1506. }
  1507. };
  1508. chrome.tabs.onUpdated.addListener(listener);
  1509. });
  1510. } else {
  1511. await chrome.tabs.update(tabId, { active: true });
  1512. }
  1513. // Inject scripts directly and wait for them to be ready
  1514. await chrome.scripting.executeScript({
  1515. target: { tabId },
  1516. files: ['content/utils.js', 'content/vps-panel.js'],
  1517. });
  1518. await new Promise(r => setTimeout(r, 1000));
  1519. // Send command directly — bypass queue/ready mechanism
  1520. await addLog(`Step 9: Filling callback URL...`);
  1521. await chrome.tabs.sendMessage(tabId, {
  1522. type: 'EXECUTE_STEP',
  1523. step: 9,
  1524. source: 'background',
  1525. payload: { localhostUrl: state.localhostUrl },
  1526. });
  1527. }
  1528. // ============================================================
  1529. // Open Side Panel on extension icon click
  1530. // ============================================================
  1531. chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });