background.js 57 KB

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