background.js 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467
  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.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)
  35. // ============================================================
  36. const DEFAULT_STATE = {
  37. currentStep: 0,
  38. stepStatuses: {
  39. 1: 'pending', 2: 'pending', 3: 'pending', 4: 'pending', 5: 'pending',
  40. 6: 'pending', 7: 'pending', 8: 'pending', 9: 'pending',
  41. },
  42. oauthUrl: null,
  43. email: null,
  44. password: null,
  45. accounts: [], // { email, password, createdAt }
  46. lastEmailTimestamp: null,
  47. localhostUrl: null,
  48. flowStartTime: null,
  49. tabRegistry: {},
  50. logs: [],
  51. vpsUrl: '',
  52. customPassword: '',
  53. mailProvider: '163', // 'qq' or '163'
  54. inbucketHost: '',
  55. inbucketMailbox: '',
  56. };
  57. async function getState() {
  58. const state = await chrome.storage.session.get(null);
  59. return { ...DEFAULT_STATE, ...state };
  60. }
  61. async function initializeSessionStorageAccess() {
  62. try {
  63. if (chrome.storage?.session?.setAccessLevel) {
  64. await chrome.storage.session.setAccessLevel({
  65. accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS',
  66. });
  67. console.log(LOG_PREFIX, 'Enabled storage.session for content scripts');
  68. }
  69. } catch (err) {
  70. console.warn(LOG_PREFIX, 'Failed to enable storage.session for content scripts:', err?.message || err);
  71. }
  72. }
  73. async function setState(updates) {
  74. console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
  75. await chrome.storage.session.set(updates);
  76. }
  77. function broadcastDataUpdate(payload) {
  78. chrome.runtime.sendMessage({
  79. type: 'DATA_UPDATED',
  80. payload,
  81. }).catch(() => {});
  82. }
  83. async function setEmailState(email) {
  84. await setState({ email });
  85. broadcastDataUpdate({ email });
  86. }
  87. async function setPasswordState(password) {
  88. await setState({ password });
  89. broadcastDataUpdate({ password });
  90. }
  91. async function resetState() {
  92. console.log(LOG_PREFIX, 'Resetting all state');
  93. // Preserve settings and persistent data across resets
  94. const prev = await chrome.storage.session.get([
  95. 'seenCodes',
  96. 'seenInbucketMailIds',
  97. 'accounts',
  98. 'tabRegistry',
  99. 'vpsUrl',
  100. 'customPassword',
  101. 'mailProvider',
  102. 'inbucketHost',
  103. 'inbucketMailbox',
  104. ]);
  105. await chrome.storage.session.clear();
  106. await chrome.storage.session.set({
  107. ...DEFAULT_STATE,
  108. seenCodes: prev.seenCodes || [],
  109. seenInbucketMailIds: prev.seenInbucketMailIds || [],
  110. accounts: prev.accounts || [],
  111. tabRegistry: prev.tabRegistry || {},
  112. vpsUrl: prev.vpsUrl || '',
  113. customPassword: prev.customPassword || '',
  114. mailProvider: prev.mailProvider || '163',
  115. inbucketHost: prev.inbucketHost || '',
  116. inbucketMailbox: prev.inbucketMailbox || '',
  117. });
  118. }
  119. /**
  120. * Generate a random password: 14 chars, mix of uppercase, lowercase, digits, symbols.
  121. */
  122. function generatePassword() {
  123. const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
  124. const lower = 'abcdefghjkmnpqrstuvwxyz';
  125. const digits = '23456789';
  126. const symbols = '!@#$%&*?';
  127. const all = upper + lower + digits + symbols;
  128. // Ensure at least one of each type
  129. let pw = '';
  130. pw += upper[Math.floor(Math.random() * upper.length)];
  131. pw += lower[Math.floor(Math.random() * lower.length)];
  132. pw += digits[Math.floor(Math.random() * digits.length)];
  133. pw += symbols[Math.floor(Math.random() * symbols.length)];
  134. // Fill remaining 10 chars
  135. for (let i = 0; i < 10; i++) {
  136. pw += all[Math.floor(Math.random() * all.length)];
  137. }
  138. // Shuffle
  139. return pw.split('').sort(() => Math.random() - 0.5).join('');
  140. }
  141. // ============================================================
  142. // Tab Registry
  143. // ============================================================
  144. async function getTabRegistry() {
  145. const state = await getState();
  146. return state.tabRegistry || {};
  147. }
  148. async function registerTab(source, tabId) {
  149. const registry = await getTabRegistry();
  150. registry[source] = { tabId, ready: true };
  151. await setState({ tabRegistry: registry });
  152. console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
  153. }
  154. async function isTabAlive(source) {
  155. const registry = await getTabRegistry();
  156. const entry = registry[source];
  157. if (!entry) return false;
  158. try {
  159. await chrome.tabs.get(entry.tabId);
  160. return true;
  161. } catch {
  162. // Tab no longer exists — clean up registry
  163. registry[source] = null;
  164. await setState({ tabRegistry: registry });
  165. return false;
  166. }
  167. }
  168. async function getTabId(source) {
  169. const registry = await getTabRegistry();
  170. return registry[source]?.tabId || null;
  171. }
  172. // ============================================================
  173. // Command Queue (for content scripts not yet ready)
  174. // ============================================================
  175. const pendingCommands = new Map(); // source -> { message, resolve, reject, timer }
  176. function queueCommand(source, message, timeout = 15000) {
  177. return new Promise((resolve, reject) => {
  178. const timer = setTimeout(() => {
  179. pendingCommands.delete(source);
  180. const err = `Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`;
  181. console.error(LOG_PREFIX, err);
  182. reject(new Error(err));
  183. }, timeout);
  184. pendingCommands.set(source, { message, resolve, reject, timer });
  185. console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
  186. });
  187. }
  188. function flushCommand(source, tabId) {
  189. const pending = pendingCommands.get(source);
  190. if (pending) {
  191. clearTimeout(pending.timer);
  192. pendingCommands.delete(source);
  193. chrome.tabs.sendMessage(tabId, pending.message).then(pending.resolve).catch(pending.reject);
  194. console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
  195. }
  196. }
  197. function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) {
  198. for (const [source, pending] of pendingCommands.entries()) {
  199. clearTimeout(pending.timer);
  200. pending.reject(new Error(reason));
  201. pendingCommands.delete(source);
  202. console.log(LOG_PREFIX, `Cancelled queued command for ${source}`);
  203. }
  204. }
  205. // ============================================================
  206. // Reuse or create tab
  207. // ============================================================
  208. async function reuseOrCreateTab(source, url, options = {}) {
  209. const alive = await isTabAlive(source);
  210. if (alive) {
  211. const tabId = await getTabId(source);
  212. const currentTab = await chrome.tabs.get(tabId);
  213. const sameUrl = currentTab.url === url;
  214. const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
  215. const registry = await getTabRegistry();
  216. if (sameUrl) {
  217. await chrome.tabs.update(tabId, { active: true });
  218. console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}) on same URL`);
  219. if (shouldReloadOnReuse) {
  220. if (registry[source]) registry[source].ready = false;
  221. await setState({ tabRegistry: registry });
  222. await chrome.tabs.reload(tabId);
  223. await new Promise((resolve) => {
  224. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  225. const listener = (tid, info) => {
  226. if (tid === tabId && info.status === 'complete') {
  227. chrome.tabs.onUpdated.removeListener(listener);
  228. clearTimeout(timer);
  229. resolve();
  230. }
  231. };
  232. chrome.tabs.onUpdated.addListener(listener);
  233. });
  234. }
  235. // For dynamically injected pages like the VPS panel, re-inject immediately.
  236. if (options.inject) {
  237. if (registry[source]) registry[source].ready = false;
  238. await setState({ tabRegistry: registry });
  239. if (options.injectSource) {
  240. await chrome.scripting.executeScript({
  241. target: { tabId },
  242. func: (injectedSource) => {
  243. window.__MULTIPAGE_SOURCE = injectedSource;
  244. },
  245. args: [options.injectSource],
  246. });
  247. }
  248. await chrome.scripting.executeScript({
  249. target: { tabId },
  250. files: options.inject,
  251. });
  252. await new Promise(r => setTimeout(r, 500));
  253. }
  254. return tabId;
  255. }
  256. // Mark as not ready BEFORE navigating — so READY signal from new page is captured correctly
  257. if (registry[source]) registry[source].ready = false;
  258. await setState({ tabRegistry: registry });
  259. // Navigate existing tab to new URL
  260. await chrome.tabs.update(tabId, { url, active: true });
  261. console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}), navigated to ${url.slice(0, 60)}`);
  262. // Wait for page load complete (with 30s timeout)
  263. await new Promise((resolve) => {
  264. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  265. const listener = (tid, info) => {
  266. if (tid === tabId && info.status === 'complete') {
  267. chrome.tabs.onUpdated.removeListener(listener);
  268. clearTimeout(timer);
  269. resolve();
  270. }
  271. };
  272. chrome.tabs.onUpdated.addListener(listener);
  273. });
  274. // If dynamic injection needed (VPS panel), re-inject after navigation
  275. if (options.inject) {
  276. if (options.injectSource) {
  277. await chrome.scripting.executeScript({
  278. target: { tabId },
  279. func: (injectedSource) => {
  280. window.__MULTIPAGE_SOURCE = injectedSource;
  281. },
  282. args: [options.injectSource],
  283. });
  284. }
  285. await chrome.scripting.executeScript({
  286. target: { tabId },
  287. files: options.inject,
  288. });
  289. }
  290. // Wait a bit for content script to inject and send READY
  291. await new Promise(r => setTimeout(r, 500));
  292. return tabId;
  293. }
  294. // Create new tab in the automation window
  295. const wid = await ensureAutomationWindowId();
  296. const tab = await chrome.tabs.create({ url, active: true, windowId: wid });
  297. console.log(LOG_PREFIX, `Created new tab ${source} (${tab.id})`);
  298. // If dynamic injection needed (VPS panel), inject scripts after load
  299. if (options.inject) {
  300. await new Promise((resolve) => {
  301. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  302. const listener = (tabId, info) => {
  303. if (tabId === tab.id && info.status === 'complete') {
  304. chrome.tabs.onUpdated.removeListener(listener);
  305. clearTimeout(timer);
  306. resolve();
  307. }
  308. };
  309. chrome.tabs.onUpdated.addListener(listener);
  310. });
  311. if (options.injectSource) {
  312. await chrome.scripting.executeScript({
  313. target: { tabId: tab.id },
  314. func: (injectedSource) => {
  315. window.__MULTIPAGE_SOURCE = injectedSource;
  316. },
  317. args: [options.injectSource],
  318. });
  319. }
  320. await chrome.scripting.executeScript({
  321. target: { tabId: tab.id },
  322. files: options.inject,
  323. });
  324. }
  325. return tab.id;
  326. }
  327. // ============================================================
  328. // Send command to content script (with readiness check)
  329. // ============================================================
  330. async function sendToContentScript(source, message) {
  331. const registry = await getTabRegistry();
  332. const entry = registry[source];
  333. if (!entry || !entry.ready) {
  334. console.log(LOG_PREFIX, `${source} not ready, queuing command`);
  335. return queueCommand(source, message);
  336. }
  337. // Verify tab is still alive
  338. const alive = await isTabAlive(source);
  339. if (!alive) {
  340. // Tab was closed — queue the command, it will be sent when tab is reopened
  341. console.log(LOG_PREFIX, `${source} tab was closed, queuing command`);
  342. return queueCommand(source, message);
  343. }
  344. console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type);
  345. return chrome.tabs.sendMessage(entry.tabId, message);
  346. }
  347. // ============================================================
  348. // Logging
  349. // ============================================================
  350. async function addLog(message, level = 'info') {
  351. const state = await getState();
  352. const logs = state.logs || [];
  353. const entry = { message, level, timestamp: Date.now() };
  354. logs.push(entry);
  355. // Keep last 500 logs
  356. if (logs.length > 500) logs.splice(0, logs.length - 500);
  357. await setState({ logs });
  358. // Broadcast to side panel
  359. chrome.runtime.sendMessage({ type: 'LOG_ENTRY', payload: entry }).catch(() => {});
  360. }
  361. // ============================================================
  362. // Step Status Management
  363. // ============================================================
  364. async function setStepStatus(step, status) {
  365. const state = await getState();
  366. const statuses = { ...state.stepStatuses };
  367. statuses[step] = status;
  368. await setState({ stepStatuses: statuses, currentStep: step });
  369. // Broadcast to side panel
  370. chrome.runtime.sendMessage({
  371. type: 'STEP_STATUS_CHANGED',
  372. payload: { step, status },
  373. }).catch(() => {});
  374. }
  375. function isStopError(error) {
  376. const message = typeof error === 'string' ? error : error?.message;
  377. return message === STOP_ERROR_MESSAGE;
  378. }
  379. function clearStopRequest() {
  380. stopRequested = false;
  381. }
  382. function throwIfStopped() {
  383. if (stopRequested) {
  384. throw new Error(STOP_ERROR_MESSAGE);
  385. }
  386. }
  387. async function sleepWithStop(ms) {
  388. const start = Date.now();
  389. while (Date.now() - start < ms) {
  390. throwIfStopped();
  391. await new Promise(r => setTimeout(r, Math.min(100, ms - (Date.now() - start))));
  392. }
  393. }
  394. async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY_MAX) {
  395. const duration = Math.floor(Math.random() * (max - min + 1)) + min;
  396. await sleepWithStop(duration);
  397. }
  398. async function clickWithDebugger(tabId, rect) {
  399. if (!tabId) {
  400. throw new Error('No auth tab found for debugger click.');
  401. }
  402. if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) {
  403. throw new Error('Step 8 debugger fallback needs a valid button position.');
  404. }
  405. const target = { tabId };
  406. try {
  407. await chrome.debugger.attach(target, '1.3');
  408. } catch (err) {
  409. throw new Error(
  410. `Debugger attach failed during step 8 fallback: ${err.message}. ` +
  411. 'If DevTools is open on the auth tab, close it and retry.'
  412. );
  413. }
  414. try {
  415. const x = Math.round(rect.centerX);
  416. const y = Math.round(rect.centerY);
  417. await chrome.debugger.sendCommand(target, 'Page.bringToFront');
  418. await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
  419. type: 'mouseMoved',
  420. x,
  421. y,
  422. button: 'none',
  423. buttons: 0,
  424. clickCount: 0,
  425. });
  426. await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
  427. type: 'mousePressed',
  428. x,
  429. y,
  430. button: 'left',
  431. buttons: 1,
  432. clickCount: 1,
  433. });
  434. await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
  435. type: 'mouseReleased',
  436. x,
  437. y,
  438. button: 'left',
  439. buttons: 0,
  440. clickCount: 1,
  441. });
  442. } finally {
  443. await chrome.debugger.detach(target).catch(() => {});
  444. }
  445. }
  446. async function broadcastStopToContentScripts() {
  447. const registry = await getTabRegistry();
  448. for (const entry of Object.values(registry)) {
  449. if (!entry?.tabId) continue;
  450. try {
  451. await chrome.tabs.sendMessage(entry.tabId, {
  452. type: 'STOP_FLOW',
  453. source: 'background',
  454. payload: {},
  455. });
  456. } catch {}
  457. }
  458. }
  459. let stopRequested = false;
  460. // ============================================================
  461. // Message Handler (central router)
  462. // ============================================================
  463. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  464. console.log(LOG_PREFIX, `Received: ${message.type} from ${message.source || 'sidepanel'}`, message);
  465. handleMessage(message, sender).then(response => {
  466. sendResponse(response);
  467. }).catch(err => {
  468. console.error(LOG_PREFIX, 'Handler error:', err);
  469. sendResponse({ error: err.message });
  470. });
  471. return true; // async response
  472. });
  473. async function handleMessage(message, sender) {
  474. switch (message.type) {
  475. case 'CONTENT_SCRIPT_READY': {
  476. const tabId = sender.tab?.id;
  477. if (tabId && message.source) {
  478. await registerTab(message.source, tabId);
  479. flushCommand(message.source, tabId);
  480. await addLog(`Content script ready: ${message.source} (tab ${tabId})`);
  481. }
  482. return { ok: true };
  483. }
  484. case 'LOG': {
  485. const { message: msg, level } = message.payload;
  486. await addLog(`[${message.source}] ${msg}`, level);
  487. return { ok: true };
  488. }
  489. case 'STEP_COMPLETE': {
  490. if (stopRequested) {
  491. await setStepStatus(message.step, 'stopped');
  492. notifyStepError(message.step, STOP_ERROR_MESSAGE);
  493. return { ok: true };
  494. }
  495. await setStepStatus(message.step, 'completed');
  496. await addLog(`Step ${message.step} completed`, 'ok');
  497. await handleStepData(message.step, message.payload);
  498. notifyStepComplete(message.step, message.payload);
  499. return { ok: true };
  500. }
  501. case 'STEP_ERROR': {
  502. if (isStopError(message.error)) {
  503. await setStepStatus(message.step, 'stopped');
  504. await addLog(`Step ${message.step} stopped by user`, 'warn');
  505. notifyStepError(message.step, message.error);
  506. } else {
  507. await setStepStatus(message.step, 'failed');
  508. await addLog(`Step ${message.step} failed: ${message.error}`, 'error');
  509. notifyStepError(message.step, message.error);
  510. }
  511. return { ok: true };
  512. }
  513. case 'GET_STATE': {
  514. return await getState();
  515. }
  516. case 'RESET': {
  517. clearStopRequest();
  518. await resetState();
  519. await addLog('Flow reset', 'info');
  520. return { ok: true };
  521. }
  522. case 'EXECUTE_STEP': {
  523. clearStopRequest();
  524. const step = message.payload.step;
  525. // Save email if provided (from side panel step 3)
  526. if (message.payload.email) {
  527. await setEmailState(message.payload.email);
  528. }
  529. await executeStep(step);
  530. return { ok: true };
  531. }
  532. case 'AUTO_RUN': {
  533. clearStopRequest();
  534. const totalRuns = message.payload?.totalRuns || 1;
  535. autoRunLoop(totalRuns); // fire-and-forget
  536. return { ok: true };
  537. }
  538. case 'RESUME_AUTO_RUN': {
  539. clearStopRequest();
  540. if (message.payload.email) {
  541. await setEmailState(message.payload.email);
  542. }
  543. resumeAutoRun(); // fire-and-forget
  544. return { ok: true };
  545. }
  546. case 'SAVE_SETTING': {
  547. const updates = {};
  548. if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
  549. if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
  550. if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
  551. if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
  552. if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
  553. await setState(updates);
  554. return { ok: true };
  555. }
  556. // Side panel data updates
  557. case 'SAVE_EMAIL': {
  558. await setEmailState(message.payload.email);
  559. return { ok: true, email: message.payload.email };
  560. }
  561. case 'FETCH_DUCK_EMAIL': {
  562. clearStopRequest();
  563. const email = await fetchDuckEmail(message.payload || {});
  564. return { ok: true, email };
  565. }
  566. case 'STOP_FLOW': {
  567. await requestStop();
  568. return { ok: true };
  569. }
  570. default:
  571. console.warn(LOG_PREFIX, `Unknown message type: ${message.type}`);
  572. return { error: `Unknown message type: ${message.type}` };
  573. }
  574. }
  575. // ============================================================
  576. // Step Data Handlers
  577. // ============================================================
  578. async function handleStepData(step, payload) {
  579. switch (step) {
  580. case 1:
  581. if (payload.oauthUrl) {
  582. await setState({ oauthUrl: payload.oauthUrl });
  583. broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
  584. }
  585. break;
  586. case 3:
  587. if (payload.email) await setEmailState(payload.email);
  588. break;
  589. case 4:
  590. if (payload.emailTimestamp) await setState({ lastEmailTimestamp: payload.emailTimestamp });
  591. break;
  592. case 8:
  593. if (payload.localhostUrl) {
  594. await setState({ localhostUrl: payload.localhostUrl });
  595. broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
  596. }
  597. break;
  598. }
  599. }
  600. // ============================================================
  601. // Step Completion Waiting
  602. // ============================================================
  603. // Map of step -> { resolve, reject } for waiting on step completion
  604. const stepWaiters = new Map();
  605. let resumeWaiter = null;
  606. function waitForStepComplete(step, timeoutMs = 120000) {
  607. return new Promise((resolve, reject) => {
  608. throwIfStopped();
  609. const timer = setTimeout(() => {
  610. stepWaiters.delete(step);
  611. reject(new Error(`Step ${step} timed out after ${timeoutMs / 1000}s`));
  612. }, timeoutMs);
  613. stepWaiters.set(step, {
  614. resolve: (data) => { clearTimeout(timer); stepWaiters.delete(step); resolve(data); },
  615. reject: (err) => { clearTimeout(timer); stepWaiters.delete(step); reject(err); },
  616. });
  617. });
  618. }
  619. function notifyStepComplete(step, payload) {
  620. const waiter = stepWaiters.get(step);
  621. if (waiter) waiter.resolve(payload);
  622. }
  623. function notifyStepError(step, error) {
  624. const waiter = stepWaiters.get(step);
  625. if (waiter) waiter.reject(new Error(error));
  626. }
  627. async function markRunningStepsStopped() {
  628. const state = await getState();
  629. const runningSteps = Object.entries(state.stepStatuses || {})
  630. .filter(([, status]) => status === 'running')
  631. .map(([step]) => Number(step));
  632. for (const step of runningSteps) {
  633. await setStepStatus(step, 'stopped');
  634. }
  635. }
  636. async function requestStop() {
  637. if (stopRequested) return;
  638. stopRequested = true;
  639. cancelPendingCommands();
  640. if (webNavListener) {
  641. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  642. webNavListener = null;
  643. }
  644. await addLog('Stop requested. Cancelling current operations...', 'warn');
  645. await broadcastStopToContentScripts();
  646. for (const waiter of stepWaiters.values()) {
  647. waiter.reject(new Error(STOP_ERROR_MESSAGE));
  648. }
  649. stepWaiters.clear();
  650. if (resumeWaiter) {
  651. resumeWaiter.reject(new Error(STOP_ERROR_MESSAGE));
  652. resumeWaiter = null;
  653. }
  654. await markRunningStepsStopped();
  655. autoRunActive = false;
  656. await setState({ autoRunning: false });
  657. chrome.runtime.sendMessage({
  658. type: 'AUTO_RUN_STATUS',
  659. payload: { phase: 'stopped', currentRun: autoRunCurrentRun, totalRuns: autoRunTotalRuns },
  660. }).catch(() => {});
  661. }
  662. // ============================================================
  663. // Step Execution
  664. // ============================================================
  665. async function executeStep(step) {
  666. console.log(LOG_PREFIX, `Executing step ${step}`);
  667. throwIfStopped();
  668. await setStepStatus(step, 'running');
  669. await addLog(`Step ${step} started`);
  670. await humanStepDelay();
  671. const state = await getState();
  672. // Set flow start time on first step
  673. if (step === 1 && !state.flowStartTime) {
  674. await setState({ flowStartTime: Date.now() });
  675. }
  676. try {
  677. switch (step) {
  678. case 1: await executeStep1(state); break;
  679. case 2: await executeStep2(state); break;
  680. case 3: await executeStep3(state); break;
  681. case 4: await executeStep4(state); break;
  682. case 5: await executeStep5(state); break;
  683. case 6: await executeStep6(state); break;
  684. case 7: await executeStep7(state); break;
  685. case 8: await executeStep8(state); break;
  686. case 9: await executeStep9(state); break;
  687. default:
  688. throw new Error(`Unknown step: ${step}`);
  689. }
  690. } catch (err) {
  691. if (isStopError(err)) {
  692. await setStepStatus(step, 'stopped');
  693. await addLog(`Step ${step} stopped by user`, 'warn');
  694. throw err;
  695. }
  696. await setStepStatus(step, 'failed');
  697. await addLog(`Step ${step} failed: ${err.message}`, 'error');
  698. throw err;
  699. }
  700. }
  701. /**
  702. * Execute a step and wait for it to complete before returning.
  703. * @param {number} step
  704. * @param {number} delayAfter - ms to wait after completion (for page transitions)
  705. */
  706. async function executeStepAndWait(step, delayAfter = 2000) {
  707. throwIfStopped();
  708. const promise = waitForStepComplete(step, 120000);
  709. await executeStep(step);
  710. await promise;
  711. // Extra delay for page transitions / DOM updates
  712. if (delayAfter > 0) {
  713. await sleepWithStop(delayAfter + Math.floor(Math.random() * 1200));
  714. }
  715. }
  716. async function fetchDuckEmail(options = {}) {
  717. throwIfStopped();
  718. const { generateNew = true } = options;
  719. await addLog(`Duck Mail: Opening autofill settings (${generateNew ? 'generate new' : 'reuse current'})...`);
  720. await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL);
  721. const result = await sendToContentScript('duck-mail', {
  722. type: 'FETCH_DUCK_EMAIL',
  723. source: 'background',
  724. payload: { generateNew },
  725. });
  726. if (result?.error) {
  727. throw new Error(result.error);
  728. }
  729. if (!result?.email) {
  730. throw new Error('Duck email not returned.');
  731. }
  732. await setEmailState(result.email);
  733. await addLog(`Duck Mail: ${result.generated ? 'Generated' : 'Loaded'} ${result.email}`, 'ok');
  734. return result.email;
  735. }
  736. // ============================================================
  737. // Auto Run Flow
  738. // ============================================================
  739. let autoRunActive = false;
  740. let autoRunCurrentRun = 0;
  741. let autoRunTotalRuns = 1;
  742. // Outer loop: runs the full flow N times
  743. async function autoRunLoop(totalRuns) {
  744. if (autoRunActive) {
  745. await addLog('Auto run already in progress', 'warn');
  746. return;
  747. }
  748. clearStopRequest();
  749. autoRunActive = true;
  750. autoRunTotalRuns = totalRuns;
  751. await setState({ autoRunning: true });
  752. for (let run = 1; run <= totalRuns; run++) {
  753. autoRunCurrentRun = run;
  754. // Reset everything at the start of each run (keep VPS/mail settings)
  755. const prevState = await getState();
  756. const keepSettings = {
  757. vpsUrl: prevState.vpsUrl,
  758. mailProvider: prevState.mailProvider,
  759. inbucketHost: prevState.inbucketHost,
  760. inbucketMailbox: prevState.inbucketMailbox,
  761. autoRunning: true,
  762. };
  763. await resetState();
  764. await setState(keepSettings);
  765. // Tell side panel to reset all UI
  766. chrome.runtime.sendMessage({ type: 'AUTO_RUN_RESET' }).catch(() => {});
  767. await sleepWithStop(500);
  768. await addLog(`=== Auto Run ${run}/${totalRuns} — Phase 1: Get OAuth link & open signup ===`, 'info');
  769. const status = (phase) => ({ type: 'AUTO_RUN_STATUS', payload: { phase, currentRun: run, totalRuns } });
  770. try {
  771. throwIfStopped();
  772. chrome.runtime.sendMessage(status('running')).catch(() => {});
  773. await executeStepAndWait(1, 2000);
  774. await executeStepAndWait(2, 2000);
  775. let emailReady = false;
  776. try {
  777. const duckEmail = await fetchDuckEmail({ generateNew: true });
  778. await addLog(`=== Run ${run}/${totalRuns} — Duck email ready: ${duckEmail} ===`, 'ok');
  779. emailReady = true;
  780. } catch (err) {
  781. await addLog(`Duck Mail auto-fetch failed: ${err.message}`, 'warn');
  782. }
  783. if (!emailReady) {
  784. await addLog(`=== Run ${run}/${totalRuns} PAUSED: Fetch Duck email or paste manually, then continue ===`, 'warn');
  785. chrome.runtime.sendMessage(status('waiting_email')).catch(() => {});
  786. // Wait for RESUME_AUTO_RUN — sets a promise that resumeAutoRun resolves
  787. await waitForResume();
  788. const resumedState = await getState();
  789. if (!resumedState.email) {
  790. await addLog('Cannot resume: no email address.', 'error');
  791. break;
  792. }
  793. }
  794. await addLog(`=== Run ${run}/${totalRuns} — Phase 2: Register, verify, login, complete ===`, 'info');
  795. chrome.runtime.sendMessage(status('running')).catch(() => {});
  796. const signupTabId = await getTabId('signup-page');
  797. if (signupTabId) {
  798. await chrome.tabs.update(signupTabId, { active: true });
  799. }
  800. await executeStepAndWait(3, 3000);
  801. await executeStepAndWait(4, 2000);
  802. await executeStepAndWait(5, 3000);
  803. await executeStepAndWait(6, 3000);
  804. await executeStepAndWait(7, 2000);
  805. await executeStepAndWait(8, 2000);
  806. await executeStepAndWait(9, 1000);
  807. await addLog(`=== Run ${run}/${totalRuns} COMPLETE! ===`, 'ok');
  808. } catch (err) {
  809. if (isStopError(err)) {
  810. await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn');
  811. } else {
  812. await addLog(`Run ${run}/${totalRuns} failed: ${err.message}`, 'error');
  813. }
  814. chrome.runtime.sendMessage(status('stopped')).catch(() => {});
  815. break; // Stop on error
  816. }
  817. }
  818. const completedRuns = autoRunCurrentRun;
  819. if (stopRequested) {
  820. await addLog(`=== Stopped after ${Math.max(0, completedRuns - 1)}/${autoRunTotalRuns} runs ===`, 'warn');
  821. chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
  822. } else if (completedRuns >= autoRunTotalRuns) {
  823. await addLog(`=== All ${autoRunTotalRuns} runs completed successfully ===`, 'ok');
  824. chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
  825. } else {
  826. await addLog(`=== Stopped after ${completedRuns}/${autoRunTotalRuns} runs ===`, 'warn');
  827. chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
  828. }
  829. autoRunActive = false;
  830. await setState({ autoRunning: false });
  831. clearStopRequest();
  832. }
  833. function waitForResume() {
  834. return new Promise((resolve, reject) => {
  835. throwIfStopped();
  836. resumeWaiter = { resolve, reject };
  837. });
  838. }
  839. async function resumeAutoRun() {
  840. throwIfStopped();
  841. const state = await getState();
  842. if (!state.email) {
  843. await addLog('Cannot resume: no email address. Paste email in Side Panel first.', 'error');
  844. return;
  845. }
  846. if (resumeWaiter) {
  847. resumeWaiter.resolve();
  848. resumeWaiter = null;
  849. }
  850. }
  851. // ============================================================
  852. // Step 1: Get OAuth Link (via vps-panel.js)
  853. // ============================================================
  854. async function executeStep1(state) {
  855. if (!state.vpsUrl) {
  856. throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.');
  857. }
  858. await addLog(`Step 1: Opening VPS panel...`);
  859. await reuseOrCreateTab('vps-panel', state.vpsUrl, {
  860. inject: ['content/utils.js', 'content/vps-panel.js'],
  861. reloadIfSameUrl: true,
  862. });
  863. await sendToContentScript('vps-panel', {
  864. type: 'EXECUTE_STEP',
  865. step: 1,
  866. source: 'background',
  867. payload: {},
  868. });
  869. }
  870. // ============================================================
  871. // Step 2: Open Signup Page (Background opens tab, signup-page.js clicks Register)
  872. // ============================================================
  873. async function executeStep2(state) {
  874. if (!state.oauthUrl) {
  875. throw new Error('No OAuth URL. Complete step 1 first.');
  876. }
  877. await addLog(`Step 2: Opening auth URL...`);
  878. await reuseOrCreateTab('signup-page', state.oauthUrl);
  879. await sendToContentScript('signup-page', {
  880. type: 'EXECUTE_STEP',
  881. step: 2,
  882. source: 'background',
  883. payload: {},
  884. });
  885. }
  886. // ============================================================
  887. // Step 3: Fill Email & Password (via signup-page.js)
  888. // ============================================================
  889. async function executeStep3(state) {
  890. if (!state.email) {
  891. throw new Error('No email address. Paste email in Side Panel first.');
  892. }
  893. const password = state.customPassword || generatePassword();
  894. await setPasswordState(password);
  895. // Save account record
  896. const accounts = state.accounts || [];
  897. accounts.push({ email: state.email, password, createdAt: new Date().toISOString() });
  898. await setState({ accounts });
  899. await addLog(
  900. `Step 3: Filling email ${state.email}, password ${state.customPassword ? 'customized' : 'generated'} (${password.length} chars)`
  901. );
  902. await sendToContentScript('signup-page', {
  903. type: 'EXECUTE_STEP',
  904. step: 3,
  905. source: 'background',
  906. payload: { email: state.email, password },
  907. });
  908. }
  909. // ============================================================
  910. // Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js)
  911. // ============================================================
  912. function getMailConfig(state) {
  913. const provider = state.mailProvider || 'qq';
  914. if (provider === '163') {
  915. 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' };
  916. }
  917. if (provider === 'inbucket') {
  918. const host = normalizeInbucketOrigin(state.inbucketHost);
  919. const mailbox = (state.inbucketMailbox || '').trim();
  920. if (!host) {
  921. return { error: 'Inbucket host is empty or invalid.' };
  922. }
  923. if (!mailbox) {
  924. return { error: 'Inbucket mailbox name is empty.' };
  925. }
  926. return {
  927. source: 'inbucket-mail',
  928. url: `${host}/m/${encodeURIComponent(mailbox)}/`,
  929. label: `Inbucket Mailbox (${mailbox})`,
  930. navigateOnReuse: true,
  931. inject: ['content/utils.js', 'content/inbucket-mail.js'],
  932. injectSource: 'inbucket-mail',
  933. };
  934. }
  935. return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
  936. }
  937. function normalizeInbucketOrigin(rawValue) {
  938. const value = (rawValue || '').trim();
  939. if (!value) return '';
  940. const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
  941. try {
  942. const parsed = new URL(candidate);
  943. return parsed.origin;
  944. } catch {
  945. return '';
  946. }
  947. }
  948. async function clickResendOnSignupPage(step) {
  949. const signupTabId = await getTabId('signup-page');
  950. if (!signupTabId) return;
  951. await chrome.tabs.update(signupTabId, { active: true });
  952. await sleepWithStop(500);
  953. try {
  954. await sendToContentScript('signup-page', {
  955. type: 'CLICK_RESEND_EMAIL',
  956. step,
  957. source: 'background',
  958. });
  959. } catch (err) {
  960. await addLog(`Step ${step}: Resend click skipped: ${err.message}`, 'warn');
  961. }
  962. }
  963. async function executeStep4(state) {
  964. // Click "重新发送电子邮件" on the signup page before polling
  965. await clickResendOnSignupPage(4);
  966. const mail = getMailConfig(state);
  967. if (mail.error) throw new Error(mail.error);
  968. await addLog(`Step 4: Opening ${mail.label}...`);
  969. // For mail tabs, only create if not alive — don't navigate (preserves login session)
  970. const alive = await isTabAlive(mail.source);
  971. if (alive) {
  972. if (mail.navigateOnReuse) {
  973. await reuseOrCreateTab(mail.source, mail.url, {
  974. inject: mail.inject,
  975. injectSource: mail.injectSource,
  976. });
  977. } else {
  978. const tabId = await getTabId(mail.source);
  979. await chrome.tabs.update(tabId, { active: true });
  980. }
  981. } else {
  982. await reuseOrCreateTab(mail.source, mail.url, {
  983. inject: mail.inject,
  984. injectSource: mail.injectSource,
  985. });
  986. }
  987. const result = await sendToContentScript(mail.source, {
  988. type: 'POLL_EMAIL',
  989. step: 4,
  990. source: 'background',
  991. payload: {
  992. filterAfterTimestamp: state.flowStartTime || 0,
  993. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
  994. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
  995. targetEmail: state.email,
  996. maxAttempts: 20,
  997. intervalMs: 3000,
  998. },
  999. });
  1000. if (result && result.error) {
  1001. throw new Error(result.error);
  1002. }
  1003. if (result && result.code) {
  1004. await setState({ lastEmailTimestamp: result.emailTimestamp });
  1005. await addLog(`Step 4: Got verification code: ${result.code}`);
  1006. // Switch to signup tab and fill code
  1007. const signupTabId = await getTabId('signup-page');
  1008. if (signupTabId) {
  1009. await chrome.tabs.update(signupTabId, { active: true });
  1010. await sendToContentScript('signup-page', {
  1011. type: 'FILL_CODE',
  1012. step: 4,
  1013. source: 'background',
  1014. payload: { code: result.code },
  1015. });
  1016. } else {
  1017. throw new Error('Signup page tab was closed. Cannot fill verification code.');
  1018. }
  1019. }
  1020. }
  1021. // ============================================================
  1022. // Step 5: Fill Name & Birthday (via signup-page.js)
  1023. // ============================================================
  1024. async function executeStep5(state) {
  1025. const { firstName, lastName } = generateRandomName();
  1026. const { year, month, day } = generateRandomBirthday();
  1027. await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
  1028. await sendToContentScript('signup-page', {
  1029. type: 'EXECUTE_STEP',
  1030. step: 5,
  1031. source: 'background',
  1032. payload: { firstName, lastName, year, month, day },
  1033. });
  1034. }
  1035. // ============================================================
  1036. // Step 6: Login ChatGPT (Background opens tab, chatgpt.js handles login)
  1037. // ============================================================
  1038. async function executeStep6(state) {
  1039. if (!state.oauthUrl) {
  1040. throw new Error('No OAuth URL. Complete step 1 first.');
  1041. }
  1042. if (!state.email) {
  1043. throw new Error('No email. Complete step 3 first.');
  1044. }
  1045. await addLog(`Step 6: Opening OAuth URL for login...`);
  1046. // Reuse the signup-page tab — navigate it to the OAuth URL
  1047. await reuseOrCreateTab('signup-page', state.oauthUrl);
  1048. // signup-page.js will inject (same auth.openai.com domain) and handle login
  1049. await sendToContentScript('signup-page', {
  1050. type: 'EXECUTE_STEP',
  1051. step: 6,
  1052. source: 'background',
  1053. payload: { email: state.email, password: state.password },
  1054. });
  1055. }
  1056. // ============================================================
  1057. // Step 7: Get Login Verification Code (qq-mail.js polls, then fills in chatgpt.js)
  1058. // ============================================================
  1059. async function executeStep7(state) {
  1060. // Click "重新发送电子邮件" on the auth page before polling
  1061. await clickResendOnSignupPage(7);
  1062. const mail = getMailConfig(state);
  1063. if (mail.error) throw new Error(mail.error);
  1064. await addLog(`Step 7: Opening ${mail.label}...`);
  1065. const alive = await isTabAlive(mail.source);
  1066. if (alive) {
  1067. if (mail.navigateOnReuse) {
  1068. await reuseOrCreateTab(mail.source, mail.url, {
  1069. inject: mail.inject,
  1070. injectSource: mail.injectSource,
  1071. });
  1072. } else {
  1073. const tabId = await getTabId(mail.source);
  1074. await chrome.tabs.update(tabId, { active: true });
  1075. }
  1076. } else {
  1077. await reuseOrCreateTab(mail.source, mail.url, {
  1078. inject: mail.inject,
  1079. injectSource: mail.injectSource,
  1080. });
  1081. }
  1082. const result = await sendToContentScript(mail.source, {
  1083. type: 'POLL_EMAIL',
  1084. step: 7,
  1085. source: 'background',
  1086. payload: {
  1087. filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
  1088. senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
  1089. subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
  1090. targetEmail: state.email,
  1091. maxAttempts: 20,
  1092. intervalMs: 3000,
  1093. },
  1094. });
  1095. if (result && result.error) {
  1096. throw new Error(result.error);
  1097. }
  1098. if (result && result.code) {
  1099. await addLog(`Step 7: Got login verification code: ${result.code}`);
  1100. // Switch to signup/auth tab and fill code
  1101. const signupTabId = await getTabId('signup-page');
  1102. if (signupTabId) {
  1103. await chrome.tabs.update(signupTabId, { active: true });
  1104. await sendToContentScript('signup-page', {
  1105. type: 'FILL_CODE',
  1106. step: 7,
  1107. source: 'background',
  1108. payload: { code: result.code },
  1109. });
  1110. } else {
  1111. throw new Error('Auth page tab was closed. Cannot fill verification code.');
  1112. }
  1113. }
  1114. }
  1115. // ============================================================
  1116. // Step 8: Complete OAuth (auto click + localhost listener)
  1117. // ============================================================
  1118. let webNavListener = null;
  1119. async function executeStep8(state) {
  1120. if (!state.oauthUrl) {
  1121. throw new Error('No OAuth URL. Complete step 1 first.');
  1122. }
  1123. // Check if the signup tab already redirected to localhost before listener setup
  1124. const signupTabIdEarly = await getTabId('signup-page');
  1125. if (signupTabIdEarly) {
  1126. try {
  1127. const tab = await chrome.tabs.get(signupTabIdEarly);
  1128. if (tab.url && (tab.url.startsWith('http://localhost') || tab.url.startsWith('http://127.0.0.1'))) {
  1129. await addLog(`Step 8: Localhost redirect already captured: ${tab.url}`, 'ok');
  1130. await setState({ localhostUrl: tab.url });
  1131. broadcastDataUpdate({ localhostUrl: tab.url });
  1132. return;
  1133. }
  1134. } catch {}
  1135. }
  1136. await addLog('Step 8: Setting up localhost redirect listener...');
  1137. // Register webNavigation listener (scoped to this step)
  1138. return new Promise((resolve, reject) => {
  1139. let resolved = false;
  1140. const isLocalhostUrl = (url) =>
  1141. url && (url.startsWith('http://localhost') || url.startsWith('http://127.0.0.1'));
  1142. const cleanupListeners = () => {
  1143. if (webNavListener) {
  1144. chrome.webNavigation.onBeforeNavigate.removeListener(webNavListener);
  1145. chrome.webNavigation.onCommitted.removeListener(webNavListener);
  1146. chrome.webNavigation.onErrorOccurred.removeListener(webNavListener);
  1147. webNavListener = null;
  1148. }
  1149. };
  1150. const captureLocalhostUrl = (url) => {
  1151. if (resolved) return;
  1152. resolved = true;
  1153. cleanupListeners();
  1154. clearTimeout(timeout);
  1155. setState({ localhostUrl: url }).then(() => {
  1156. addLog(`Step 8: Captured localhost URL: ${url}`, 'ok');
  1157. setStepStatus(8, 'completed');
  1158. notifyStepComplete(8, { localhostUrl: url });
  1159. broadcastDataUpdate({ localhostUrl: url });
  1160. resolve();
  1161. });
  1162. };
  1163. const timeout = setTimeout(() => {
  1164. cleanupListeners();
  1165. reject(new Error('Localhost redirect not captured after 120s. Step 8 click may have been blocked.'));
  1166. }, 120000);
  1167. webNavListener = (details) => {
  1168. if (details.frameId === 0 && isLocalhostUrl(details.url)) {
  1169. console.log(LOG_PREFIX, `Captured localhost redirect: ${details.url}`);
  1170. captureLocalhostUrl(details.url);
  1171. }
  1172. };
  1173. chrome.webNavigation.onBeforeNavigate.addListener(webNavListener);
  1174. chrome.webNavigation.onCommitted.addListener(webNavListener);
  1175. chrome.webNavigation.onErrorOccurred.addListener(webNavListener);
  1176. // After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
  1177. // with a "继续" button. We locate the button in-page, then click it through
  1178. // the debugger Input API directly.
  1179. (async () => {
  1180. try {
  1181. let signupTabId = await getTabId('signup-page');
  1182. if (signupTabId) {
  1183. await chrome.tabs.update(signupTabId, { active: true });
  1184. await addLog('Step 8: Switched to auth page. Preparing debugger click...');
  1185. } else {
  1186. signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
  1187. await addLog('Step 8: Auth tab reopened. Preparing debugger click...');
  1188. }
  1189. const clickResult = await sendToContentScript('signup-page', {
  1190. type: 'STEP8_FIND_AND_CLICK',
  1191. source: 'background',
  1192. payload: {},
  1193. });
  1194. if (clickResult?.error) {
  1195. throw new Error(clickResult.error);
  1196. }
  1197. if (!resolved) {
  1198. await clickWithDebugger(signupTabId, clickResult?.rect);
  1199. await addLog('Step 8: Debugger click dispatched, waiting for redirect...');
  1200. // Fallback: poll tab URL in case webNavigation listeners missed the redirect
  1201. for (let i = 0; i < 30 && !resolved; i++) {
  1202. await new Promise(r => setTimeout(r, 1000));
  1203. try {
  1204. const tab = await chrome.tabs.get(signupTabId);
  1205. if (isLocalhostUrl(tab.url)) {
  1206. captureLocalhostUrl(tab.url);
  1207. break;
  1208. }
  1209. } catch { break; }
  1210. }
  1211. }
  1212. } catch (err) {
  1213. clearTimeout(timeout);
  1214. cleanupListeners();
  1215. reject(err);
  1216. }
  1217. })();
  1218. });
  1219. }
  1220. // ============================================================
  1221. // Step 9: VPS Verify (via vps-panel.js)
  1222. // ============================================================
  1223. async function executeStep9(state) {
  1224. if (!state.localhostUrl) {
  1225. throw new Error('No localhost URL. Complete step 8 first.');
  1226. }
  1227. if (!state.vpsUrl) {
  1228. throw new Error('VPS URL not set. Please enter VPS URL in the side panel.');
  1229. }
  1230. await addLog('Step 9: Opening VPS panel...');
  1231. let tabId = await getTabId('vps-panel');
  1232. const alive = tabId && await isTabAlive('vps-panel');
  1233. if (!alive) {
  1234. // Create new tab in the automation window
  1235. const wid = await ensureAutomationWindowId();
  1236. const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true, windowId: wid });
  1237. tabId = tab.id;
  1238. await new Promise(resolve => {
  1239. const listener = (tid, info) => {
  1240. if (tid === tabId && info.status === 'complete') {
  1241. chrome.tabs.onUpdated.removeListener(listener);
  1242. resolve();
  1243. }
  1244. };
  1245. chrome.tabs.onUpdated.addListener(listener);
  1246. });
  1247. } else {
  1248. await chrome.tabs.update(tabId, { active: true });
  1249. }
  1250. // Inject scripts directly and wait for them to be ready
  1251. await chrome.scripting.executeScript({
  1252. target: { tabId },
  1253. files: ['content/utils.js', 'content/vps-panel.js'],
  1254. });
  1255. await new Promise(r => setTimeout(r, 1000));
  1256. // Send command directly — bypass queue/ready mechanism
  1257. await addLog(`Step 9: Filling callback URL...`);
  1258. await chrome.tabs.sendMessage(tabId, {
  1259. type: 'EXECUTE_STEP',
  1260. step: 9,
  1261. source: 'background',
  1262. payload: { localhostUrl: state.localhostUrl },
  1263. });
  1264. }
  1265. // ============================================================
  1266. // Open Side Panel on extension icon click
  1267. // ============================================================
  1268. chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });