background.js 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740
  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.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
  558. if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
  559. await setState(updates);
  560. return { ok: true };
  561. }
  562. // Side panel data updates
  563. case 'SAVE_EMAIL': {
  564. await setEmailState(message.payload.email);
  565. return { ok: true, email: message.payload.email };
  566. }
  567. case 'FETCH_DUCK_EMAIL': {
  568. clearStopRequest();
  569. const email = await fetchDuckEmail(message.payload || {});
  570. return { ok: true, email };
  571. }
  572. case 'STOP_FLOW': {
  573. await requestStop();
  574. return { ok: true };
  575. }
  576. default:
  577. console.warn(LOG_PREFIX, `Unknown message type: ${message.type}`);
  578. return { error: `Unknown message type: ${message.type}` };
  579. }
  580. }
  581. // ============================================================
  582. // Step Data Handlers
  583. // ============================================================
  584. async function handleStepData(step, payload) {
  585. switch (step) {
  586. case 1:
  587. if (payload.oauthUrl) {
  588. await setState({ oauthUrl: payload.oauthUrl });
  589. broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
  590. }
  591. break;
  592. case 3:
  593. if (payload.email) await setEmailState(payload.email);
  594. break;
  595. case 4:
  596. if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp });
  597. break;
  598. case 8:
  599. if (payload.localhostUrl) {
  600. await setState({ localhostUrl: payload.localhostUrl });
  601. broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
  602. }
  603. break;
  604. }
  605. }
  606. // ============================================================
  607. // Step Completion Waiting
  608. // ============================================================
  609. // Map of step -> { resolve, reject } for waiting on step completion
  610. const stepWaiters = new Map();
  611. let resumeWaiter = null;
  612. function waitForStepComplete(step, timeoutMs = 120000) {
  613. return new Promise((resolve, reject) => {
  614. throwIfStopped();
  615. const timer = setTimeout(() => {
  616. stepWaiters.delete(step);
  617. reject(new Error(`Step ${step} timed out after ${timeoutMs / 1000}s`));
  618. }, timeoutMs);
  619. stepWaiters.set(step, {
  620. resolve: (data) => { clearTimeout(timer); stepWaiters.delete(step); resolve(data); },
  621. reject: (err) => { clearTimeout(timer); stepWaiters.delete(step); reject(err); },
  622. });
  623. });
  624. }
  625. function notifyStepComplete(step, payload) {
  626. const waiter = stepWaiters.get(step);
  627. if (waiter) waiter.resolve(payload);
  628. }
  629. function notifyStepError(step, error) {
  630. const waiter = stepWaiters.get(step);
  631. if (waiter) waiter.reject(new Error(error));
  632. }
  633. async function markRunningStepsStopped() {
  634. const state = await getState();
  635. const runningSteps = Object.entries(state.stepStatuses || {})
  636. .filter(([, status]) => status === 'running')
  637. .map(([step]) => Number(step));
  638. for (const step of runningSteps) {
  639. await setStepStatus(step, 'stopped');
  640. }
  641. }
  642. async function requestStop() {
  643. if (stopRequested) return;
  644. stopRequested = true;
  645. cancelPendingCommands();
  646. if (webNavListener) {
  647. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  648. webNavListener = null;
  649. }
  650. await addLog('Stop requested. Cancelling current operations...', 'warn');
  651. await broadcastStopToContentScripts();
  652. for (const waiter of stepWaiters.values()) {
  653. waiter.reject(new Error(STOP_ERROR_MESSAGE));
  654. }
  655. stepWaiters.clear();
  656. if (resumeWaiter) {
  657. resumeWaiter.reject(new Error(STOP_ERROR_MESSAGE));
  658. resumeWaiter = null;
  659. }
  660. await markRunningStepsStopped();
  661. autoRunActive = false;
  662. await setState({ autoRunning: false });
  663. chrome.runtime.sendMessage({
  664. type: 'AUTO_RUN_STATUS',
  665. payload: { phase: 'stopped', currentRun: autoRunCurrentRun, totalRuns: autoRunTotalRuns },
  666. }).catch(() => {});
  667. }
  668. // ============================================================
  669. // Step Execution
  670. // ============================================================
  671. async function executeStep(step) {
  672. console.log(LOG_PREFIX, `Executing step ${step}`);
  673. throwIfStopped();
  674. await setStepStatus(step, 'running');
  675. await addLog(`Step ${step} started`);
  676. await humanStepDelay();
  677. const state = await getState();
  678. // Set flow start time on first step
  679. if (step === 1 && !state.flowStartTime) {
  680. await setState({ flowStartTime: Date.now() });
  681. }
  682. try {
  683. switch (step) {
  684. case 1: await executeStep1(state); break;
  685. case 2: await executeStep2(state); break;
  686. case 3: await executeStep3(state); break;
  687. case 4: await executeStep4(state); break;
  688. case 5: await executeStep5(state); break;
  689. case 6: await executeStep6(state); break;
  690. case 7: await executeStep7(state); break;
  691. case 8: await executeStep8(state); break;
  692. case 9: await executeStep9(state); break;
  693. default:
  694. throw new Error(`Unknown step: ${step}`);
  695. }
  696. } catch (err) {
  697. if (isStopError(err)) {
  698. await setStepStatus(step, 'stopped');
  699. await addLog(`Step ${step} stopped by user`, 'warn');
  700. throw err;
  701. }
  702. await setStepStatus(step, 'failed');
  703. await addLog(`Step ${step} failed: ${err.message}`, 'error');
  704. throw err;
  705. }
  706. }
  707. /**
  708. * Execute a step and wait for it to complete before returning.
  709. * @param {number} step
  710. * @param {number} delayAfter - ms to wait after completion (for page transitions)
  711. */
  712. async function executeStepAndWait(step, delayAfter = 2000) {
  713. throwIfStopped();
  714. const promise = waitForStepComplete(step, 120000);
  715. await executeStep(step);
  716. await promise;
  717. // Extra delay for page transitions / DOM updates
  718. if (delayAfter > 0) {
  719. await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200));
  720. }
  721. }
  722. async function fetchDuckEmail(options = {}) {
  723. throwIfStopped();
  724. // Try API-based generation first (no tab needed)
  725. const state = await getState();
  726. let token = state.duckToken;
  727. if (token) {
  728. try {
  729. const email = await generateDuckEmailViaApi(token);
  730. await setEmailState(email);
  731. await addLog(`Duck Mail: Generated ${email} (via API, no tab)`, 'ok');
  732. return email;
  733. } catch (err) {
  734. await addLog(`Duck Mail: API generation failed: ${err.message}, falling back to page...`, 'warn');
  735. // Token may be expired, clear it so we re-extract
  736. token = null;
  737. await setState({ duckToken: null });
  738. }
  739. }
  740. // Fallback: open page, extract token for future use, then generate
  741. await addLog('Duck Mail: Opening page to extract token...');
  742. await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL);
  743. // Try to extract and save the access token
  744. try {
  745. const tokenResult = await sendToContentScript('duck-mail', {
  746. type: 'EXTRACT_DUCK_TOKEN',
  747. source: 'background',
  748. payload: {},
  749. });
  750. if (tokenResult?.token) {
  751. token = tokenResult.token;
  752. await setState({ duckToken: token });
  753. await addLog(`Duck Mail: Token saved for ${tokenResult.username}, future runs won't need page`, 'ok');
  754. // Now use API to generate
  755. const email = await generateDuckEmailViaApi(token);
  756. await setEmailState(email);
  757. await addLog(`Duck Mail: Generated ${email} (via API)`, 'ok');
  758. return email;
  759. }
  760. } catch (err) {
  761. await addLog(`Duck Mail: Token extraction failed: ${err.message}, using page method`, 'warn');
  762. }
  763. // Final fallback: use the old page-based method
  764. const result = await sendToContentScript('duck-mail', {
  765. type: 'FETCH_DUCK_EMAIL',
  766. source: 'background',
  767. payload: { generateNew: true },
  768. });
  769. if (result?.error) throw new Error(result.error);
  770. if (!result?.email) throw new Error('Duck email not returned.');
  771. await setEmailState(result.email);
  772. await addLog(`Duck Mail: ${result.generated ? 'Generated' : 'Loaded'} ${result.email} (via page)`, 'ok');
  773. return result.email;
  774. }
  775. async function generateDuckEmailViaApi(token) {
  776. const resp = await fetch('https://quack.duckduckgo.com/api/email/addresses', {
  777. method: 'POST',
  778. headers: {
  779. 'Authorization': `Bearer ${token}`,
  780. 'Content-Type': 'application/json',
  781. },
  782. });
  783. if (!resp.ok) {
  784. throw new Error(`DuckDuckGo API returned ${resp.status}`);
  785. }
  786. const data = await resp.json();
  787. // Response: { address: "random-words" }
  788. const address = data?.address;
  789. if (!address) throw new Error('No address in API response');
  790. return `${address}@duck.com`;
  791. }
  792. // ============================================================
  793. // Auto Run Flow
  794. // ============================================================
  795. let autoRunActive = false;
  796. let autoRunCurrentRun = 0;
  797. let autoRunTotalRuns = 1;
  798. // Outer loop: runs the full flow N times
  799. async function autoRunLoop(totalRuns) {
  800. if (autoRunActive) {
  801. await addLog('Auto run already in progress', 'warn');
  802. return;
  803. }
  804. clearStopRequest();
  805. autoRunActive = true;
  806. autoRunTotalRuns = totalRuns;
  807. let successCount = 0;
  808. let failCount = 0;
  809. await setState({ autoRunning: true });
  810. for (let run = 1; run <= totalRuns; run++) {
  811. autoRunCurrentRun = run;
  812. // Reset everything at the start of each run (keep VPS/mail settings)
  813. const prevState = await getState();
  814. await resetState();
  815. await setState({ autoRunning: true });
  816. // Tell side panel to reset all UI
  817. chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {});
  818. await sleepWithStop(500);
  819. await addLog(`=== Auto Run ${run}/${totalRuns} — Phase 1: Get OAuth link & open signup ===`, 'info');
  820. const status = (phase) => ({ type: 'AUTO_RUN_STATUS', payload: { phase, currentRun: run, totalRuns, successCount } });
  821. try {
  822. throwIfStopped();
  823. chrome.runtime.sendMessage(status('running')).catch(() => {});
  824. await executeStepAndWait(1, 2000);
  825. await executeStepAndWait(2, 2000);
  826. const duckEmail = await fetchDuckEmail({ generateNew: true });
  827. await addLog(`=== Run ${run}/${totalRuns} — Duck email ready: ${duckEmail} ===`, 'ok');
  828. await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info');
  829. chrome.runtime.sendMessage(status('running')).catch(() => {});
  830. const signupTabId = await getTabId('signup-page');
  831. if (signupTabId) {
  832. await chrome.tabs.update(signupTabId, { active: true });
  833. }
  834. await executeStepAndWait(3, 3000);
  835. await executeStepAndWait(4, 2000);
  836. await executeStepAndWait(5, 3000);
  837. await executeStepAndWait(6, 3000);
  838. await executeStepAndWait(7, 2000);
  839. // After login+verification, check if page landed on add-phone (only expected after step 5, not here)
  840. const phoneCheckTabId = await getTabId('signup-page');
  841. if (phoneCheckTabId) {
  842. try {
  843. const phoneTab = await chrome.tabs.get(phoneCheckTabId);
  844. if (phoneTab.url && (phoneTab.url.includes('add-phone') || phoneTab.url.includes('/phone'))) {
  845. throw new Error('PHONE_VERIFY_REQUIRED');
  846. }
  847. } catch (e) {
  848. if (e.message === 'PHONE_VERIFY_REQUIRED') throw e;
  849. }
  850. }
  851. await executeStepAndWait(8, 2000);
  852. await executeStepAndWait(9, 1000);
  853. successCount++;
  854. await addLog(`=== Run ${run}/${totalRuns} COMPLETE! (${successCount} succeeded, ${failCount} failed) ===`, 'ok');
  855. } catch (err) {
  856. if (isStopError(err)) {
  857. await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn');
  858. chrome.runtime.sendMessage(status('stopped')).catch(() => {});
  859. break; // Only stop on explicit user stop
  860. } else {
  861. failCount++;
  862. if (err.message === 'PHONE_VERIFY_REQUIRED') {
  863. await addLog(`Run ${run}/${totalRuns} failed: Phone verification required — sleeping 30s before next run (${successCount} succeeded, ${failCount} failed)`, 'error');
  864. await sleepWithStop(30000);
  865. } else {
  866. await addLog(`Run ${run}/${totalRuns} failed: ${err.message} (${successCount} succeeded, ${failCount} failed)`, 'error');
  867. }
  868. await addLog(`Skipping to next run...`, 'warn');
  869. continue;
  870. }
  871. }
  872. }
  873. const attempted = autoRunCurrentRun;
  874. if (stopRequested) {
  875. await addLog(`=== Stopped after ${Math.max(0, attempted - 1)}/${autoRunTotalRuns} runs — ${successCount} succeeded, ${failCount} failed ===`, 'warn');
  876. chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: attempted, totalRuns: autoRunTotalRuns, successCount, failCount } }).catch(() => {});
  877. } else {
  878. await addLog(`=== All ${autoRunTotalRuns} runs finished — ${successCount} succeeded, ${failCount} failed ===`, successCount > 0 ? 'ok' : 'warn');
  879. chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: attempted, totalRuns: autoRunTotalRuns, successCount, failCount } }).catch(() => {});
  880. }
  881. autoRunActive = false;
  882. await setState({ autoRunning: false });
  883. clearStopRequest();
  884. }
  885. function waitForResume() {
  886. return new Promise((resolve, reject) => {
  887. throwIfStopped();
  888. resumeWaiter = { resolve, reject };
  889. });
  890. }
  891. async function resumeAutoRun() {
  892. throwIfStopped();
  893. const state = await getState();
  894. if (!state.email) {
  895. await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error');
  896. return;
  897. }
  898. if (resumeWaiter) {
  899. resumeWaiter.resolve();
  900. resumeWaiter = null;
  901. }
  902. }
  903. // ============================================================
  904. // Step 1: Get OAuth Link (via vps-panel.js)
  905. // ============================================================
  906. async function executeStep1(state) {
  907. if (!state.vpsUrl) {
  908. throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.');
  909. }
  910. await addLog(`Step 1: Opening VPS panel...`);
  911. await reuseOrCreateTab('vps-panel', state.vpsUrl, {
  912. inject: ['content/utils.js', 'content/vps-panel.js'],
  913. reloadIfSameUrl: true,
  914. });
  915. await sendToContentScript('vps-panel', {
  916. type: 'EXECUTE_STEP',
  917. step: 1,
  918. source: 'background',
  919. payload: {},
  920. });
  921. }
  922. // ============================================================
  923. // Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register)
  924. // ============================================================
  925. async function executeStep2(state) {
  926. if (!state.oauthUrl) {
  927. throw new Error('No OAuth URL. Complete step 1 first.');
  928. }
  929. await addLog(`Step 2: Opening auth URL...`);
  930. await reuseOrCreateTab('signup-page', state.oauthUrl);
  931. await sendToContentScript('signup-page', {
  932. type: 'EXECUTE_STEP',
  933. step: 2,
  934. source: 'background',
  935. payload: {},
  936. });
  937. }
  938. // ============================================================
  939. // Step 3: Fill Email & Password (via signup-page.js)
  940. // ============================================================
  941. async function executeStep3(state) {
  942. if (!state.email) {
  943. throw new Error('No email address. Paste email in Side Panel first.');
  944. }
  945. const password = state.customPassword || generatePassword();
  946. await setPasswordState(password);
  947. // Save account record
  948. const accounts = state.accounts || [];
  949. accounts.push({ email: state.email, password, createdAt: new Date().toISOString() });
  950. await setState({ accounts });
  951. await addLog(
  952. `Step 3: Filling email ${state.email}, password ${state.customPassword ? 'customized' : 'generated'} (${password.length} chars)`
  953. );
  954. await sendToContentScript('signup-page', {
  955. type: 'EXECUTE_STEP',
  956. step: 3,
  957. source: 'background',
  958. payload: { email: state.email, password },
  959. });
  960. }
  961. // ============================================================
  962. // Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js)
  963. // ============================================================
  964. async function getMailConfig(state) {
  965. const provider = state.mailProvider || 'temp-api';
  966. if (provider === 'temp-api') {
  967. let rawUrl = (state.tempApiUrl || '').trim();
  968. // Fallback: read directly from chrome.storage.local in case session is stale
  969. if (!rawUrl) {
  970. const local = await chrome.storage.local.get('tempApiUrl');
  971. rawUrl = (local.tempApiUrl || '').trim();
  972. }
  973. if (!rawUrl) {
  974. return { error: 'Temp Email API URL is empty. Please paste the full URL with JWT in Side Panel.' };
  975. }
  976. // Extract base origin and JWT from the URL (e.g. https://host/?jwt=xxx)
  977. let apiOrigin, jwt;
  978. try {
  979. const parsed = new URL(rawUrl);
  980. apiOrigin = parsed.origin;
  981. jwt = parsed.searchParams.get('jwt');
  982. } catch {
  983. return { error: 'Temp Email API URL is invalid.' };
  984. }
  985. if (!jwt) {
  986. return { error: 'Temp Email API URL has no jwt parameter.' };
  987. }
  988. return {
  989. source: 'temp-api',
  990. apiOrigin,
  991. jwt,
  992. label: 'Temp Email API',
  993. useFetch: true, // signal that this provider uses fetch, not a tab
  994. };
  995. }
  996. if (provider === '163') {
  997. 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' };
  998. }
  999. if (provider === 'inbucket') {
  1000. const host = normalizeInbucketOrigin(state.inbucketHost);
  1001. const mailbox = (state.inbucketMailbox || '').trim();
  1002. if (!host) {
  1003. return { error: 'Inbucket host is empty or invalid.' };
  1004. }
  1005. if (!mailbox) {
  1006. return { error: 'Inbucket mailbox name is empty.' };
  1007. }
  1008. return {
  1009. source: 'inbucket-mail',
  1010. url: `${host}/m/${encodeURIComponent(mailbox)}/`,
  1011. label: `Inbucket Mailbox (${mailbox})`,
  1012. navigateOnReuse: true,
  1013. inject: ['content/utils.js', 'content/inbucket-mail.js'],
  1014. injectSource: 'inbucket-mail',
  1015. };
  1016. }
  1017. return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
  1018. }
  1019. function normalizeInbucketOrigin(rawValue) {
  1020. const value = (rawValue || '').trim();
  1021. if (!value) return '';
  1022. const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
  1023. try {
  1024. const parsed = new URL(candidate);
  1025. return parsed.origin;
  1026. } catch {
  1027. return '';
  1028. }
  1029. }
  1030. // ============================================================
  1031. // Temp Email API: fetch-based polling (no tab needed)
  1032. // ============================================================
  1033. function extractVerificationCodeFromText(text) {
  1034. // Pattern 1: Chinese format "代码为 370794" or "验证码...370794"
  1035. const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
  1036. if (matchCn) return matchCn[1];
  1037. // Pattern 2: English format "code is 370794" or "code: 370794"
  1038. const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
  1039. if (matchEn) return matchEn[1] || matchEn[2];
  1040. // Pattern 3: standalone 6-digit number
  1041. const match6 = text.match(/\b(\d{6})\b/);
  1042. if (match6) return match6[1];
  1043. return null;
  1044. }
  1045. function decodeMimeWord(encoded) {
  1046. // Decode =?charset?encoding?text?= (RFC 2047)
  1047. return encoded.replace(/=\?([^?]+)\?(B|Q)\?([^?]*)\?=/gi, (_, charset, enc, text) => {
  1048. if (enc.toUpperCase() === 'B') {
  1049. // Base64
  1050. try { return atob(text); } catch { return text; }
  1051. }
  1052. // Quoted-Printable
  1053. return text.replace(/_/g, ' ').replace(/=([0-9A-Fa-f]{2})/g, (__, hex) =>
  1054. String.fromCharCode(parseInt(hex, 16))
  1055. );
  1056. });
  1057. }
  1058. function decodeUtf8Bytes(str) {
  1059. // If the decoded string contains raw UTF-8 bytes, decode them
  1060. try { return decodeURIComponent(escape(str)); } catch { return str; }
  1061. }
  1062. function extractSubjectFromRaw(raw) {
  1063. // Extract Subject header (may span multiple lines with folding)
  1064. const match = raw.match(/^Subject:\s*([\s\S]*?)(?=\r?\n[^\s]|\r?\n\r?\n)/mi);
  1065. if (!match) return '';
  1066. // Unfold continuation lines and decode MIME words
  1067. const unfolded = match[1].replace(/\r?\n\s+/g, ' ').trim();
  1068. return decodeUtf8Bytes(decodeMimeWord(unfolded));
  1069. }
  1070. function extractFromHeader(raw) {
  1071. const match = raw.match(/^From:\s*([\s\S]*?)(?=\r?\n[^\s]|\r?\n\r?\n)/mi);
  1072. if (!match) return '';
  1073. return match[1].replace(/\r?\n\s+/g, ' ').trim().toLowerCase();
  1074. }
  1075. function extractPlainTextFromRaw(raw) {
  1076. // Try to extract the text/plain part from a multipart email
  1077. const plainMatch = raw.match(/Content-Type:\s*text\/plain[\s\S]*?\r?\n\r?\n([\s\S]*?)(?:\r?\n--|\r?\n$)/i);
  1078. if (plainMatch) {
  1079. let text = plainMatch[1];
  1080. // Decode quoted-printable
  1081. text = text.replace(/=\r?\n/g, '');
  1082. text = text.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
  1083. try { return decodeURIComponent(escape(text)); } catch { return text; }
  1084. }
  1085. return raw;
  1086. }
  1087. async function pollTempEmailApi(mail, step, payload) {
  1088. const { senderFilters, subjectFilters, maxAttempts, intervalMs, usedMailIds, usedCodes } = payload;
  1089. const usedIdSet = new Set((usedMailIds || []).map(String));
  1090. const usedCodeSet = new Set(usedCodes || []);
  1091. const headers = { 'Authorization': `Bearer ${mail.jwt}` };
  1092. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  1093. throwIfStopped();
  1094. await addLog(`Step ${step}: Polling Temp Email API... attempt ${attempt}/${maxAttempts}`);
  1095. try {
  1096. const resp = await fetch(`${mail.apiOrigin}/api/mails?limit=10&offset=0`, { headers });
  1097. if (!resp.ok) throw new Error(`API returned ${resp.status}`);
  1098. const data = await resp.json();
  1099. const mails = data.results || [];
  1100. for (const m of mails) {
  1101. if (usedIdSet.has(String(m.id))) continue;
  1102. const raw = m.raw || '';
  1103. const source = (m.source || '').toLowerCase();
  1104. const fromHeader = extractFromHeader(raw);
  1105. const subject = extractSubjectFromRaw(raw).toLowerCase();
  1106. const plainText = extractPlainTextFromRaw(raw);
  1107. // Match against both envelope sender and From header
  1108. const senderMatch = senderFilters.some(f => {
  1109. const fl = f.toLowerCase();
  1110. return source.includes(fl) || fromHeader.includes(fl);
  1111. });
  1112. const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()));
  1113. if (senderMatch || subjectMatch) {
  1114. const code = extractVerificationCodeFromText(subject + ' ' + plainText);
  1115. if (code) {
  1116. if (usedCodeSet.has(code)) {
  1117. console.log(LOG_PREFIX, `Step ${step}: Skipping already-used code ${code} (id: ${m.id})`);
  1118. continue;
  1119. }
  1120. await addLog(`Step ${step}: Code found from API: ${code} (subject: ${subject.slice(0, 40)})`, 'ok');
  1121. return { ok: true, code, emailTimestamp: Date.now(), mailId: String(m.id) };
  1122. }
  1123. }
  1124. }
  1125. } catch (err) {
  1126. await addLog(`Step ${step}: API fetch error: ${err.message}`, 'warn');
  1127. }
  1128. if (attempt < maxAttempts) {
  1129. await sleepWithStop(intervalMs);
  1130. }
  1131. }
  1132. throw new Error(
  1133. `No matching email found via Temp Email API after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
  1134. 'Email may be delayed.'
  1135. );
  1136. }
  1137. async function clickResendOnSignupPage(step) {
  1138. const signupTabId = await getTabId('signup-page');
  1139. if (!signupTabId) return;
  1140. await chrome.tabs.update(signupTabId, { active: true });
  1141. await sleepWithStop(500);
  1142. try {
  1143. await sendToContentScript('signup-page', {
  1144. type: 'CLICK_RESEND_EMAIL',
  1145. step,
  1146. source: 'background',
  1147. });
  1148. } catch (err) {
  1149. await addLog(`Step ${step}: Resend click skipped: ${err.message}`, 'warn');
  1150. }
  1151. }
  1152. async function executeStep4(state) {
  1153. // Click "重新发送电子邮件" on the signup page before polling
  1154. await clickResendOnSignupPage(4);
  1155. // Re-read state to ensure tempApiUrl is fresh (may have been saved after initial getState)
  1156. const freshState = await getState();
  1157. const mail = await getMailConfig(freshState);
  1158. if (mail.error) throw new Error(mail.error);
  1159. const pollPayload = {
  1160. filterAfterTimestamp: state.flowStartTime || 0,
  1161. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
  1162. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
  1163. targetEmail: state.email,
  1164. maxAttempts: 20,
  1165. intervalMs: 3000,
  1166. usedMailIds: state.usedMailIds || [],
  1167. usedCodes: state.usedCodes || [],
  1168. };
  1169. let result;
  1170. if (mail.useFetch) {
  1171. // Temp Email API: fetch directly, no tab needed
  1172. await addLog(`Step 4: Polling ${mail.label}...`);
  1173. result = await pollTempEmailApi(mail, 4, pollPayload);
  1174. } else {
  1175. await addLog(`Step 4: Opening ${mail.label}...`);
  1176. // For mail tabs, only create if not alive — don't navigate (preserves login session)
  1177. const alive = await isTabAlive(mail.source);
  1178. if (alive) {
  1179. if (mail.navigateOnReuse) {
  1180. await reuseOrCreateTab(mail.source, mail.url, {
  1181. inject: mail.inject,
  1182. injectSource: mail.injectSource,
  1183. });
  1184. } else {
  1185. const tabId = await getTabId(mail.source);
  1186. await chrome.tabs.update(tabId, { active: true });
  1187. }
  1188. } else {
  1189. await reuseOrCreateTab(mail.source, mail.url, {
  1190. inject: mail.inject,
  1191. injectSource: mail.injectSource,
  1192. });
  1193. }
  1194. result = await sendToContentScript(mail.source, {
  1195. type: 'POLL_EMAIL',
  1196. step: 4,
  1197. source: 'background',
  1198. payload: pollPayload,
  1199. });
  1200. }
  1201. if (result && result.error) {
  1202. throw new Error(result.error);
  1203. }
  1204. if (result && result.code) {
  1205. // Track used mail IDs and codes to avoid reusing stale ones
  1206. const usedMailIds = [...(state.usedMailIds || [])];
  1207. const usedCodes = [...(state.usedCodes || [])];
  1208. if (result.mailId) usedMailIds.push(result.mailId);
  1209. usedCodes.push(result.code);
  1210. await setState({ lastEmailTimestamp: result.emailTimestamp, usedMailIds, usedCodes });
  1211. await addLog(`Step 4: Got verification code: ${result.code}`);
  1212. // Switch to signup tab and fill code
  1213. const signupTabId = await getTabId('signup-page');
  1214. if (signupTabId) {
  1215. await chrome.tabs.update(signupTabId, { active: true });
  1216. await sendToContentScript('signup-page', {
  1217. type: 'FILL_CODE',
  1218. step: 4,
  1219. source: 'background',
  1220. payload: { code: result.code },
  1221. });
  1222. } else {
  1223. throw new Error('Signup page tab was closed. Cannot fill verification code.');
  1224. }
  1225. }
  1226. }
  1227. // ============================================================
  1228. // Step 5: Fill Name & Birthday (via signup-page.js)
  1229. // ============================================================
  1230. async function executeStep5(state) {
  1231. const { firstName, lastName } = generateRandomName();
  1232. const { year, month, day } = generateRandomBirthday();
  1233. // Save birthday data to state for potential reuse on about-you page
  1234. await setState({ birthYear: year, birthMonth: month, birthDay: day, fullName: `${firstName} ${lastName}` });
  1235. await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
  1236. await sendToContentScript('signup-page', {
  1237. type: 'EXECUTE_STEP',
  1238. step: 5,
  1239. source: 'background',
  1240. payload: { firstName, lastName, year, month, day },
  1241. });
  1242. }
  1243. // ============================================================
  1244. // Step 6: Login ChatGPT (Background opens tab, chatgpt.js handles login)
  1245. // ============================================================
  1246. async function executeStep6(state) {
  1247. if (!state.oauthUrl) {
  1248. throw new Error('No OAuth URL. Complete step 1 first.');
  1249. }
  1250. if (!state.email) {
  1251. throw new Error('No email. Complete step 3 first.');
  1252. }
  1253. await addLog(`Step 6: Opening OAuth URL for login...`);
  1254. // Reuse the signup-page tab — navigate it to the OAuth URL
  1255. await reuseOrCreateTab('signup-page', state.oauthUrl);
  1256. // signup-page.js will inject (same auth.openai.com domain) and handle login
  1257. await sendToContentScript('signup-page', {
  1258. type: 'EXECUTE_STEP',
  1259. step: 6,
  1260. source: 'background',
  1261. payload: { email: state.email, password: state.password },
  1262. });
  1263. }
  1264. // ============================================================
  1265. // Step 7: Get Login Verification Code (qq-mail.js polls, then fills in chatgpt.js)
  1266. // ============================================================
  1267. async function executeStep7(state) {
  1268. // Check if the page is on /about-you (birthday not filled during signup)
  1269. // and handle it before proceeding with verification code polling
  1270. try {
  1271. const aboutYouResult = await sendToContentScript('signup-page', {
  1272. type: 'HANDLE_ABOUT_YOU',
  1273. step: 7,
  1274. source: 'background',
  1275. payload: {
  1276. year: state.birthYear,
  1277. month: state.birthMonth,
  1278. day: state.birthDay,
  1279. fullName: state.fullName,
  1280. },
  1281. });
  1282. if (aboutYouResult && aboutYouResult.handled) {
  1283. await addLog('Step 7: Handled about-you page, waiting for next page...', 'info');
  1284. await sleep(3000);
  1285. }
  1286. } catch (e) {
  1287. // Not on about-you page, continue normally
  1288. }
  1289. // Click "重新发送电子邮件" on the auth page before polling
  1290. await clickResendOnSignupPage(7);
  1291. const mail = await getMailConfig(state);
  1292. if (mail.error) throw new Error(mail.error);
  1293. // Re-read state to get latest usedMailIds/usedCodes
  1294. const freshState = await getState();
  1295. const pollPayload = {
  1296. filterAfterTimestamp: freshState.lastEmailTimestamp || freshState.flowStartTime || 0,
  1297. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
  1298. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
  1299. targetEmail: freshState.email,
  1300. maxAttempts: 20,
  1301. intervalMs: 3000,
  1302. usedMailIds: freshState.usedMailIds || [],
  1303. usedCodes: freshState.usedCodes || [],
  1304. };
  1305. let result;
  1306. if (mail.useFetch) {
  1307. await addLog(`Step 7: Polling ${mail.label}...`);
  1308. result = await pollTempEmailApi(mail, 7, pollPayload);
  1309. } else {
  1310. await addLog(`Step 7: Opening ${mail.label}...`);
  1311. const alive = await isTabAlive(mail.source);
  1312. if (alive) {
  1313. if (mail.navigateOnReuse) {
  1314. await reuseOrCreateTab(mail.source, mail.url, {
  1315. inject: mail.inject,
  1316. injectSource: mail.injectSource,
  1317. });
  1318. } else {
  1319. const tabId = await getTabId(mail.source);
  1320. await chrome.tabs.update(tabId, { active: true });
  1321. }
  1322. } else {
  1323. await reuseOrCreateTab(mail.source, mail.url, {
  1324. inject: mail.inject,
  1325. injectSource: mail.injectSource,
  1326. });
  1327. }
  1328. result = await sendToContentScript(mail.source, {
  1329. type: 'POLL_EMAIL',
  1330. step: 7,
  1331. source: 'background',
  1332. payload: pollPayload,
  1333. });
  1334. }
  1335. if (result && result.error) {
  1336. throw new Error(result.error);
  1337. }
  1338. if (result && result.code) {
  1339. // Track used mail IDs and codes
  1340. const usedMailIds = [...(freshState.usedMailIds || [])];
  1341. const usedCodes = [...(freshState.usedCodes || [])];
  1342. if (result.mailId) usedMailIds.push(result.mailId);
  1343. usedCodes.push(result.code);
  1344. await setState({ usedMailIds, usedCodes });
  1345. await addLog(`Step 7: Got login verification code: ${result.code}`);
  1346. // Switch to signup/auth tab and fill code
  1347. const signupTabId = await getTabId('signup-page');
  1348. if (signupTabId) {
  1349. await chrome.tabs.update(signupTabId, { active: true });
  1350. await sendToContentScript('signup-page', {
  1351. type: 'FILL_CODE',
  1352. step: 7,
  1353. source: 'background',
  1354. payload: { code: result.code },
  1355. });
  1356. } else {
  1357. throw new Error('Auth page tab was closed. Cannot fill verification code.');
  1358. }
  1359. }
  1360. }
  1361. // ============================================================
  1362. // Step 8: Complete OAuth (auto click + localhost listener)
  1363. // ============================================================
  1364. let webNavListener = null;
  1365. async function executeStep8(state) {
  1366. if (!state.oauthUrl) {
  1367. throw new Error('No OAuth URL. Complete step 1 first.');
  1368. }
  1369. // Check if the signup tab already redirected to localhost before listener setup
  1370. const signupTabIdEarly = await getTabId('signup-page');
  1371. if (signupTabIdEarly) {
  1372. try {
  1373. const tab = await chrome.tabs.get(signupTabIdEarly);
  1374. if (tab.url && (tab.url.startsWith('http://localhost') || tab.url.startsWith('http://127.0.0.1'))) {
  1375. await addLog(`Step 8: Localhost redirect already captured: ${tab.url}`, 'ok');
  1376. await setState({ localhostUrl: tab.url });
  1377. broadcastDataUpdate({ localhostUrl: tab.url });
  1378. return;
  1379. }
  1380. } catch {}
  1381. }
  1382. await addLog('Step 8: Setting up localhost redirect listener...');
  1383. // Register webNavigation listener (scoped to this step)
  1384. return new Promise((resolve, reject) => {
  1385. let resolved = false;
  1386. const isLocalhostUrl = (url) =>
  1387. url && (url.startsWith('http://localhost') || url.startsWith('http://127.0.0.1'));
  1388. const cleanupListeners = () => {
  1389. if (webNavListener) {
  1390. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  1391. chrome.webNavigation.onCommitted.removeListener(webNavListener);
  1392. chrome.webNavigation.onErrorOccurred.removeListener(webNavListener);
  1393. webNavListener = null;
  1394. }
  1395. };
  1396. const captureLocalhostUrl = (url) => {
  1397. if (resolved) return;
  1398. resolved = true;
  1399. cleanupListeners();
  1400. clearTimeout(timeout);
  1401. setState({ localhostUrl: url }).then(() => {
  1402. addLog(`Step 8: Captured localhost URL: ${url}`, 'ok');
  1403. setStepStatus(8, 'completed');
  1404. notifyStepComplete(8, { localhostUrl: url });
  1405. broadcastDataUpdate({ localhostUrl: url });
  1406. resolve();
  1407. });
  1408. };
  1409. const timeout = setTimeout(() => {
  1410. cleanupListeners();
  1411. reject(new Error('Localhost redirect not captured after 120s. Step 8 click may have been blocked.'));
  1412. }, 120000);
  1413. webNavListener = (details) => {
  1414. if (details.frameId === 0 && isLocalhostUrl(details.url)) {
  1415. console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`);
  1416. captureLocalhostUrl(details.url);
  1417. }
  1418. };
  1419. chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
  1420. chrome.webNavigation.onCommitted.addListener(webNavListener);
  1421. chrome.webNavigation.onErrorOccurred.addListener(webNavListener);
  1422. // After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
  1423. // with a "继续" button. Content script clicks it directly (no debugger needed).
  1424. (async () => {
  1425. try {
  1426. let signupTabId = await getTabId('signup-page');
  1427. if (signupTabId) {
  1428. await chrome.tabs.update(signupTabId, { active: true });
  1429. await addLog('Step 8: Switched to auth page...');
  1430. } else {
  1431. signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
  1432. await addLog('Step 8: Auth tab reopened...');
  1433. }
  1434. const clickResult = await sendToContentScript('signup-page', {
  1435. type: 'STEP8_FIND_AND_CLICK',
  1436. source: 'background',
  1437. payload: {},
  1438. });
  1439. if (clickResult?.error) {
  1440. throw new Error(clickResult.error);
  1441. }
  1442. await addLog('Step 8: "继续" button clicked, waiting for redirect...');
  1443. // Poll tab URL in case webNavigation listeners missed the redirect
  1444. for (let i = 0; i < 30 && !resolved; i++) {
  1445. await new Promise(r => setTimeout(r, 1000));
  1446. try {
  1447. const tab = await chrome.tabs.get(signupTabId);
  1448. if (isLocalhostUrl(tab.url)) {
  1449. captureLocalhostUrl(tab.url);
  1450. break;
  1451. }
  1452. } catch { break; }
  1453. }
  1454. } catch (err) {
  1455. clearTimeout(timeout);
  1456. cleanupListeners();
  1457. reject(err);
  1458. }
  1459. })();
  1460. });
  1461. }
  1462. // ============================================================
  1463. // Step 9: VPS Verify (via vps-panel.js)
  1464. // ============================================================
  1465. async function executeStep9(state) {
  1466. if (!state.localhostUrl) {
  1467. throw new Error('No localhost URL. Complete step 8 first.');
  1468. }
  1469. if (!state.vpsUrl) {
  1470. throw new Error('VPS URL not set. Please enter VPS URL in the side panel.');
  1471. }
  1472. await addLog('Step 9: Opening VPS panel...');
  1473. let tabId = await getTabId('vps-panel');
  1474. const alive = tabId && await isTabAlive('vps-panel');
  1475. if (!alive) {
  1476. // Create new tab in the automation window
  1477. const wid = await ensureAutomationWindowId();
  1478. const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true, windowId: wid });
  1479. tabId = tab.id;
  1480. await new Promise(resolve => {
  1481. const listener = (tid, info) => {
  1482. if (tid === tabId && info.status === 'complete') {
  1483. chrome.tabs.onUpdated.removeListener(listener);
  1484. resolve();
  1485. }
  1486. };
  1487. chrome.tabs.onUpdated.addListener(listener);
  1488. });
  1489. } else {
  1490. await chrome.tabs.update(tabId, { active: true });
  1491. }
  1492. // Inject scripts directly and wait for them to be ready
  1493. await chrome.scripting.executeScript({
  1494. target: { tabId },
  1495. files: ['content/utils.js', 'content/vps-panel.js'],
  1496. });
  1497. await new Promise(r => setTimeout(r, 1000));
  1498. // Send command directly — bypass queue/ready mechanism
  1499. await addLog(`Step 9: Filling callback URL...`);
  1500. await chrome.tabs.sendMessage(tabId, {
  1501. type: 'EXECUTE_STEP',
  1502. step: 9,
  1503. source: 'background',
  1504. payload: { localhostUrl: state.localhostUrl },
  1505. });
  1506. }
  1507. // ============================================================
  1508. // Open Side Panel on extension icon click
  1509. // ============================================================
  1510. chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });