background.js 43 KB

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